import type {BreadcrumbTypeNavigation, Crumb} from 'sentry/types/breadcrumbs'; import {BreadcrumbType} from 'sentry/types/breadcrumbs'; import type {Event, EventTransaction} from 'sentry/types/event'; import { breadcrumbFactory, replayTimestamps, rrwebEventListFactory, spansFactory, } from 'sentry/utils/replays/replayDataUtils'; import type { MemorySpanType, RecordingEvent, ReplayCrumb, ReplayError, ReplayRecord, ReplaySpan, } from 'sentry/views/replays/types'; interface ReplayReaderParams { breadcrumbs: ReplayCrumb[] | undefined; errors: ReplayError[] | undefined; /** * The root Replay event, created at the start of the browser session. */ event: Event | undefined; /** * The captured data from rrweb. * Saved as N attachments that belong to the root Replay event. */ rrwebEvents: RecordingEvent[] | undefined; spans: ReplaySpan[] | undefined; } type RequiredNotNull = { [P in keyof T]: NonNullable; }; export default class ReplayReader { static factory({breadcrumbs, event, errors, rrwebEvents, spans}: ReplayReaderParams) { if (!breadcrumbs || !event || !rrwebEvents || !spans || !errors) { return null; } return new ReplayReader({breadcrumbs, event, errors, rrwebEvents, spans}); } private constructor({ breadcrumbs, event, errors, rrwebEvents, spans, }: RequiredNotNull) { const {startTimestampMs, endTimestampMs} = replayTimestamps( rrwebEvents, breadcrumbs, spans ); this.spans = spansFactory(spans); this.breadcrumbs = breadcrumbFactory( startTimestampMs, event, errors, breadcrumbs, this.spans ); this.rrwebEvents = rrwebEventListFactory( startTimestampMs, endTimestampMs, rrwebEvents ); this.event = { ...event, startTimestamp: startTimestampMs / 1000, endTimestamp: endTimestampMs / 1000, } as EventTransaction; const urls = ( this.getRawCrumbs().filter( crumb => crumb.category === BreadcrumbType.NAVIGATION ) as BreadcrumbTypeNavigation[] ) .map(crumb => crumb.data?.to) .filter(Boolean) as string[]; this.replayRecord = { countErrors: this.getRawCrumbs().filter( crumb => crumb.category === BreadcrumbType.ERROR ).length, countSegments: 0, countUrls: urls.length, errorIds: [], dist: this.event.dist, duration: endTimestampMs - startTimestampMs, environment: null, finishedAt: new Date(endTimestampMs), // TODO(replay): Convert from string to Date when reading API longestTransaction: 0, platform: this.event.platform, projectId: this.event.projectID, projectSlug: '', // TODO(replay): Read from useProject to fill this in release: null, // event.release is not a string, expected to be `version@1.4` replayId: this.event.id, sdkName: this.event.sdk?.name, sdkVersion: this.event.sdk?.version, startedAt: new Date(startTimestampMs), // TODO(replay): Convert from string to Date when reading API tags: this.event.tags.reduce((tags, {key, value}) => { tags[key] = value; return tags; }, {} as ReplayRecord['tags']), title: this.event.title, traceIds: [], urls, userAgent: '', user: { email: this.event.user?.email, id: this.event.user?.id, ipAddress: this.event.user?.ip_address, name: this.event.user?.name, }, } as ReplayRecord; } private event: EventTransaction; private replayRecord: ReplayRecord; private rrwebEvents: RecordingEvent[]; private breadcrumbs: Crumb[]; private spans: ReplaySpan[]; getEvent = () => { return this.event; }; /** * @returns Duration of Replay (milliseonds) */ getDurationMs = () => { return this.replayRecord.duration; }; getReplay = () => { return this.replayRecord; }; getRRWebEvents = () => { return this.rrwebEvents; }; getRawCrumbs = () => { return this.breadcrumbs; }; getRawSpans = () => { return this.spans; }; isMemorySpan = (span: ReplaySpan): span is MemorySpanType => { return span.op === 'memory'; }; isNotMemorySpan = (span: ReplaySpan) => { return !this.isMemorySpan(span); }; }