utils.tsx 24 KB

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