replayerStepper.tsx 2.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110
  1. import {Replayer} from '@sentry-internal/rrweb';
  2. import type {RecordingFrame, ReplayFrame} from 'sentry/utils/replays/types';
  3. interface Args<Frame extends ReplayFrame | RecordingFrame, CollectionData> {
  4. frames: Frame[] | undefined;
  5. onVisitFrame: (
  6. frame: Frame,
  7. collection: Map<Frame, CollectionData>,
  8. replayer: Replayer
  9. ) => void;
  10. rrwebEvents: RecordingFrame[] | undefined;
  11. shouldVisitFrame: (frame: Frame, replayer: Replayer) => boolean;
  12. startTimestampMs: number;
  13. }
  14. type FrameRef<Frame extends ReplayFrame | RecordingFrame> = {
  15. frame: Frame | undefined;
  16. };
  17. export default function replayerStepper<
  18. Frame extends ReplayFrame | RecordingFrame,
  19. CollectionData,
  20. >({
  21. frames,
  22. onVisitFrame,
  23. rrwebEvents,
  24. shouldVisitFrame,
  25. startTimestampMs,
  26. }: Args<Frame, CollectionData>): Promise<Map<Frame, CollectionData>> {
  27. const collection = new Map<Frame, CollectionData>();
  28. return new Promise(resolve => {
  29. if (!frames?.length || !rrwebEvents?.length) {
  30. resolve(new Map());
  31. return;
  32. }
  33. const replayer = createHiddenPlayer(rrwebEvents);
  34. const nextFrame = (function () {
  35. let i = 0;
  36. return () => frames[i++];
  37. })();
  38. const onDone = () => {
  39. resolve(collection);
  40. };
  41. const nextOrDone = () => {
  42. const next = nextFrame();
  43. if (next) {
  44. considerFrame(next);
  45. } else {
  46. onDone();
  47. }
  48. };
  49. const frameRef: FrameRef<Frame> = {
  50. frame: undefined,
  51. };
  52. const considerFrame = (frame: Frame) => {
  53. if (shouldVisitFrame(frame, replayer)) {
  54. frameRef.frame = frame;
  55. window.setTimeout(() => {
  56. const timestamp =
  57. 'offsetMs' in frame ? frame.offsetMs : frame.timestamp - startTimestampMs;
  58. replayer.pause(timestamp);
  59. }, 0);
  60. } else {
  61. frameRef.frame = undefined;
  62. nextOrDone();
  63. }
  64. };
  65. const handlePause = () => {
  66. onVisitFrame(frameRef.frame!, collection, replayer);
  67. nextOrDone();
  68. };
  69. replayer.on('pause', handlePause);
  70. considerFrame(nextFrame());
  71. });
  72. }
  73. function createHiddenPlayer(rrwebEvents: RecordingFrame[]): Replayer {
  74. const domRoot = document.createElement('div');
  75. domRoot.className = 'sentry-block';
  76. const {style} = domRoot;
  77. style.position = 'fixed';
  78. style.inset = '0';
  79. style.width = '0';
  80. style.height = '0';
  81. style.overflow = 'hidden';
  82. document.body.appendChild(domRoot);
  83. return new Replayer(rrwebEvents, {
  84. root: domRoot,
  85. loadTimeout: 1,
  86. showWarning: false,
  87. blockClass: 'sentry-block',
  88. speed: 99999,
  89. skipInactive: true,
  90. triggerFocus: false,
  91. mouseTail: false,
  92. });
  93. }