utils.tsx 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946
  1. import {browserHistory} from 'react-router';
  2. import {Location} from 'history';
  3. import isNumber from 'lodash/isNumber';
  4. import isString from 'lodash/isString';
  5. import maxBy from 'lodash/maxBy';
  6. import set from 'lodash/set';
  7. import moment from 'moment';
  8. import {
  9. TOGGLE_BORDER_BOX,
  10. TOGGLE_BUTTON_MAX_WIDTH,
  11. } from 'sentry/components/performance/waterfall/treeConnector';
  12. import {Organization} from 'sentry/types';
  13. import {EntryType, EventTransaction} from 'sentry/types/event';
  14. import {assert} from 'sentry/types/utils';
  15. import trackAdvancedAnalyticsEvent from 'sentry/utils/analytics/trackAdvancedAnalyticsEvent';
  16. import {WebVital} from 'sentry/utils/fields';
  17. import {TraceError} from 'sentry/utils/performance/quickTrace/types';
  18. import {WEB_VITAL_DETAILS} from 'sentry/utils/performance/vitals/constants';
  19. import {getPerformanceTransaction} from 'sentry/utils/performanceForSentry';
  20. import {Theme} from 'sentry/utils/theme';
  21. import {MERGE_LABELS_THRESHOLD_PERCENT} from './constants';
  22. import {
  23. EnhancedSpan,
  24. GapSpanType,
  25. OrphanSpanType,
  26. OrphanTreeDepth,
  27. ParsedTraceType,
  28. ProcessedSpanType,
  29. RawSpanType,
  30. SpanEntry,
  31. SpanType,
  32. TraceContextType,
  33. TreeDepthType,
  34. } from './types';
  35. export const isValidSpanID = (maybeSpanID: any) =>
  36. isString(maybeSpanID) && maybeSpanID.length > 0;
  37. export const setSpansOnTransaction = (spanCount: number) => {
  38. const transaction = getPerformanceTransaction();
  39. if (!transaction || spanCount === 0) {
  40. return;
  41. }
  42. const spanCountGroups = [10, 100, 200, 300, 400, 500, 600, 700, 800, 900, 1001];
  43. const spanGroup = spanCountGroups.find(g => spanCount <= g) || -1;
  44. transaction.setTag('ui.spanCount', spanCount);
  45. transaction.setTag('ui.spanCount.grouped', `<=${spanGroup}`);
  46. };
  47. export type SpanBoundsType = {endTimestamp: number; startTimestamp: number};
  48. export type SpanGeneratedBoundsType =
  49. | {isSpanVisibleInView: boolean; type: 'TRACE_TIMESTAMPS_EQUAL'}
  50. | {isSpanVisibleInView: boolean; type: 'INVALID_VIEW_WINDOW'}
  51. | {
  52. isSpanVisibleInView: boolean;
  53. start: number;
  54. type: 'TIMESTAMPS_EQUAL';
  55. width: number;
  56. }
  57. | {
  58. end: number;
  59. isSpanVisibleInView: boolean;
  60. start: number;
  61. type: 'TIMESTAMPS_REVERSED';
  62. }
  63. | {
  64. end: number;
  65. isSpanVisibleInView: boolean;
  66. start: number;
  67. type: 'TIMESTAMPS_STABLE';
  68. };
  69. export type SpanViewBoundsType = {
  70. isSpanVisibleInView: boolean;
  71. left: undefined | number;
  72. warning: undefined | string;
  73. width: undefined | number;
  74. };
  75. const normalizeTimestamps = (spanBounds: SpanBoundsType): SpanBoundsType => {
  76. const {startTimestamp, endTimestamp} = spanBounds;
  77. if (startTimestamp > endTimestamp) {
  78. return {startTimestamp: endTimestamp, endTimestamp: startTimestamp};
  79. }
  80. return spanBounds;
  81. };
  82. export enum TimestampStatus {
  83. Stable,
  84. Reversed,
  85. Equal,
  86. }
  87. export const parseSpanTimestamps = (spanBounds: SpanBoundsType): TimestampStatus => {
  88. const startTimestamp: number = spanBounds.startTimestamp;
  89. const endTimestamp: number = spanBounds.endTimestamp;
  90. if (startTimestamp < endTimestamp) {
  91. return TimestampStatus.Stable;
  92. }
  93. if (startTimestamp === endTimestamp) {
  94. return TimestampStatus.Equal;
  95. }
  96. return TimestampStatus.Reversed;
  97. };
  98. // given the start and end trace timestamps, and the view window, we want to generate a function
  99. // that'll output the relative %'s for the width and placements relative to the left-hand side.
  100. //
  101. // The view window (viewStart and viewEnd) are percentage values (between 0% and 100%), they correspond to the window placement
  102. // between the start and end trace timestamps.
  103. export const boundsGenerator = (bounds: {
  104. // unix timestamp
  105. traceEndTimestamp: number;
  106. traceStartTimestamp: number;
  107. // in [0, 1]
  108. viewEnd: number;
  109. // unix timestamp
  110. viewStart: number; // in [0, 1]
  111. }) => {
  112. const {viewStart, viewEnd} = bounds;
  113. const {startTimestamp: traceStartTimestamp, endTimestamp: traceEndTimestamp} =
  114. normalizeTimestamps({
  115. startTimestamp: bounds.traceStartTimestamp,
  116. endTimestamp: bounds.traceEndTimestamp,
  117. });
  118. // viewStart and viewEnd are percentage values (%) of the view window relative to the left
  119. // side of the trace view minimap
  120. // invariant: viewStart <= viewEnd
  121. // duration of the entire trace in seconds
  122. const traceDuration = traceEndTimestamp - traceStartTimestamp;
  123. const viewStartTimestamp = traceStartTimestamp + viewStart * traceDuration;
  124. const viewEndTimestamp = traceEndTimestamp - (1 - viewEnd) * traceDuration;
  125. const viewDuration = viewEndTimestamp - viewStartTimestamp;
  126. return (spanBounds: SpanBoundsType): SpanGeneratedBoundsType => {
  127. // TODO: alberto.... refactor so this is impossible 😠
  128. if (traceDuration <= 0) {
  129. return {
  130. type: 'TRACE_TIMESTAMPS_EQUAL',
  131. isSpanVisibleInView: true,
  132. };
  133. }
  134. if (viewDuration <= 0) {
  135. return {
  136. type: 'INVALID_VIEW_WINDOW',
  137. isSpanVisibleInView: true,
  138. };
  139. }
  140. const {startTimestamp, endTimestamp} = normalizeTimestamps(spanBounds);
  141. const timestampStatus = parseSpanTimestamps(spanBounds);
  142. const start = (startTimestamp - viewStartTimestamp) / viewDuration;
  143. const end = (endTimestamp - viewStartTimestamp) / viewDuration;
  144. const isSpanVisibleInView = end > 0 && start < 1;
  145. switch (timestampStatus) {
  146. case TimestampStatus.Equal: {
  147. return {
  148. type: 'TIMESTAMPS_EQUAL',
  149. start,
  150. width: 1,
  151. // a span bar is visible even if they're at the extreme ends of the view selection.
  152. // these edge cases are:
  153. // start == end == 0, and
  154. // start == end == 1
  155. isSpanVisibleInView: end >= 0 && start <= 1,
  156. };
  157. }
  158. case TimestampStatus.Reversed: {
  159. return {
  160. type: 'TIMESTAMPS_REVERSED',
  161. start,
  162. end,
  163. isSpanVisibleInView,
  164. };
  165. }
  166. case TimestampStatus.Stable: {
  167. return {
  168. type: 'TIMESTAMPS_STABLE',
  169. start,
  170. end,
  171. isSpanVisibleInView,
  172. };
  173. }
  174. default: {
  175. const _exhaustiveCheck: never = timestampStatus;
  176. return _exhaustiveCheck;
  177. }
  178. }
  179. };
  180. };
  181. export function generateRootSpan(trace: ParsedTraceType): RawSpanType {
  182. const rootSpan: RawSpanType = {
  183. trace_id: trace.traceID,
  184. span_id: trace.rootSpanID,
  185. parent_span_id: trace.parentSpanID,
  186. start_timestamp: trace.traceStartTimestamp,
  187. timestamp: trace.traceEndTimestamp,
  188. op: trace.op,
  189. description: trace.description,
  190. data: {},
  191. status: trace.rootSpanStatus,
  192. hash: trace.hash,
  193. exclusive_time: trace.exclusiveTime,
  194. };
  195. return rootSpan;
  196. }
  197. // start and end are assumed to be unix timestamps with fractional seconds
  198. export function getTraceDateTimeRange(input: {end: number; start: number}): {
  199. end: string;
  200. start: string;
  201. } {
  202. const start = moment
  203. .unix(input.start)
  204. .subtract(12, 'hours')
  205. .utc()
  206. .format('YYYY-MM-DDTHH:mm:ss.SSS');
  207. const end = moment
  208. .unix(input.end)
  209. .add(12, 'hours')
  210. .utc()
  211. .format('YYYY-MM-DDTHH:mm:ss.SSS');
  212. return {
  213. start,
  214. end,
  215. };
  216. }
  217. export function isGapSpan(span: ProcessedSpanType): span is GapSpanType {
  218. if ('type' in span) {
  219. return span.type === 'gap';
  220. }
  221. return false;
  222. }
  223. export function isOrphanSpan(span: ProcessedSpanType): span is OrphanSpanType {
  224. if ('type' in span) {
  225. if (span.type === 'orphan') {
  226. return true;
  227. }
  228. if (span.type === 'gap') {
  229. return span.isOrphan;
  230. }
  231. }
  232. return false;
  233. }
  234. export function getSpanID(span: ProcessedSpanType, defaultSpanID: string = ''): string {
  235. if (isGapSpan(span)) {
  236. return defaultSpanID;
  237. }
  238. return span.span_id;
  239. }
  240. export function getSpanOperation(span: ProcessedSpanType): string | undefined {
  241. if (isGapSpan(span)) {
  242. return undefined;
  243. }
  244. return span.op;
  245. }
  246. export function getSpanTraceID(span: ProcessedSpanType): string {
  247. if (isGapSpan(span)) {
  248. return 'gap-span';
  249. }
  250. return span.trace_id;
  251. }
  252. export function getSpanParentSpanID(span: ProcessedSpanType): string | undefined {
  253. if (isGapSpan(span)) {
  254. return 'gap-span';
  255. }
  256. return span.parent_span_id;
  257. }
  258. export function getTraceContext(
  259. event: Readonly<EventTransaction>
  260. ): TraceContextType | undefined {
  261. return event?.contexts?.trace;
  262. }
  263. export function parseTrace(event: Readonly<EventTransaction>): ParsedTraceType {
  264. const spanEntry = event.entries.find((entry: SpanEntry | any): entry is SpanEntry => {
  265. return entry.type === EntryType.SPANS;
  266. });
  267. const spans: Array<RawSpanType> = spanEntry?.data ?? [];
  268. const traceContext = getTraceContext(event);
  269. const traceID = (traceContext && traceContext.trace_id) || '';
  270. const rootSpanID = (traceContext && traceContext.span_id) || '';
  271. const rootSpanOpName = (traceContext && traceContext.op) || 'transaction';
  272. const description = traceContext && traceContext.description;
  273. const parentSpanID = traceContext && traceContext.parent_span_id;
  274. const rootSpanStatus = traceContext && traceContext.status;
  275. const hash = traceContext && traceContext.hash;
  276. const exclusiveTime = traceContext && traceContext.exclusive_time;
  277. if (!spanEntry || spans.length <= 0) {
  278. return {
  279. op: rootSpanOpName,
  280. childSpans: {},
  281. traceStartTimestamp: event.startTimestamp,
  282. traceEndTimestamp: event.endTimestamp,
  283. traceID,
  284. rootSpanID,
  285. rootSpanStatus,
  286. parentSpanID,
  287. spans: [],
  288. description,
  289. hash,
  290. exclusiveTime,
  291. };
  292. }
  293. // any span may be a parent of another span
  294. const potentialParents = new Set(
  295. spans.map(span => {
  296. return span.span_id;
  297. })
  298. );
  299. // the root transaction span is a parent of all other spans
  300. potentialParents.add(rootSpanID);
  301. // we reduce spans to become an object mapping span ids to their children
  302. const init: ParsedTraceType = {
  303. op: rootSpanOpName,
  304. childSpans: {},
  305. traceStartTimestamp: event.startTimestamp,
  306. traceEndTimestamp: event.endTimestamp,
  307. traceID,
  308. rootSpanID,
  309. rootSpanStatus,
  310. parentSpanID,
  311. spans,
  312. description,
  313. hash,
  314. exclusiveTime,
  315. };
  316. const reduced: ParsedTraceType = spans.reduce((acc, inputSpan) => {
  317. let span: SpanType = inputSpan;
  318. const parentSpanId = getSpanParentSpanID(span);
  319. const hasParent = parentSpanId && potentialParents.has(parentSpanId);
  320. if (!isValidSpanID(parentSpanId) || !hasParent) {
  321. // this span is considered an orphan with respect to the spans within this transaction.
  322. // although the span is an orphan, it's still a descendant of this transaction,
  323. // so we set its parent span id to be the root transaction span's id
  324. span.parent_span_id = rootSpanID;
  325. span = {
  326. type: 'orphan',
  327. ...span,
  328. } as OrphanSpanType;
  329. }
  330. assert(span.parent_span_id);
  331. // get any span children whose parent_span_id is equal to span.parent_span_id,
  332. // otherwise start with an empty array
  333. const spanChildren: Array<SpanType> = acc.childSpans[span.parent_span_id] ?? [];
  334. spanChildren.push(span);
  335. set(acc.childSpans, span.parent_span_id, spanChildren);
  336. // set trace start & end timestamps based on given span's start and end timestamps
  337. if (!acc.traceStartTimestamp || span.start_timestamp < acc.traceStartTimestamp) {
  338. acc.traceStartTimestamp = span.start_timestamp;
  339. }
  340. // establish trace end timestamp
  341. const hasEndTimestamp = isNumber(span.timestamp);
  342. if (!acc.traceEndTimestamp) {
  343. if (hasEndTimestamp) {
  344. acc.traceEndTimestamp = span.timestamp;
  345. return acc;
  346. }
  347. acc.traceEndTimestamp = span.start_timestamp;
  348. return acc;
  349. }
  350. if (hasEndTimestamp && span.timestamp! > acc.traceEndTimestamp) {
  351. acc.traceEndTimestamp = span.timestamp;
  352. return acc;
  353. }
  354. if (span.start_timestamp > acc.traceEndTimestamp) {
  355. acc.traceEndTimestamp = span.start_timestamp;
  356. }
  357. return acc;
  358. }, init);
  359. // sort span children
  360. Object.values(reduced.childSpans).forEach(spanChildren => {
  361. spanChildren.sort(sortSpans);
  362. });
  363. return reduced;
  364. }
  365. function sortSpans(firstSpan: SpanType, secondSpan: SpanType) {
  366. // orphan spans come after non-orphan spans.
  367. if (isOrphanSpan(firstSpan) && !isOrphanSpan(secondSpan)) {
  368. // sort secondSpan before firstSpan
  369. return 1;
  370. }
  371. if (!isOrphanSpan(firstSpan) && isOrphanSpan(secondSpan)) {
  372. // sort firstSpan before secondSpan
  373. return -1;
  374. }
  375. // sort spans by their start timestamp in ascending order
  376. if (firstSpan.start_timestamp < secondSpan.start_timestamp) {
  377. // sort firstSpan before secondSpan
  378. return -1;
  379. }
  380. if (firstSpan.start_timestamp === secondSpan.start_timestamp) {
  381. return 0;
  382. }
  383. // sort secondSpan before firstSpan
  384. return 1;
  385. }
  386. export function isOrphanTreeDepth(
  387. treeDepth: TreeDepthType
  388. ): treeDepth is OrphanTreeDepth {
  389. if (typeof treeDepth === 'number') {
  390. return false;
  391. }
  392. return treeDepth?.type === 'orphan';
  393. }
  394. export function unwrapTreeDepth(treeDepth: TreeDepthType): number {
  395. if (isOrphanTreeDepth(treeDepth)) {
  396. return treeDepth.depth;
  397. }
  398. return treeDepth;
  399. }
  400. export function isEventFromBrowserJavaScriptSDK(event: EventTransaction): boolean {
  401. const sdkName = event.sdk?.name;
  402. if (!sdkName) {
  403. return false;
  404. }
  405. // based on https://github.com/getsentry/sentry-javascript/blob/master/packages/browser/src/version.ts
  406. return [
  407. 'sentry.javascript.browser',
  408. 'sentry.javascript.react',
  409. 'sentry.javascript.gatsby',
  410. 'sentry.javascript.ember',
  411. 'sentry.javascript.vue',
  412. 'sentry.javascript.angular',
  413. 'sentry.javascript.nextjs',
  414. 'sentry.javascript.electron',
  415. 'sentry.javascript.remix',
  416. 'sentry.javascript.svelte',
  417. ].includes(sdkName.toLowerCase());
  418. }
  419. // Durationless ops from: https://github.com/getsentry/sentry-javascript/blob/0defcdcc2dfe719343efc359d58c3f90743da2cd/packages/apm/src/integrations/tracing.ts#L629-L688
  420. // PerformanceMark: Duration is 0 as per https://developer.mozilla.org/en-US/docs/Web/API/PerformanceMark
  421. // PerformancePaintTiming: Duration is 0 as per https://developer.mozilla.org/en-US/docs/Web/API/PerformancePaintTiming
  422. export const durationlessBrowserOps = ['mark', 'paint'];
  423. type Measurements = {
  424. [name: string]: {
  425. failedThreshold: boolean;
  426. timestamp: number;
  427. value: number | undefined;
  428. };
  429. };
  430. type VerticalMark = {
  431. failedThreshold: boolean;
  432. marks: Measurements;
  433. };
  434. function hasFailedThreshold(marks: Measurements): boolean {
  435. const names = Object.keys(marks);
  436. const records = Object.values(WEB_VITAL_DETAILS).filter(vital =>
  437. names.includes(vital.slug)
  438. );
  439. return records.some(record => {
  440. const {value} = marks[record.slug];
  441. if (typeof value === 'number' && typeof record.poorThreshold === 'number') {
  442. return value >= record.poorThreshold;
  443. }
  444. return false;
  445. });
  446. }
  447. export function getMeasurements(
  448. event: EventTransaction,
  449. generateBounds: (bounds: SpanBoundsType) => SpanGeneratedBoundsType
  450. ): Map<number, VerticalMark> {
  451. if (!event.measurements || !event.startTimestamp) {
  452. return new Map();
  453. }
  454. const {startTimestamp} = event;
  455. // Note: CLS and INP should not be included here, since they are not timeline-based measurements.
  456. const allowedVitals = new Set<string>([
  457. WebVital.FCP,
  458. WebVital.FP,
  459. WebVital.FID,
  460. WebVital.LCP,
  461. WebVital.TTFB,
  462. ]);
  463. const measurements = Object.keys(event.measurements)
  464. .filter(name => allowedVitals.has(`measurements.${name}`))
  465. .map(name => {
  466. const associatedMeasurement = event.measurements![name];
  467. return {
  468. name,
  469. // Time timestamp is in seconds, but the measurement value is given in ms so convert it here
  470. timestamp: startTimestamp + associatedMeasurement.value / 1000,
  471. value: associatedMeasurement ? associatedMeasurement.value : undefined,
  472. };
  473. });
  474. const mergedMeasurements = new Map<number, VerticalMark>();
  475. measurements.forEach(measurement => {
  476. const name = measurement.name;
  477. const value = measurement.value;
  478. const bounds = generateBounds({
  479. startTimestamp: measurement.timestamp,
  480. endTimestamp: measurement.timestamp,
  481. });
  482. // This condition will never be hit, since we're using the same value for start and end in generateBounds
  483. // I've put this condition here to prevent the TS linter from complaining
  484. if (bounds.type !== 'TIMESTAMPS_EQUAL') {
  485. return;
  486. }
  487. const roundedPos = Math.round(bounds.start * 100);
  488. // Compare this position with the position of the other measurements, to determine if
  489. // they are close enough to be bucketed together
  490. for (const [otherPos] of mergedMeasurements) {
  491. const positionDelta = Math.abs(otherPos - roundedPos);
  492. if (positionDelta <= MERGE_LABELS_THRESHOLD_PERCENT) {
  493. const verticalMark = mergedMeasurements.get(otherPos)!;
  494. const {poorThreshold} = WEB_VITAL_DETAILS[`measurements.${name}`];
  495. verticalMark.marks = {
  496. ...verticalMark.marks,
  497. [name]: {
  498. value,
  499. timestamp: measurement.timestamp,
  500. failedThreshold: value ? value >= poorThreshold : false,
  501. },
  502. };
  503. if (!verticalMark.failedThreshold) {
  504. verticalMark.failedThreshold = hasFailedThreshold(verticalMark.marks);
  505. }
  506. mergedMeasurements.set(otherPos, verticalMark);
  507. return;
  508. }
  509. }
  510. const {poorThreshold} = WEB_VITAL_DETAILS[`measurements.${name}`];
  511. const marks = {
  512. [name]: {
  513. value,
  514. timestamp: measurement.timestamp,
  515. failedThreshold: value ? value >= poorThreshold : false,
  516. },
  517. };
  518. mergedMeasurements.set(roundedPos, {
  519. marks,
  520. failedThreshold: hasFailedThreshold(marks),
  521. });
  522. });
  523. return mergedMeasurements;
  524. }
  525. export function getMeasurementBounds(
  526. timestamp: number,
  527. generateBounds: (bounds: SpanBoundsType) => SpanGeneratedBoundsType
  528. ): SpanViewBoundsType {
  529. const bounds = generateBounds({
  530. startTimestamp: timestamp,
  531. endTimestamp: timestamp,
  532. });
  533. switch (bounds.type) {
  534. case 'TRACE_TIMESTAMPS_EQUAL':
  535. case 'INVALID_VIEW_WINDOW': {
  536. return {
  537. warning: undefined,
  538. left: undefined,
  539. width: undefined,
  540. isSpanVisibleInView: bounds.isSpanVisibleInView,
  541. };
  542. }
  543. case 'TIMESTAMPS_EQUAL': {
  544. return {
  545. warning: undefined,
  546. left: bounds.start,
  547. width: 0.00001,
  548. isSpanVisibleInView: bounds.isSpanVisibleInView,
  549. };
  550. }
  551. case 'TIMESTAMPS_REVERSED': {
  552. return {
  553. warning: undefined,
  554. left: bounds.start,
  555. width: bounds.end - bounds.start,
  556. isSpanVisibleInView: bounds.isSpanVisibleInView,
  557. };
  558. }
  559. case 'TIMESTAMPS_STABLE': {
  560. return {
  561. warning: void 0,
  562. left: bounds.start,
  563. width: bounds.end - bounds.start,
  564. isSpanVisibleInView: bounds.isSpanVisibleInView,
  565. };
  566. }
  567. default: {
  568. const _exhaustiveCheck: never = bounds;
  569. return _exhaustiveCheck;
  570. }
  571. }
  572. }
  573. export function scrollToSpan(
  574. spanId: string,
  575. scrollToHash: (hash: string) => void,
  576. location: Location,
  577. organization: Organization
  578. ) {
  579. return (e: React.MouseEvent<Element>) => {
  580. // do not use the default anchor behaviour
  581. // because it will be hidden behind the minimap
  582. e.preventDefault();
  583. const hash = spanTargetHash(spanId);
  584. scrollToHash(hash);
  585. // TODO(txiao): This is causing a rerender of the whole page,
  586. // which can be slow.
  587. //
  588. // make sure to update the location
  589. browserHistory.push({
  590. ...location,
  591. hash,
  592. });
  593. trackAdvancedAnalyticsEvent('performance_views.event_details.anchor_span', {
  594. organization,
  595. span_id: spanId,
  596. });
  597. };
  598. }
  599. export function spanTargetHash(spanId: string): string {
  600. return `#span-${spanId}`;
  601. }
  602. export function getSiblingGroupKey(span: SpanType, occurrence?: number): string {
  603. if (occurrence !== undefined) {
  604. return `${span.op}.${span.description}.${occurrence}`;
  605. }
  606. return `${span.op}.${span.description}`;
  607. }
  608. export function getSpanGroupTimestamps(spanGroup: EnhancedSpan[]) {
  609. return spanGroup.reduce(
  610. (acc, spanGroupItem) => {
  611. const {start_timestamp, timestamp} = spanGroupItem.span;
  612. let newStartTimestamp = acc.startTimestamp;
  613. let newEndTimestamp = acc.endTimestamp;
  614. if (start_timestamp < newStartTimestamp) {
  615. newStartTimestamp = start_timestamp;
  616. }
  617. if (newEndTimestamp < timestamp) {
  618. newEndTimestamp = timestamp;
  619. }
  620. return {
  621. startTimestamp: newStartTimestamp,
  622. endTimestamp: newEndTimestamp,
  623. };
  624. },
  625. {
  626. startTimestamp: spanGroup[0].span.start_timestamp,
  627. endTimestamp: spanGroup[0].span.timestamp,
  628. }
  629. );
  630. }
  631. export function getSpanGroupBounds(
  632. spanGroup: EnhancedSpan[],
  633. generateBounds: (bounds: SpanBoundsType) => SpanGeneratedBoundsType
  634. ): SpanViewBoundsType {
  635. const {startTimestamp, endTimestamp} = getSpanGroupTimestamps(spanGroup);
  636. const bounds = generateBounds({
  637. startTimestamp,
  638. endTimestamp,
  639. });
  640. switch (bounds.type) {
  641. case 'TRACE_TIMESTAMPS_EQUAL':
  642. case 'INVALID_VIEW_WINDOW': {
  643. return {
  644. warning: void 0,
  645. left: void 0,
  646. width: void 0,
  647. isSpanVisibleInView: bounds.isSpanVisibleInView,
  648. };
  649. }
  650. case 'TIMESTAMPS_EQUAL': {
  651. return {
  652. warning: void 0,
  653. left: bounds.start,
  654. width: 0.00001,
  655. isSpanVisibleInView: bounds.isSpanVisibleInView,
  656. };
  657. }
  658. case 'TIMESTAMPS_REVERSED':
  659. case 'TIMESTAMPS_STABLE': {
  660. return {
  661. warning: void 0,
  662. left: bounds.start,
  663. width: bounds.end - bounds.start,
  664. isSpanVisibleInView: bounds.isSpanVisibleInView,
  665. };
  666. }
  667. default: {
  668. const _exhaustiveCheck: never = bounds;
  669. return _exhaustiveCheck;
  670. }
  671. }
  672. }
  673. export class SpansInViewMap {
  674. spanDepthsInView: Map<string, number>;
  675. treeDepthSum: number;
  676. length: number;
  677. isRootSpanInView: boolean;
  678. constructor(isRootSpanInView: boolean) {
  679. this.spanDepthsInView = new Map();
  680. this.treeDepthSum = 0;
  681. this.length = 0;
  682. this.isRootSpanInView = isRootSpanInView;
  683. }
  684. /**
  685. *
  686. * @param spanId
  687. * @param treeDepth
  688. * @returns false if the span is already stored, true otherwise
  689. */
  690. addSpan(spanId: string, treeDepth: number): boolean {
  691. if (this.spanDepthsInView.has(spanId)) {
  692. return false;
  693. }
  694. this.spanDepthsInView.set(spanId, treeDepth);
  695. this.length += 1;
  696. this.treeDepthSum += treeDepth;
  697. if (treeDepth === 0) {
  698. this.isRootSpanInView = true;
  699. }
  700. return true;
  701. }
  702. /**
  703. *
  704. * @param spanId
  705. * @returns false if the span does not exist within the span, true otherwise
  706. */
  707. removeSpan(spanId: string): boolean {
  708. if (!this.spanDepthsInView.has(spanId)) {
  709. return false;
  710. }
  711. const treeDepth = this.spanDepthsInView.get(spanId);
  712. this.spanDepthsInView.delete(spanId);
  713. this.length -= 1;
  714. this.treeDepthSum -= treeDepth!;
  715. if (treeDepth === 0) {
  716. this.isRootSpanInView = false;
  717. }
  718. return true;
  719. }
  720. has(spanId: string) {
  721. return this.spanDepthsInView.has(spanId);
  722. }
  723. getScrollVal() {
  724. if (this.isRootSpanInView) {
  725. return 0;
  726. }
  727. const avgDepth = Math.round(this.treeDepthSum / this.length);
  728. return avgDepth * (TOGGLE_BORDER_BOX / 2) - TOGGLE_BUTTON_MAX_WIDTH / 2;
  729. }
  730. }
  731. export function getCumulativeAlertLevelFromErrors(
  732. errors?: Pick<TraceError, 'level'>[]
  733. ): keyof Theme['alert'] | undefined {
  734. const highestErrorLevel = maxBy(
  735. errors || [],
  736. error => ERROR_LEVEL_WEIGHTS[error.level]
  737. )?.level;
  738. if (!highestErrorLevel) {
  739. return undefined;
  740. }
  741. return ERROR_LEVEL_TO_ALERT_TYPE[highestErrorLevel];
  742. }
  743. // Maps the six known error levels to one of three Alert component types
  744. const ERROR_LEVEL_TO_ALERT_TYPE: Record<TraceError['level'], keyof Theme['alert']> = {
  745. fatal: 'error',
  746. error: 'error',
  747. default: 'error',
  748. warning: 'warning',
  749. sample: 'info',
  750. info: 'info',
  751. };
  752. // Allows sorting errors according to their level of severity
  753. const ERROR_LEVEL_WEIGHTS: Record<TraceError['level'], number> = {
  754. fatal: 5,
  755. error: 4,
  756. default: 4,
  757. warning: 3,
  758. sample: 2,
  759. info: 1,
  760. };
  761. /**
  762. * Formats start and end unix timestamps by inserting a leading zero if needed, so they can have the same length
  763. */
  764. export function getFormattedTimeRangeWithLeadingZero(start: number, end: number) {
  765. const startStrings = String(start).split('.');
  766. const endStrings = String(end).split('.');
  767. if (startStrings.length !== endStrings.length) {
  768. return {
  769. start: String(start),
  770. end: String(end),
  771. };
  772. }
  773. const newTimestamps = startStrings.reduce(
  774. (acc, startString, index) => {
  775. if (startString.length > endStrings[index].length) {
  776. acc.start.push(startString);
  777. acc.end.push(endStrings[index].padStart(startString.length, '0'));
  778. return acc;
  779. }
  780. acc.start.push(startString.padStart(endStrings[index].length, '0'));
  781. acc.end.push(endStrings[index]);
  782. return acc;
  783. },
  784. {start: [], end: []} as {
  785. end: string[];
  786. start: string[];
  787. }
  788. );
  789. return {
  790. start: newTimestamps.start.join('.'),
  791. end: newTimestamps.end.join('.'),
  792. };
  793. }