traceTree.measurements.tsx 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174
  1. import type {Measurement} from 'sentry/types/event';
  2. import {MobileVital, WebVital} from 'sentry/utils/fields';
  3. import {
  4. MOBILE_VITAL_DETAILS,
  5. WEB_VITAL_DETAILS,
  6. } from 'sentry/utils/performance/vitals/constants';
  7. import type {Vital} from 'sentry/utils/performance/vitals/types';
  8. import type {TraceTree} from './traceTree';
  9. import type {TraceTreeNode} from './traceTreeNode';
  10. // cls is not included as it is a cumulative layout shift and not a single point in time
  11. const RENDERABLE_MEASUREMENTS = [
  12. WebVital.TTFB,
  13. WebVital.FP,
  14. WebVital.FCP,
  15. WebVital.LCP,
  16. MobileVital.TIME_TO_FULL_DISPLAY,
  17. MobileVital.TIME_TO_INITIAL_DISPLAY,
  18. ]
  19. .map(n => n.replace('measurements.', ''))
  20. .reduce(
  21. (acc, curr) => {
  22. acc[curr] = true;
  23. return acc;
  24. },
  25. {} as Record<string, boolean>
  26. );
  27. const WEB_VITALS = [
  28. WebVital.TTFB,
  29. WebVital.FP,
  30. WebVital.FCP,
  31. WebVital.LCP,
  32. WebVital.CLS,
  33. WebVital.FID,
  34. WebVital.INP,
  35. WebVital.REQUEST_TIME,
  36. ].map(n => n.replace('measurements.', ''));
  37. const MOBILE_VITALS = [
  38. MobileVital.APP_START_COLD,
  39. MobileVital.APP_START_WARM,
  40. MobileVital.TIME_TO_INITIAL_DISPLAY,
  41. MobileVital.TIME_TO_FULL_DISPLAY,
  42. MobileVital.FRAMES_TOTAL,
  43. MobileVital.FRAMES_SLOW,
  44. MobileVital.FRAMES_FROZEN,
  45. MobileVital.FRAMES_SLOW_RATE,
  46. MobileVital.FRAMES_FROZEN_RATE,
  47. MobileVital.STALL_COUNT,
  48. MobileVital.STALL_TOTAL_TIME,
  49. MobileVital.STALL_LONGEST_TIME,
  50. MobileVital.STALL_PERCENTAGE,
  51. ].map(n => n.replace('measurements.', ''));
  52. const WEB_VITALS_LOOKUP = new Set<string>(WEB_VITALS);
  53. const MOBILE_VITALS_LOOKUP = new Set<string>(MOBILE_VITALS);
  54. const COLLECTABLE_MEASUREMENTS = [...WEB_VITALS, ...MOBILE_VITALS].map(n =>
  55. n.replace('measurements.', '')
  56. );
  57. const MEASUREMENT_ACRONYM_MAPPING = {
  58. [MobileVital.TIME_TO_FULL_DISPLAY.replace('measurements.', '')]: 'TTFD',
  59. [MobileVital.TIME_TO_INITIAL_DISPLAY.replace('measurements.', '')]: 'TTID',
  60. };
  61. const MEASUREMENT_THRESHOLDS = {
  62. [WebVital.TTFB.replace('measurements.', '')]: 600,
  63. [WebVital.FP.replace('measurements.', '')]: 3000,
  64. [WebVital.FCP.replace('measurements.', '')]: 3000,
  65. [WebVital.LCP.replace('measurements.', '')]: 4000,
  66. [MobileVital.TIME_TO_INITIAL_DISPLAY.replace('measurements.', '')]: 2000,
  67. };
  68. export const TRACE_MEASUREMENT_LOOKUP: Record<string, Vital> = {};
  69. for (const key in {...MOBILE_VITAL_DETAILS, ...WEB_VITAL_DETAILS}) {
  70. TRACE_MEASUREMENT_LOOKUP[key.replace('measurements.', '')] = {
  71. ...MOBILE_VITAL_DETAILS[key as keyof typeof MOBILE_VITAL_DETAILS],
  72. ...WEB_VITAL_DETAILS[key as keyof typeof WEB_VITAL_DETAILS],
  73. };
  74. }
  75. function traceMeasurementToTimestamp(
  76. start_timestamp: number,
  77. measurement: number,
  78. unit: string
  79. ) {
  80. if (unit === 'second') {
  81. return (start_timestamp + measurement) * 1e3;
  82. }
  83. if (unit === 'millisecond') {
  84. return start_timestamp + measurement;
  85. }
  86. if (unit === 'nanosecond') {
  87. return (start_timestamp + measurement) * 1e-6;
  88. }
  89. throw new TypeError(`Unsupported measurement unit', ${unit}`);
  90. }
  91. // Collects measurements from a trace node and adds them to the indicators stored on trace tree
  92. export function collectTraceMeasurements(
  93. node: TraceTreeNode<TraceTree.NodeValue>,
  94. start_timestamp: number,
  95. measurements: Record<string, Measurement> | undefined,
  96. vitals: Map<TraceTreeNode<TraceTree.NodeValue>, TraceTree.CollectedVital[]>,
  97. vital_types: Set<'web' | 'mobile'>
  98. ): TraceTree.Indicator[] {
  99. const indicators: TraceTree.Indicator[] = [];
  100. if (!measurements) {
  101. return indicators;
  102. }
  103. for (const measurement of COLLECTABLE_MEASUREMENTS) {
  104. const value = measurements[measurement];
  105. if (!value || typeof value.value !== 'number') {
  106. continue;
  107. }
  108. if (!vitals.has(node)) {
  109. vitals.set(node, []);
  110. }
  111. if (WEB_VITALS_LOOKUP.has(measurement)) {
  112. vital_types.add('web');
  113. }
  114. if (MOBILE_VITALS_LOOKUP.has(measurement)) {
  115. vital_types.add('mobile');
  116. }
  117. const score = Math.round(
  118. (measurements[`score.${measurement}`]?.value! /
  119. measurements[`score.weight.${measurement}`]?.value!) *
  120. 100
  121. );
  122. const vital = vitals.get(node)!;
  123. vital.push({
  124. key: measurement,
  125. measurement: value,
  126. score,
  127. });
  128. if (!RENDERABLE_MEASUREMENTS[measurement]) {
  129. continue;
  130. }
  131. const timestamp = traceMeasurementToTimestamp(
  132. start_timestamp,
  133. value.value,
  134. value.unit ?? 'millisecond'
  135. );
  136. const indicator: TraceTree.Indicator = {
  137. start: timestamp,
  138. duration: 0,
  139. measurement: value,
  140. poor: MEASUREMENT_THRESHOLDS[measurement]
  141. ? value.value > MEASUREMENT_THRESHOLDS[measurement]
  142. : false,
  143. type: measurement as TraceTree.Indicator['type'],
  144. label: (MEASUREMENT_ACRONYM_MAPPING[measurement] ?? measurement).toUpperCase(),
  145. score,
  146. };
  147. indicators.push(indicator);
  148. }
  149. return indicators;
  150. }