utils.tsx 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891
  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 {TraceError} from 'sentry/utils/performance/quickTrace/types';
  17. import {WEB_VITAL_DETAILS} from 'sentry/utils/performance/vitals/constants';
  18. import {getPerformanceTransaction} from 'sentry/utils/performanceForSentry';
  19. import {Theme} from 'sentry/utils/theme';
  20. import {MERGE_LABELS_THRESHOLD_PERCENT} from './constants';
  21. import {
  22. EnhancedSpan,
  23. FocusedSpanIDMap,
  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. ].includes(sdkName.toLowerCase());
  416. }
  417. // Durationless ops from: https://github.com/getsentry/sentry-javascript/blob/0defcdcc2dfe719343efc359d58c3f90743da2cd/packages/apm/src/integrations/tracing.ts#L629-L688
  418. // PerformanceMark: Duration is 0 as per https://developer.mozilla.org/en-US/docs/Web/API/PerformanceMark
  419. // PerformancePaintTiming: Duration is 0 as per https://developer.mozilla.org/en-US/docs/Web/API/PerformancePaintTiming
  420. export const durationlessBrowserOps = ['mark', 'paint'];
  421. type Measurements = {
  422. [name: string]: {
  423. timestamp: number;
  424. value: number | undefined;
  425. };
  426. };
  427. type VerticalMark = {
  428. failedThreshold: boolean;
  429. marks: Measurements;
  430. };
  431. function hasFailedThreshold(marks: Measurements): boolean {
  432. const names = Object.keys(marks);
  433. const records = Object.values(WEB_VITAL_DETAILS).filter(vital =>
  434. names.includes(vital.slug)
  435. );
  436. return records.some(record => {
  437. const {value} = marks[record.slug];
  438. if (typeof value === 'number' && typeof record.poorThreshold === 'number') {
  439. return value >= record.poorThreshold;
  440. }
  441. return false;
  442. });
  443. }
  444. export function getMeasurements(
  445. event: EventTransaction,
  446. generateBounds: (bounds: SpanBoundsType) => SpanGeneratedBoundsType
  447. ): Map<number, VerticalMark> {
  448. if (!event.measurements) {
  449. return new Map();
  450. }
  451. const measurements = Object.keys(event.measurements)
  452. .filter(name => name.startsWith('mark.'))
  453. .map(name => {
  454. const slug = name.slice('mark.'.length);
  455. const associatedMeasurement = event.measurements![slug];
  456. return {
  457. name,
  458. timestamp: event.measurements![name].value,
  459. value: associatedMeasurement ? associatedMeasurement.value : undefined,
  460. };
  461. });
  462. const mergedMeasurements = new Map<number, VerticalMark>();
  463. measurements.forEach(measurement => {
  464. const name = measurement.name.slice('mark.'.length);
  465. const value = measurement.value;
  466. const bounds = generateBounds({
  467. startTimestamp: measurement.timestamp,
  468. endTimestamp: measurement.timestamp,
  469. });
  470. // This condition will never be hit, since we're using the same value for start and end in generateBounds
  471. // I've put this condition here to prevent the TS linter from complaining
  472. if (bounds.type !== 'TIMESTAMPS_EQUAL') {
  473. return;
  474. }
  475. const roundedPos = Math.round(bounds.start * 100);
  476. // Compare this position with the position of the other measurements, to determine if
  477. // they are close enough to be bucketed together
  478. for (const [otherPos] of mergedMeasurements) {
  479. const positionDelta = Math.abs(otherPos - roundedPos);
  480. if (positionDelta <= MERGE_LABELS_THRESHOLD_PERCENT) {
  481. const verticalMark = mergedMeasurements.get(otherPos)!;
  482. verticalMark.marks = {
  483. ...verticalMark.marks,
  484. [name]: {
  485. value,
  486. timestamp: measurement.timestamp,
  487. },
  488. };
  489. if (!verticalMark.failedThreshold) {
  490. verticalMark.failedThreshold = hasFailedThreshold(verticalMark.marks);
  491. }
  492. mergedMeasurements.set(otherPos, verticalMark);
  493. return;
  494. }
  495. }
  496. const marks = {
  497. [name]: {value, timestamp: measurement.timestamp},
  498. };
  499. mergedMeasurements.set(roundedPos, {
  500. marks,
  501. failedThreshold: hasFailedThreshold(marks),
  502. });
  503. });
  504. return mergedMeasurements;
  505. }
  506. export function getMeasurementBounds(
  507. timestamp: number,
  508. generateBounds: (bounds: SpanBoundsType) => SpanGeneratedBoundsType
  509. ): SpanViewBoundsType {
  510. const bounds = generateBounds({
  511. startTimestamp: timestamp,
  512. endTimestamp: timestamp,
  513. });
  514. switch (bounds.type) {
  515. case 'TRACE_TIMESTAMPS_EQUAL':
  516. case 'INVALID_VIEW_WINDOW': {
  517. return {
  518. warning: undefined,
  519. left: undefined,
  520. width: undefined,
  521. isSpanVisibleInView: bounds.isSpanVisibleInView,
  522. };
  523. }
  524. case 'TIMESTAMPS_EQUAL': {
  525. return {
  526. warning: undefined,
  527. left: bounds.start,
  528. width: 0.00001,
  529. isSpanVisibleInView: bounds.isSpanVisibleInView,
  530. };
  531. }
  532. case 'TIMESTAMPS_REVERSED': {
  533. return {
  534. warning: undefined,
  535. left: bounds.start,
  536. width: bounds.end - bounds.start,
  537. isSpanVisibleInView: bounds.isSpanVisibleInView,
  538. };
  539. }
  540. case 'TIMESTAMPS_STABLE': {
  541. return {
  542. warning: void 0,
  543. left: bounds.start,
  544. width: bounds.end - bounds.start,
  545. isSpanVisibleInView: bounds.isSpanVisibleInView,
  546. };
  547. }
  548. default: {
  549. const _exhaustiveCheck: never = bounds;
  550. return _exhaustiveCheck;
  551. }
  552. }
  553. }
  554. export function scrollToSpan(
  555. spanId: string,
  556. scrollToHash: (hash: string) => void,
  557. location: Location,
  558. organization: Organization
  559. ) {
  560. return (e: React.MouseEvent<Element>) => {
  561. // do not use the default anchor behaviour
  562. // because it will be hidden behind the minimap
  563. e.preventDefault();
  564. const hash = spanTargetHash(spanId);
  565. scrollToHash(hash);
  566. // TODO(txiao): This is causing a rerender of the whole page,
  567. // which can be slow.
  568. //
  569. // make sure to update the location
  570. browserHistory.push({
  571. ...location,
  572. hash,
  573. });
  574. trackAdvancedAnalyticsEvent('performance_views.event_details.anchor_span', {
  575. organization,
  576. span_id: spanId,
  577. });
  578. };
  579. }
  580. export function spanTargetHash(spanId: string): string {
  581. return `#span-${spanId}`;
  582. }
  583. export function getSiblingGroupKey(span: SpanType, occurrence?: number): string {
  584. if (occurrence !== undefined) {
  585. return `${span.op}.${span.description}.${occurrence}`;
  586. }
  587. return `${span.op}.${span.description}`;
  588. }
  589. export function getSpanGroupTimestamps(spanGroup: EnhancedSpan[]) {
  590. return spanGroup.reduce(
  591. (acc, spanGroupItem) => {
  592. const {start_timestamp, timestamp} = spanGroupItem.span;
  593. let newStartTimestamp = acc.startTimestamp;
  594. let newEndTimestamp = acc.endTimestamp;
  595. if (start_timestamp < newStartTimestamp) {
  596. newStartTimestamp = start_timestamp;
  597. }
  598. if (newEndTimestamp < timestamp) {
  599. newEndTimestamp = timestamp;
  600. }
  601. return {
  602. startTimestamp: newStartTimestamp,
  603. endTimestamp: newEndTimestamp,
  604. };
  605. },
  606. {
  607. startTimestamp: spanGroup[0].span.start_timestamp,
  608. endTimestamp: spanGroup[0].span.timestamp,
  609. }
  610. );
  611. }
  612. export function getSpanGroupBounds(
  613. spanGroup: EnhancedSpan[],
  614. generateBounds: (bounds: SpanBoundsType) => SpanGeneratedBoundsType
  615. ): SpanViewBoundsType {
  616. const {startTimestamp, endTimestamp} = getSpanGroupTimestamps(spanGroup);
  617. const bounds = generateBounds({
  618. startTimestamp,
  619. endTimestamp,
  620. });
  621. switch (bounds.type) {
  622. case 'TRACE_TIMESTAMPS_EQUAL':
  623. case 'INVALID_VIEW_WINDOW': {
  624. return {
  625. warning: void 0,
  626. left: void 0,
  627. width: void 0,
  628. isSpanVisibleInView: bounds.isSpanVisibleInView,
  629. };
  630. }
  631. case 'TIMESTAMPS_EQUAL': {
  632. return {
  633. warning: void 0,
  634. left: bounds.start,
  635. width: 0.00001,
  636. isSpanVisibleInView: bounds.isSpanVisibleInView,
  637. };
  638. }
  639. case 'TIMESTAMPS_REVERSED':
  640. case 'TIMESTAMPS_STABLE': {
  641. return {
  642. warning: void 0,
  643. left: bounds.start,
  644. width: bounds.end - bounds.start,
  645. isSpanVisibleInView: bounds.isSpanVisibleInView,
  646. };
  647. }
  648. default: {
  649. const _exhaustiveCheck: never = bounds;
  650. return _exhaustiveCheck;
  651. }
  652. }
  653. }
  654. export class SpansInViewMap {
  655. spanDepthsInView: Map<string, number>;
  656. treeDepthSum: number;
  657. length: number;
  658. isRootSpanInView: boolean;
  659. constructor() {
  660. this.spanDepthsInView = new Map();
  661. this.treeDepthSum = 0;
  662. this.length = 0;
  663. this.isRootSpanInView = false;
  664. }
  665. /**
  666. *
  667. * @param spanId
  668. * @param treeDepth
  669. * @returns false if the span is already stored, true otherwise
  670. */
  671. addSpan(spanId: string, treeDepth: number): boolean {
  672. if (this.spanDepthsInView.has(spanId)) {
  673. return false;
  674. }
  675. this.spanDepthsInView.set(spanId, treeDepth);
  676. this.length += 1;
  677. this.treeDepthSum += treeDepth;
  678. if (treeDepth === 0) {
  679. this.isRootSpanInView = true;
  680. }
  681. return true;
  682. }
  683. /**
  684. *
  685. * @param spanId
  686. * @returns false if the span does not exist within the span, true otherwise
  687. */
  688. removeSpan(spanId: string): boolean {
  689. if (!this.spanDepthsInView.has(spanId)) {
  690. return false;
  691. }
  692. const treeDepth = this.spanDepthsInView.get(spanId);
  693. this.spanDepthsInView.delete(spanId);
  694. this.length -= 1;
  695. this.treeDepthSum -= treeDepth!;
  696. if (treeDepth === 0) {
  697. this.isRootSpanInView = false;
  698. }
  699. return true;
  700. }
  701. has(spanId: string) {
  702. return this.spanDepthsInView.has(spanId);
  703. }
  704. getScrollVal() {
  705. if (this.isRootSpanInView) {
  706. return 0;
  707. }
  708. const avgDepth = Math.round(this.treeDepthSum / this.length);
  709. return avgDepth * (TOGGLE_BORDER_BOX / 2) - TOGGLE_BUTTON_MAX_WIDTH / 2;
  710. }
  711. }
  712. export function isSpanIdFocused(spanId: string, focusedSpanIds: FocusedSpanIDMap) {
  713. return (
  714. spanId in focusedSpanIds ||
  715. Object.values(focusedSpanIds).some(relatedSpans => relatedSpans.has(spanId))
  716. );
  717. }
  718. export function getCumulativeAlertLevelFromErrors(
  719. errors?: Pick<TraceError, 'level'>[]
  720. ): keyof Theme['alert'] | undefined {
  721. const highestErrorLevel = maxBy(
  722. errors || [],
  723. error => ERROR_LEVEL_WEIGHTS[error.level]
  724. )?.level;
  725. if (!highestErrorLevel) {
  726. return undefined;
  727. }
  728. return ERROR_LEVEL_TO_ALERT_TYPE[highestErrorLevel];
  729. }
  730. // Maps the six known error levels to one of three Alert component types
  731. const ERROR_LEVEL_TO_ALERT_TYPE: Record<TraceError['level'], keyof Theme['alert']> = {
  732. fatal: 'error',
  733. error: 'error',
  734. default: 'error',
  735. warning: 'warning',
  736. sample: 'info',
  737. info: 'info',
  738. };
  739. // Allows sorting errors according to their level of severity
  740. const ERROR_LEVEL_WEIGHTS: Record<TraceError['level'], number> = {
  741. fatal: 5,
  742. error: 4,
  743. default: 4,
  744. warning: 3,
  745. sample: 2,
  746. info: 1,
  747. };