replayDataUtils.tsx 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163
  1. import first from 'lodash/first';
  2. import {transformCrumbs} from 'sentry/components/events/interfaces/breadcrumbs/utils';
  3. import {t} from 'sentry/locale';
  4. import type {
  5. BreadcrumbTypeDefault,
  6. BreadcrumbTypeNavigation,
  7. Crumb,
  8. RawCrumb,
  9. } from 'sentry/types/breadcrumbs';
  10. import {BreadcrumbLevelType, BreadcrumbType} from 'sentry/types/breadcrumbs';
  11. import {Event} from 'sentry/types/event';
  12. import type {
  13. RecordingEvent,
  14. ReplayCrumb,
  15. ReplayError,
  16. ReplaySpan,
  17. } from 'sentry/views/replays/types';
  18. export function rrwebEventListFactory(
  19. startTimestampMS: number,
  20. endTimestampMS: number,
  21. rrwebEvents: RecordingEvent[]
  22. ) {
  23. const events = ([] as RecordingEvent[]).concat(rrwebEvents).concat({
  24. type: 5, // EventType.Custom,
  25. timestamp: endTimestampMS,
  26. data: {
  27. tag: 'replay-end',
  28. },
  29. });
  30. events.sort((a, b) => a.timestamp - b.timestamp);
  31. const firstRRWebEvent = first(events);
  32. if (firstRRWebEvent) {
  33. firstRRWebEvent.timestamp = startTimestampMS;
  34. }
  35. return events;
  36. }
  37. export function breadcrumbFactory(
  38. startTimestamp: number,
  39. rootEvent: Event,
  40. errors: ReplayError[],
  41. rawCrumbs: ReplayCrumb[],
  42. spans: ReplaySpan[]
  43. ): Crumb[] {
  44. const {tags} = rootEvent;
  45. const initialUrl = tags.find(tag => tag.key === 'url')?.value;
  46. const initBreadcrumb = {
  47. type: BreadcrumbType.INIT,
  48. timestamp: new Date(startTimestamp).toISOString(),
  49. level: BreadcrumbLevelType.INFO,
  50. message: initialUrl,
  51. data: {
  52. action: 'replay-init',
  53. label: t('Start recording'),
  54. url: initialUrl,
  55. },
  56. } as BreadcrumbTypeDefault;
  57. const errorCrumbs: RawCrumb[] = errors.map(error => ({
  58. type: BreadcrumbType.ERROR,
  59. level: BreadcrumbLevelType.ERROR,
  60. category: 'exception',
  61. message: error['error.value'],
  62. data: {
  63. label: error['error.type'],
  64. },
  65. timestamp: error.timestamp,
  66. }));
  67. const spanCrumbs: (BreadcrumbTypeDefault | BreadcrumbTypeNavigation)[] = spans
  68. .filter(span =>
  69. ['navigation.navigate', 'navigation.reload', 'largest-contentful-paint'].includes(
  70. span.op
  71. )
  72. )
  73. .map(span => {
  74. if (span.op.startsWith('navigation')) {
  75. const [, action] = span.op.split('.');
  76. return {
  77. category: 'default',
  78. type: BreadcrumbType.NAVIGATION,
  79. timestamp: new Date(span.startTimestamp * 1000).toISOString(),
  80. level: BreadcrumbLevelType.INFO,
  81. message: span.description,
  82. action,
  83. data: {
  84. to: span.description,
  85. label:
  86. action === 'reload'
  87. ? t('Reload')
  88. : action === 'navigate'
  89. ? t('Page load')
  90. : t('Navigation'),
  91. ...span.data,
  92. },
  93. };
  94. }
  95. return {
  96. type: BreadcrumbType.DEBUG,
  97. timestamp: new Date(span.startTimestamp * 1000).toISOString(),
  98. level: BreadcrumbLevelType.INFO,
  99. category: 'default',
  100. data: {
  101. action: span.op,
  102. ...span.data,
  103. label: span.op === 'largest-contentful-paint' ? t('LCP') : span.op,
  104. },
  105. };
  106. });
  107. const hasPageLoad = spans.find(span => span.op === 'navigation.navigate');
  108. const result = transformCrumbs([
  109. ...(!hasPageLoad ? [initBreadcrumb] : []),
  110. ...(rawCrumbs.map(({timestamp, ...crumb}) => ({
  111. ...crumb,
  112. type: BreadcrumbType.DEFAULT,
  113. timestamp: new Date(timestamp * 1000).toISOString(),
  114. })) as RawCrumb[]),
  115. ...errorCrumbs,
  116. ...spanCrumbs,
  117. ]);
  118. return result.sort((a, b) => +new Date(a.timestamp || 0) - +new Date(b.timestamp || 0));
  119. }
  120. export function spansFactory(spans: ReplaySpan[]) {
  121. return spans.sort((a, b) => a.startTimestamp - b.startTimestamp);
  122. }
  123. /**
  124. * The original `this._event.startTimestamp` and `this._event.endTimestamp`
  125. * are the same. It's because the root replay event is re-purposing the
  126. * `transaction` type, but it is not a real span occurring over time.
  127. * So we need to figure out the real start and end timestamps based on when
  128. * first and last bits of data were collected. In milliseconds.
  129. */
  130. export function replayTimestamps(
  131. rrwebEvents: RecordingEvent[],
  132. rawCrumbs: ReplayCrumb[],
  133. rawSpanData: ReplaySpan[]
  134. ) {
  135. const rrwebTimestamps = rrwebEvents.map(event => event.timestamp);
  136. const breadcrumbTimestamps = (
  137. rawCrumbs.map(rawCrumb => rawCrumb.timestamp).filter(Boolean) as number[]
  138. ).map(timestamp => +new Date(timestamp * 1000));
  139. const spanStartTimestamps = rawSpanData.map(span => span.startTimestamp * 1000);
  140. const spanEndTimestamps = rawSpanData.map(span => span.endTimestamp * 1000);
  141. return {
  142. startTimestampMS: Math.min(
  143. ...[...rrwebTimestamps, ...breadcrumbTimestamps, ...spanStartTimestamps]
  144. ),
  145. endTimestampMS: Math.max(
  146. ...[...rrwebTimestamps, ...breadcrumbTimestamps, ...spanEndTimestamps]
  147. ),
  148. };
  149. }