replayDataUtils.tsx 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181
  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 type {
  12. RecordingEvent,
  13. ReplayCrumb,
  14. ReplayError,
  15. ReplayRecord,
  16. ReplaySpan,
  17. } from 'sentry/views/replays/types';
  18. export function mapResponseToReplayRecord(apiResponse: any): ReplayRecord {
  19. return {
  20. ...apiResponse,
  21. ...(apiResponse.startedAt ? {startedAt: new Date(apiResponse.startedAt)} : {}),
  22. ...(apiResponse.finishedAt ? {finishedAt: new Date(apiResponse.finishedAt)} : {}),
  23. };
  24. }
  25. export function rrwebEventListFactory(
  26. replayRecord: ReplayRecord,
  27. rrwebEvents: RecordingEvent[]
  28. ) {
  29. const events = ([] as RecordingEvent[]).concat(rrwebEvents).concat({
  30. type: 5, // EventType.Custom,
  31. timestamp: replayRecord.finishedAt.getTime(),
  32. data: {
  33. tag: 'replay-end',
  34. },
  35. });
  36. events.sort((a, b) => a.timestamp - b.timestamp);
  37. const firstRRWebEvent = first(events) as RecordingEvent;
  38. firstRRWebEvent.timestamp = replayRecord.startedAt.getTime();
  39. return events;
  40. }
  41. export function breadcrumbFactory(
  42. replayRecord: ReplayRecord,
  43. errors: ReplayError[],
  44. rawCrumbs: ReplayCrumb[],
  45. spans: ReplaySpan[]
  46. ): Crumb[] {
  47. const UNWANTED_CRUMB_CATEGORIES = ['ui.focus', 'ui.blur'];
  48. const initialUrl = replayRecord.tags.url?.join(', ');
  49. const initBreadcrumb = {
  50. type: BreadcrumbType.INIT,
  51. timestamp: replayRecord.startedAt.toISOString(),
  52. level: BreadcrumbLevelType.INFO,
  53. message: initialUrl,
  54. data: {
  55. action: 'replay-init',
  56. label: t('Start recording'),
  57. url: initialUrl,
  58. },
  59. } as BreadcrumbTypeDefault;
  60. const errorCrumbs: RawCrumb[] = errors.map(error => ({
  61. type: BreadcrumbType.ERROR,
  62. level: BreadcrumbLevelType.ERROR,
  63. category: 'exception',
  64. message: error['error.value'],
  65. data: {
  66. label: error['error.type'],
  67. },
  68. timestamp: error.timestamp,
  69. }));
  70. const spanCrumbs: (BreadcrumbTypeDefault | BreadcrumbTypeNavigation)[] = spans
  71. .filter(span =>
  72. ['navigation.navigate', 'navigation.reload', 'largest-contentful-paint'].includes(
  73. span.op
  74. )
  75. )
  76. .map(span => {
  77. if (span.op.startsWith('navigation')) {
  78. const [, action] = span.op.split('.');
  79. return {
  80. category: 'default',
  81. type: BreadcrumbType.NAVIGATION,
  82. timestamp: new Date(span.startTimestamp * 1000).toISOString(),
  83. level: BreadcrumbLevelType.INFO,
  84. message: span.description,
  85. action,
  86. data: {
  87. to: span.description,
  88. label:
  89. action === 'reload'
  90. ? t('Reload')
  91. : action === 'navigate'
  92. ? t('Page load')
  93. : t('Navigation'),
  94. ...span.data,
  95. },
  96. };
  97. }
  98. return {
  99. type: BreadcrumbType.DEBUG,
  100. timestamp: new Date(span.startTimestamp * 1000).toISOString(),
  101. level: BreadcrumbLevelType.INFO,
  102. category: 'default',
  103. data: {
  104. action: span.op,
  105. ...span.data,
  106. label: span.op === 'largest-contentful-paint' ? t('LCP') : span.op,
  107. },
  108. };
  109. });
  110. const hasPageLoad = spans.find(span => span.op === 'navigation.navigate');
  111. const rawCrumbsWithTimestamp: RawCrumb[] = rawCrumbs
  112. .filter(crumb => {
  113. return !UNWANTED_CRUMB_CATEGORIES.includes(crumb.category || '');
  114. })
  115. .map(crumb => {
  116. return {
  117. ...crumb,
  118. type: BreadcrumbType.DEFAULT,
  119. timestamp: new Date(crumb.timestamp * 1000).toISOString(),
  120. };
  121. });
  122. const result = transformCrumbs([
  123. ...(!hasPageLoad ? [initBreadcrumb] : []),
  124. ...rawCrumbsWithTimestamp,
  125. ...errorCrumbs,
  126. ...spanCrumbs,
  127. ]);
  128. return result.sort((a, b) => +new Date(a.timestamp || 0) - +new Date(b.timestamp || 0));
  129. }
  130. export function spansFactory(spans: ReplaySpan[]) {
  131. return spans.sort((a, b) => a.startTimestamp - b.startTimestamp);
  132. }
  133. /**
  134. * We need to figure out the real start and end timestamps based on when
  135. * first and last bits of data were collected. In milliseconds.
  136. *
  137. * @deprecated Once the backend returns the corrected timestamps, this is not needed.
  138. */
  139. export function replayTimestamps(
  140. replayRecord: ReplayRecord,
  141. rrwebEvents: RecordingEvent[],
  142. rawCrumbs: ReplayCrumb[],
  143. rawSpanData: ReplaySpan[]
  144. ) {
  145. const rrwebTimestamps = rrwebEvents.map(event => event.timestamp).filter(Boolean);
  146. const breadcrumbTimestamps = (
  147. rawCrumbs.map(rawCrumb => rawCrumb.timestamp).filter(Boolean) as number[]
  148. )
  149. .map(timestamp => +new Date(timestamp * 1000))
  150. .filter(Boolean);
  151. const spanStartTimestamps = rawSpanData.map(span => span.startTimestamp * 1000);
  152. const spanEndTimestamps = rawSpanData.map(span => span.endTimestamp * 1000);
  153. return {
  154. startTimestampMs: Math.min(
  155. replayRecord.startedAt.getTime(),
  156. ...[...rrwebTimestamps, ...breadcrumbTimestamps, ...spanStartTimestamps]
  157. ),
  158. endTimestampMs: Math.max(
  159. replayRecord.finishedAt.getTime(),
  160. ...[...rrwebTimestamps, ...breadcrumbTimestamps, ...spanEndTimestamps]
  161. ),
  162. };
  163. }