countDomNodes.tsx 2.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108
  1. import {Replayer} from '@sentry-internal/rrweb';
  2. import type {RecordingFrame} from 'sentry/utils/replays/types';
  3. export type DomNodeChartDatapoint = {
  4. count: number;
  5. endTimestampMs: number;
  6. startTimestampMs: number;
  7. timestampMs: number;
  8. };
  9. type Args = {
  10. frames: RecordingFrame[] | undefined;
  11. rrwebEvents: RecordingFrame[] | undefined;
  12. startTimestampMs: number;
  13. };
  14. export default function countDomNodes({
  15. frames = [],
  16. rrwebEvents,
  17. startTimestampMs,
  18. }: Args): Promise<DomNodeChartDatapoint[]> {
  19. return new Promise(resolve => {
  20. const datapoints = new Map<RecordingFrame, DomNodeChartDatapoint>();
  21. const player = createPlayer(rrwebEvents);
  22. const nextFrame = (function () {
  23. let i = 0;
  24. const len = frames.length;
  25. // how many frames we look at depends on the number of total frames
  26. return () => frames[(i += Math.max(Math.round(len * 0.007), 1))];
  27. })();
  28. const onDone = () => {
  29. resolve(Array.from(datapoints.values()));
  30. };
  31. const nextOrDone = () => {
  32. const next = nextFrame();
  33. if (next) {
  34. matchFrame(next);
  35. } else {
  36. onDone();
  37. }
  38. };
  39. type FrameRef = {
  40. frame: undefined | RecordingFrame;
  41. };
  42. const nodeIdRef: FrameRef = {
  43. frame: undefined,
  44. };
  45. const handlePause = () => {
  46. const frame = nodeIdRef.frame as RecordingFrame;
  47. const idCount = player.getMirror().getIds().length; // gets number of DOM nodes present
  48. datapoints.set(frame as RecordingFrame, {
  49. count: idCount,
  50. timestampMs: frame.timestamp,
  51. startTimestampMs: frame.timestamp,
  52. endTimestampMs: frame.timestamp,
  53. });
  54. nextOrDone();
  55. };
  56. const matchFrame = frame => {
  57. if (!frame) {
  58. nextOrDone();
  59. return;
  60. }
  61. nodeIdRef.frame = frame;
  62. window.setTimeout(() => {
  63. player.pause(frame.timestamp - startTimestampMs);
  64. }, 0);
  65. };
  66. player.on('pause', handlePause);
  67. matchFrame(nextFrame());
  68. });
  69. }
  70. function createPlayer(rrwebEvents): Replayer {
  71. const domRoot = document.createElement('div');
  72. domRoot.className = 'sentry-block';
  73. const {style} = domRoot;
  74. style.position = 'fixed';
  75. style.inset = '0';
  76. style.width = '0';
  77. style.height = '0';
  78. style.overflow = 'hidden';
  79. document.body.appendChild(domRoot);
  80. const replayerRef = new Replayer(rrwebEvents, {
  81. root: domRoot,
  82. loadTimeout: 1,
  83. showWarning: false,
  84. blockClass: 'sentry-block',
  85. speed: 99999,
  86. skipInactive: true,
  87. triggerFocus: false,
  88. mouseTail: false,
  89. });
  90. return replayerRef;
  91. }