utils.tsx 24 KB

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