123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317 |
- import * as Sentry from '@sentry/react';
- import {incrementalSnapshotEvent, IncrementalSource} from '@sentry-internal/rrweb';
- import memoize from 'lodash/memoize';
- import {duration} from 'moment';
- import domId from 'sentry/utils/domId';
- import localStorageWrapper from 'sentry/utils/localStorage';
- import countDomNodes from 'sentry/utils/replays/countDomNodes';
- import extractDomNodes from 'sentry/utils/replays/extractDomNodes';
- import hydrateBreadcrumbs, {
- replayInitBreadcrumb,
- } from 'sentry/utils/replays/hydrateBreadcrumbs';
- import hydrateErrors from 'sentry/utils/replays/hydrateErrors';
- import hydrateFrames from 'sentry/utils/replays/hydrateFrames';
- import {
- recordingEndFrame,
- recordingStartFrame,
- } from 'sentry/utils/replays/hydrateRRWebRecordingFrames';
- import hydrateSpans from 'sentry/utils/replays/hydrateSpans';
- import {replayTimestamps} from 'sentry/utils/replays/replayDataUtils';
- import type {
- BreadcrumbFrame,
- ErrorFrame,
- MemoryFrame,
- OptionFrame,
- RecordingFrame,
- SlowClickFrame,
- SpanFrame,
- } from 'sentry/utils/replays/types';
- import {
- BreadcrumbCategories,
- EventType,
- isDeadClick,
- isDeadRageClick,
- isLCPFrame,
- isPaintFrame,
- } from 'sentry/utils/replays/types';
- import type {ReplayError, ReplayRecord} from 'sentry/views/replays/types';
- interface ReplayReaderParams {
- /**
- * Loaded segment data
- *
- * This is a mix of rrweb data, breadcrumbs and spans/transactions sorted by time
- * All three types are mixed together.
- */
- attachments: unknown[] | undefined;
- /**
- * Error objects related to this replay
- *
- * Error instances could be frontend, backend, or come from the error platform
- * like performance-errors or replay-errors
- */
- errors: ReplayError[] | undefined;
- /**
- * The root Replay event, created at the start of the browser session.
- */
- replayRecord: ReplayRecord | undefined;
- }
- type RequiredNotNull<T> = {
- [P in keyof T]: NonNullable<T[P]>;
- };
- const sortFrames = (a, b) => a.timestampMs - b.timestampMs;
- function removeDuplicateClicks(frames: BreadcrumbFrame[]) {
- const slowClickFrames = frames.filter(
- frame => frame.category === 'ui.slowClickDetected'
- );
- const clickFrames = frames.filter(frame => frame.category === 'ui.click');
- const otherFrames = frames.filter(
- frame => !(slowClickFrames.includes(frame) || clickFrames.includes(frame))
- );
- const uniqueClickFrames: BreadcrumbFrame[] = clickFrames.filter(clickFrame => {
- return !slowClickFrames.some(
- slowClickFrame =>
- slowClickFrame.data &&
- 'nodeId' in slowClickFrame.data &&
- clickFrame.data &&
- 'nodeId' in clickFrame.data &&
- slowClickFrame.data.nodeId === clickFrame.data.nodeId &&
- slowClickFrame.timestampMs === clickFrame.timestampMs
- );
- });
- return uniqueClickFrames.concat(otherFrames).concat(slowClickFrames);
- }
- export default class ReplayReader {
- static factory({attachments, errors, replayRecord}: ReplayReaderParams) {
- if (!attachments || !replayRecord || !errors) {
- return null;
- }
- try {
- return new ReplayReader({attachments, errors, replayRecord});
- } catch (err) {
- Sentry.captureException(err);
- // If something happens then we don't really know if it's the attachments
- // array or errors array to blame (it's probably attachments though).
- // Either way we can use the replayRecord to show some metadata, and then
- // put an error message below it.
- return new ReplayReader({
- attachments: [],
- errors: [],
- replayRecord,
- });
- }
- }
- private constructor({
- attachments,
- errors,
- replayRecord,
- }: RequiredNotNull<ReplayReaderParams>) {
- this._cacheKey = domId('replayReader-');
- const {breadcrumbFrames, optionFrame, rrwebFrames, spanFrames} =
- hydrateFrames(attachments);
- if (localStorageWrapper.getItem('REPLAY-BACKEND-TIMESTAMPS') !== '1') {
- // TODO(replays): We should get correct timestamps from the backend instead
- // of having to fix them up here.
- const {startTimestampMs, endTimestampMs} = replayTimestamps(
- replayRecord,
- rrwebFrames,
- breadcrumbFrames,
- spanFrames
- );
- this.timestampDeltas = {
- startedAtDelta: startTimestampMs - replayRecord.started_at.getTime(),
- finishedAtDelta: endTimestampMs - replayRecord.finished_at.getTime(),
- };
- replayRecord.started_at = new Date(startTimestampMs);
- replayRecord.finished_at = new Date(endTimestampMs);
- replayRecord.duration = duration(
- replayRecord.finished_at.getTime() - replayRecord.started_at.getTime()
- );
- }
- // Hydrate the data we were given
- this._replayRecord = replayRecord;
- // Errors don't need to be sorted here, they will be merged with breadcrumbs
- // and spans in the getter and then sorted together.
- this._errors = hydrateErrors(replayRecord, errors).sort(sortFrames);
- // RRWeb Events are not sorted here, they are fetched in sorted order.
- this._sortedRRWebEvents = rrwebFrames;
- // Breadcrumbs must be sorted. Crumbs like `slowClick` and `multiClick` will
- // have the same timestamp as the click breadcrumb, but will be emitted a
- // few seconds later.
- this._sortedBreadcrumbFrames = hydrateBreadcrumbs(
- replayRecord,
- breadcrumbFrames
- ).sort(sortFrames);
- // Spans must be sorted so components like the Timeline and Network Chart
- // can have an easier time to render.
- this._sortedSpanFrames = hydrateSpans(replayRecord, spanFrames).sort(sortFrames);
- this._optionFrame = optionFrame;
- // Insert extra records to satisfy minimum requirements for the UI
- this._sortedBreadcrumbFrames.push(replayInitBreadcrumb(replayRecord));
- this._sortedRRWebEvents.unshift(recordingStartFrame(replayRecord));
- this._sortedRRWebEvents.push(recordingEndFrame(replayRecord));
- }
- public timestampDeltas = {startedAtDelta: 0, finishedAtDelta: 0};
- private _cacheKey: string;
- private _errors: ErrorFrame[];
- private _optionFrame: undefined | OptionFrame;
- private _replayRecord: ReplayRecord;
- private _sortedBreadcrumbFrames: BreadcrumbFrame[];
- private _sortedRRWebEvents: RecordingFrame[];
- private _sortedSpanFrames: SpanFrame[];
- toJSON = () => this._cacheKey;
- /**
- * @returns Duration of Replay (milliseonds)
- */
- getDurationMs = () => {
- return this._replayRecord.duration.asMilliseconds();
- };
- getReplay = () => {
- return this._replayRecord;
- };
- getRRWebFrames = () => this._sortedRRWebEvents;
- getRRWebMutations = () =>
- this._sortedRRWebEvents.filter(
- event =>
- [EventType.IncrementalSnapshot].includes(event.type) &&
- [IncrementalSource.Mutation].includes(
- (event as incrementalSnapshotEvent).data.source
- ) // filter only for mutation events
- );
- getErrorFrames = () => this._errors;
- getConsoleFrames = memoize(() =>
- this._sortedBreadcrumbFrames.filter(
- frame =>
- frame.category === 'console' || !BreadcrumbCategories.includes(frame.category)
- )
- );
- getNavigationFrames = memoize(() =>
- [
- ...this._sortedBreadcrumbFrames.filter(frame => frame.category === 'replay.init'),
- ...this._sortedSpanFrames.filter(frame => frame.op.startsWith('navigation.')),
- ].sort(sortFrames)
- );
- getNetworkFrames = memoize(() =>
- this._sortedSpanFrames.filter(
- frame => frame.op.startsWith('navigation.') || frame.op.startsWith('resource.')
- )
- );
- getDOMFrames = memoize(() =>
- [
- ...removeDuplicateClicks(
- this._sortedBreadcrumbFrames
- .filter(frame => 'nodeId' in (frame.data ?? {}))
- .filter(
- frame =>
- !(
- (frame.category === 'ui.slowClickDetected' &&
- !isDeadClick(frame as SlowClickFrame)) ||
- frame.category === 'ui.multiClick'
- )
- )
- ),
- ...this._sortedSpanFrames.filter(frame => 'nodeId' in (frame.data ?? {})),
- ].sort(sortFrames)
- );
- countDomNodes = memoize(() =>
- countDomNodes({
- frames: this.getRRWebMutations(),
- rrwebEvents: this.getRRWebFrames(),
- startTimestampMs: this._replayRecord.started_at.getTime(),
- })
- );
- getDomNodes = memoize(() =>
- extractDomNodes({
- frames: this.getDOMFrames(),
- rrwebEvents: this.getRRWebFrames(),
- })
- );
- getMemoryFrames = memoize(() =>
- this._sortedSpanFrames.filter((frame): frame is MemoryFrame => frame.op === 'memory')
- );
- getChapterFrames = memoize(() =>
- [
- ...this.getPerfFrames(),
- ...this._sortedBreadcrumbFrames.filter(frame =>
- ['replay.init', 'replay.mutations'].includes(frame.category)
- ),
- ...this._errors,
- ].sort(sortFrames)
- );
- getPerfFrames = memoize(() =>
- [
- ...removeDuplicateClicks(
- this._sortedBreadcrumbFrames.filter(
- frame =>
- ['navigation', 'ui.click'].includes(frame.category) ||
- (frame.category === 'ui.slowClickDetected' &&
- (isDeadClick(frame as SlowClickFrame) ||
- isDeadRageClick(frame as SlowClickFrame)))
- )
- ),
- ...this._sortedSpanFrames.filter(frame => frame.op.startsWith('navigation.')),
- ].sort(sortFrames)
- );
- getLPCFrames = memoize(() => this._sortedSpanFrames.filter(isLCPFrame));
- getPaintFrames = memoize(() => this._sortedSpanFrames.filter(isPaintFrame));
- getSDKOptions = () => this._optionFrame;
- isNetworkDetailsSetup = memoize(() => {
- const sdkOptions = this.getSDKOptions();
- if (sdkOptions) {
- return sdkOptions.networkDetailHasUrls;
- }
- // Network data was added in JS SDK 7.50.0 while sdkConfig was added in v7.51.1
- // So even if we don't have the config object, we should still fallback and
- // look for spans with network data, as that means things are setup!
- return this.getNetworkFrames().some(
- frame =>
- // We'd need to `filter()` before calling `some()` in order for TS to be happy
- // @ts-expect-error
- Object.keys(frame?.data?.request?.headers ?? {}).length ||
- // @ts-expect-error
- Object.keys(frame?.data?.response?.headers ?? {}).length
- );
- });
- }
|