replayDataUtils.tsx 5.0 KB

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