123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198 |
- import * as Sentry from '@sentry/react';
- import memoize from 'lodash/memoize';
- import {duration} from 'moment';
- import type {Crumb} from 'sentry/types/breadcrumbs';
- import {BreadcrumbType} from 'sentry/types/breadcrumbs';
- import {
- breadcrumbFactory,
- replayTimestamps,
- rrwebEventListFactory,
- spansFactory,
- } from 'sentry/utils/replays/replayDataUtils';
- import splitAttachmentsByType from 'sentry/utils/replays/splitAttachmentsByType';
- import {EventType} from 'sentry/utils/replays/types';
- import type {
- MemorySpan,
- NetworkSpan,
- RecordingEvent,
- RecordingOptions,
- ReplayCrumb,
- ReplayError,
- ReplayRecord,
- ReplaySpan,
- } 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;
- 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]>;
- };
- export default class ReplayReader {
- static factory({attachments, replayRecord, errors}: ReplayReaderParams) {
- if (!attachments || !replayRecord || !errors) {
- return null;
- }
- try {
- return new ReplayReader({attachments, replayRecord, errors});
- } 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,
- replayRecord,
- errors,
- }: RequiredNotNull<ReplayReaderParams>) {
- const {rawBreadcrumbs, rawRRWebEvents, rawNetworkSpans, rawMemorySpans} =
- splitAttachmentsByType(attachments);
- const spans = [...rawMemorySpans, ...rawNetworkSpans] as ReplaySpan[];
- // TODO(replays): We should get correct timestamps from the backend instead
- // of having to fix them up here.
- const {startTimestampMs, endTimestampMs} = replayTimestamps(
- replayRecord,
- rawRRWebEvents as RecordingEvent[],
- rawBreadcrumbs as ReplayCrumb[],
- spans
- );
- replayRecord.started_at = new Date(startTimestampMs);
- replayRecord.finished_at = new Date(endTimestampMs);
- replayRecord.duration = duration(
- replayRecord.finished_at.getTime() - replayRecord.started_at.getTime()
- );
- this.rawErrors = errors;
- this.sortedSpans = spansFactory(spans);
- this.breadcrumbs = breadcrumbFactory(
- replayRecord,
- errors,
- rawBreadcrumbs as ReplayCrumb[],
- this.sortedSpans
- );
- this.rrwebEvents = rrwebEventListFactory(
- replayRecord,
- rawRRWebEvents as RecordingEvent[]
- );
- this.replayRecord = replayRecord;
- }
- private rawErrors: ReplayError[];
- private sortedSpans: ReplaySpan[];
- private replayRecord: ReplayRecord;
- private rrwebEvents: RecordingEvent[];
- private breadcrumbs: Crumb[];
- /**
- * @returns Duration of Replay (milliseonds)
- */
- getDurationMs = () => {
- return this.replayRecord.duration.asMilliseconds();
- };
- getReplay = () => {
- return this.replayRecord;
- };
- getRRWebEvents = () => {
- return this.rrwebEvents;
- };
- getCrumbsWithRRWebNodes = memoize(() =>
- this.breadcrumbs.filter(
- crumb => crumb.data && typeof crumb.data === 'object' && 'nodeId' in crumb.data
- )
- );
- getUserActionCrumbs = memoize(() => {
- const USER_ACTIONS = [
- BreadcrumbType.ERROR,
- BreadcrumbType.INIT,
- BreadcrumbType.NAVIGATION,
- BreadcrumbType.UI,
- BreadcrumbType.USER,
- ];
- return this.breadcrumbs.filter(crumb => USER_ACTIONS.includes(crumb.type));
- });
- getConsoleCrumbs = memoize(() =>
- this.breadcrumbs.filter(crumb => ['console', 'issue'].includes(crumb.category || ''))
- );
- getRawErrors = memoize(() => this.rawErrors);
- getNonConsoleCrumbs = memoize(() =>
- this.breadcrumbs.filter(crumb => crumb.category !== 'console')
- );
- getNavCrumbs = memoize(() =>
- this.breadcrumbs.filter(crumb =>
- [BreadcrumbType.INIT, BreadcrumbType.NAVIGATION].includes(crumb.type)
- )
- );
- getNetworkSpans = memoize(() => this.sortedSpans.filter(isNetworkSpan));
- getMemorySpans = memoize(() => this.sortedSpans.filter(isMemorySpan));
- sdkConfig = memoize(() => {
- const found = this.rrwebEvents.find(
- event => event.type === EventType.Custom && event.data.tag === 'options'
- ) as undefined | RecordingOptions;
- return found?.data?.payload;
- });
- isNetworkDetailsSetup = memoize(() => {
- const config = this.sdkConfig();
- if (config) {
- return this.sdkConfig()?.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.getNetworkSpans().some(
- span =>
- Object.keys(span.data.request?.headers || {}).length ||
- Object.keys(span.data.response?.headers || {}).length
- );
- });
- }
- const isMemorySpan = (span: ReplaySpan): span is MemorySpan => {
- return span.op === 'memory';
- };
- const isNetworkSpan = (span: ReplaySpan): span is NetworkSpan => {
- return span.op?.startsWith('navigation.') || span.op?.startsWith('resource.');
- };
|