replayReader.tsx 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202
  1. import * as Sentry from '@sentry/react';
  2. import memoize from 'lodash/memoize';
  3. import {duration} from 'moment';
  4. import type {Crumb} from 'sentry/types/breadcrumbs';
  5. import {BreadcrumbType} from 'sentry/types/breadcrumbs';
  6. import {
  7. breadcrumbFactory,
  8. replayTimestamps,
  9. rrwebEventListFactory,
  10. spansFactory,
  11. } from 'sentry/utils/replays/replayDataUtils';
  12. import splitAttachmentsByType from 'sentry/utils/replays/splitAttachmentsByType';
  13. import {EventType} from 'sentry/utils/replays/types';
  14. import type {
  15. MemorySpan,
  16. NetworkSpan,
  17. RecordingEvent,
  18. RecordingOptions,
  19. ReplayCrumb,
  20. ReplayError,
  21. ReplayRecord,
  22. ReplaySpan,
  23. } from 'sentry/views/replays/types';
  24. interface ReplayReaderParams {
  25. /**
  26. * Loaded segment data
  27. *
  28. * This is a mix of rrweb data, breadcrumbs and spans/transactions sorted by time
  29. * All three types are mixed together.
  30. */
  31. attachments: unknown[] | undefined;
  32. errors: ReplayError[] | undefined;
  33. /**
  34. * The root Replay event, created at the start of the browser session.
  35. */
  36. replayRecord: ReplayRecord | undefined;
  37. }
  38. type RequiredNotNull<T> = {
  39. [P in keyof T]: NonNullable<T[P]>;
  40. };
  41. export default class ReplayReader {
  42. static factory({attachments, replayRecord, errors}: ReplayReaderParams) {
  43. if (!attachments || !replayRecord || !errors) {
  44. return null;
  45. }
  46. try {
  47. return new ReplayReader({attachments, replayRecord, errors});
  48. } catch (err) {
  49. Sentry.captureException(err);
  50. // If something happens then we don't really know if it's the attachments
  51. // array or errors array to blame (it's probably attachments though).
  52. // Either way we can use the replayRecord to show some metadata, and then
  53. // put an error message below it.
  54. return new ReplayReader({
  55. attachments: [],
  56. errors: [],
  57. replayRecord,
  58. });
  59. }
  60. }
  61. private constructor({
  62. attachments,
  63. replayRecord,
  64. errors,
  65. }: RequiredNotNull<ReplayReaderParams>) {
  66. const {rawBreadcrumbs, rawRRWebEvents, rawNetworkSpans, rawMemorySpans} =
  67. splitAttachmentsByType(attachments);
  68. const spans = [...rawMemorySpans, ...rawNetworkSpans] as ReplaySpan[];
  69. // TODO(replays): We should get correct timestamps from the backend instead
  70. // of having to fix them up here.
  71. const {startTimestampMs, endTimestampMs} = replayTimestamps(
  72. replayRecord,
  73. rawRRWebEvents as RecordingEvent[],
  74. rawBreadcrumbs as ReplayCrumb[],
  75. spans
  76. );
  77. replayRecord.started_at = new Date(startTimestampMs);
  78. replayRecord.finished_at = new Date(endTimestampMs);
  79. replayRecord.duration = duration(
  80. replayRecord.finished_at.getTime() - replayRecord.started_at.getTime()
  81. );
  82. this.rawErrors = errors;
  83. this.sortedSpans = spansFactory(spans);
  84. this.breadcrumbs = breadcrumbFactory(
  85. replayRecord,
  86. errors,
  87. rawBreadcrumbs as ReplayCrumb[],
  88. this.sortedSpans
  89. );
  90. this.rrwebEvents = rrwebEventListFactory(
  91. replayRecord,
  92. rawRRWebEvents as RecordingEvent[]
  93. );
  94. this.replayRecord = replayRecord;
  95. }
  96. private rawErrors: ReplayError[];
  97. private sortedSpans: ReplaySpan[];
  98. private replayRecord: ReplayRecord;
  99. private rrwebEvents: RecordingEvent[];
  100. private breadcrumbs: Crumb[];
  101. /**
  102. * @returns Duration of Replay (milliseonds)
  103. */
  104. getDurationMs = () => {
  105. return this.replayRecord.duration.asMilliseconds();
  106. };
  107. getReplay = () => {
  108. return this.replayRecord;
  109. };
  110. getRRWebEvents = () => {
  111. return this.rrwebEvents;
  112. };
  113. getCrumbsWithRRWebNodes = memoize(() =>
  114. this.breadcrumbs.filter(
  115. crumb => crumb.data && typeof crumb.data === 'object' && 'nodeId' in crumb.data
  116. )
  117. );
  118. getUserActionCrumbs = memoize(() => {
  119. const USER_ACTIONS = [
  120. BreadcrumbType.ERROR,
  121. BreadcrumbType.INIT,
  122. BreadcrumbType.NAVIGATION,
  123. BreadcrumbType.UI,
  124. BreadcrumbType.USER,
  125. ];
  126. return this.breadcrumbs.filter(crumb => USER_ACTIONS.includes(crumb.type));
  127. });
  128. getConsoleCrumbs = memoize(() =>
  129. this.breadcrumbs.filter(crumb => crumb.category === 'console')
  130. );
  131. getRawErrors = memoize(() => this.rawErrors);
  132. getIssueCrumbs = memoize(() =>
  133. this.breadcrumbs.filter(crumb => crumb.category === 'issue')
  134. );
  135. getNonConsoleCrumbs = memoize(() =>
  136. this.breadcrumbs.filter(crumb => crumb.category !== 'console')
  137. );
  138. getNavCrumbs = memoize(() =>
  139. this.breadcrumbs.filter(crumb =>
  140. [BreadcrumbType.INIT, BreadcrumbType.NAVIGATION].includes(crumb.type)
  141. )
  142. );
  143. getNetworkSpans = memoize(() => this.sortedSpans.filter(isNetworkSpan));
  144. getMemorySpans = memoize(() => this.sortedSpans.filter(isMemorySpan));
  145. sdkConfig = memoize(() => {
  146. const found = this.rrwebEvents.find(
  147. event => event.type === EventType.Custom && event.data.tag === 'options'
  148. ) as undefined | RecordingOptions;
  149. return found?.data?.payload;
  150. });
  151. isNetworkDetailsSetup = memoize(() => {
  152. const config = this.sdkConfig();
  153. if (config) {
  154. return this.sdkConfig()?.networkDetailHasUrls;
  155. }
  156. // Network data was added in JS SDK 7.50.0 while sdkConfig was added in v7.51.1
  157. // So even if we don't have the config object, we should still fallback and
  158. // look for spans with network data, as that means things are setup!
  159. return this.getNetworkSpans().some(
  160. span =>
  161. Object.keys(span.data.request?.headers || {}).length ||
  162. Object.keys(span.data.response?.headers || {}).length
  163. );
  164. });
  165. }
  166. const isMemorySpan = (span: ReplaySpan): span is MemorySpan => {
  167. return span.op === 'memory';
  168. };
  169. const isNetworkSpan = (span: ReplaySpan): span is NetworkSpan => {
  170. return span.op?.startsWith('navigation.') || span.op?.startsWith('resource.');
  171. };