traceProfilingLink.ts 3.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121
  1. import type {Location, LocationDescriptor} from 'history';
  2. import {generateContinuousProfileFlamechartRouteWithQuery} from 'sentry/utils/profiling/routes';
  3. import {
  4. isSpanNode,
  5. isTransactionNode,
  6. } from 'sentry/views/performance/newTraceDetails/guards';
  7. import type {
  8. TraceTree,
  9. TraceTreeNode,
  10. } from 'sentry/views/performance/newTraceDetails/traceModels/traceTree';
  11. function toDate(value: unknown): Date | null {
  12. if (typeof value !== 'string' && typeof value !== 'number') {
  13. return null;
  14. }
  15. const dateObj = new Date(value);
  16. if (isNaN(dateObj.getTime())) {
  17. return null;
  18. }
  19. return dateObj;
  20. }
  21. function getNodeId(node: TraceTreeNode<TraceTree.NodeValue>): string | undefined {
  22. if (isTransactionNode(node)) {
  23. return node.value.event_id;
  24. }
  25. if (isSpanNode(node)) {
  26. return node.value.span_id;
  27. }
  28. return undefined;
  29. }
  30. // In the current version, a segment is a parent transaction
  31. function getEventId(node: TraceTreeNode<TraceTree.NodeValue>): string | undefined {
  32. if (isTransactionNode(node)) {
  33. return node.value.event_id;
  34. }
  35. return node.parent_transaction?.value?.event_id;
  36. }
  37. /**
  38. * Generates a link to a continuous profile for a given trace element type
  39. */
  40. export function makeTraceContinuousProfilingLink(
  41. node: TraceTreeNode<TraceTree.NodeValue>,
  42. profilerId: string,
  43. options: {
  44. orgSlug: string;
  45. projectSlug: string;
  46. threadId: string | undefined;
  47. traceId: string;
  48. },
  49. query: Location['query'] = {}
  50. ): LocationDescriptor | null {
  51. if (!options.projectSlug || !options.orgSlug) {
  52. return null;
  53. }
  54. // We compute a time offset based on the duration of the span so that
  55. // users can see some context of things that occurred before and after the span.
  56. const transaction = isTransactionNode(node) ? node : node.parent_transaction;
  57. if (!transaction) {
  58. return null;
  59. }
  60. let start: Date | null = toDate(transaction.space[0]);
  61. let end: Date | null = toDate(transaction.space[0] + transaction.space[1]);
  62. // End timestamp is required to generate a link
  63. if (end === null || typeof profilerId !== 'string' || profilerId === '') {
  64. return null;
  65. }
  66. // If we have an end, but no start, then we'll generate a window of time around end timestamp
  67. // so that we can show context around the event.
  68. if (end && end.getTime() === start?.getTime()) {
  69. const PRE_CONTEXT_WINDOW_MS = 100;
  70. const POST_CONTEXT_WINDOW_MS = 100;
  71. start = new Date(start.getTime() - PRE_CONTEXT_WINDOW_MS);
  72. end = new Date(end.getTime() + POST_CONTEXT_WINDOW_MS);
  73. }
  74. // We require a full time range to open a flamechart
  75. if (start === null) {
  76. return null;
  77. }
  78. // TransactionId is required to generate a link because
  79. // we need to link to the segment of the trace and fetch its spans
  80. const eventId = getEventId(node);
  81. if (!eventId) {
  82. return null;
  83. }
  84. const queryWithEventData: Record<string, string> = {
  85. ...query,
  86. eventId,
  87. traceId: options.traceId,
  88. };
  89. if (typeof options.threadId === 'string') {
  90. queryWithEventData.tid = options.threadId;
  91. }
  92. const spanId = getNodeId(node);
  93. if (spanId) {
  94. queryWithEventData.spanId = spanId;
  95. }
  96. return generateContinuousProfileFlamechartRouteWithQuery(
  97. options.orgSlug,
  98. options.projectSlug,
  99. profilerId,
  100. start.toISOString(),
  101. end.toISOString(),
  102. queryWithEventData
  103. );
  104. }