11import type { RecordingEvent , ReplayContainer } from '@sentry/replay/build/npm/types/types' ;
22import type { Breadcrumb , Event , ReplayEvent } from '@sentry/types' ;
3+ import pako from 'pako' ;
34import type { Page , Request } from 'playwright' ;
45
56import { envelopeRequestParser } from './helpers' ;
@@ -13,6 +14,17 @@ type PerformanceSpan = {
1314 data : Record < string , number > ;
1415} ;
1516
17+ export type RecordingSnapshot = {
18+ node : SnapshotNode ;
19+ initialOffset : number ;
20+ } ;
21+
22+ type SnapshotNode = {
23+ type : number ;
24+ id : number ;
25+ childNodes : SnapshotNode [ ] ;
26+ } ;
27+
1628/**
1729 * Waits for a replay request to be sent by the page and returns it.
1830 *
@@ -85,7 +97,7 @@ export function getCustomRecordingEvents(replayRequest: Request): {
8597 breadcrumbs : Breadcrumb [ ] ;
8698 performanceSpans : PerformanceSpan [ ] ;
8799} {
88- const recordingEvents = envelopeRequestParser ( replayRequest , 5 ) as RecordingEvent [ ] ;
100+ const recordingEvents = getDecompressedRecordingEvents ( replayRequest ) ;
89101
90102 const breadcrumbs = getReplayBreadcrumbs ( recordingEvents ) ;
91103 const performanceSpans = getReplayPerformanceSpans ( recordingEvents ) ;
@@ -108,3 +120,60 @@ function getReplayPerformanceSpans(recordingEvents: RecordingEvent[]): Performan
108120 . filter ( data => data . tag === 'performanceSpan' )
109121 . map ( data => data . payload ) as PerformanceSpan [ ] ;
110122}
123+
124+ export function getFullRecordingSnapshots ( replayRequest : Request ) : RecordingSnapshot [ ] {
125+ const events = getDecompressedRecordingEvents ( replayRequest ) as RecordingEvent [ ] ;
126+ return events . filter ( event => event . type === 2 ) . map ( event => event . data as RecordingSnapshot ) ;
127+ }
128+
129+ function getDecompressedRecordingEvents ( replayRequest : Request ) : RecordingEvent [ ] {
130+ return replayEnvelopeRequestParser ( replayRequest , 5 ) as RecordingEvent [ ] ;
131+ }
132+
133+ /**
134+ * Copy of the envelopeParser from ./helpers.ts, but with the ability
135+ * to decompress zlib-compressed envelope payloads which we need to inspect for replay recordings.
136+ * This parser can handle uncompressed as well as compressed replay recordings.
137+ */
138+ const replayEnvelopeRequestParser = ( request : Request | null , envelopeIndex = 2 ) : Event => {
139+ const envelope = replayEnvelopeParser ( request ) ;
140+ return envelope [ envelopeIndex ] as Event ;
141+ } ;
142+
143+ const replayEnvelopeParser = ( request : Request | null ) : unknown [ ] => {
144+ // https://develop.sentry.dev/sdk/envelopes/
145+ const envelopeBytes = request ?. postDataBuffer ( ) || '' ;
146+
147+ // first, we convert the bugger to string to split and go through the uncompressed lines
148+ const envelopeString = envelopeBytes . toString ( ) ;
149+
150+ const lines = envelopeString . split ( '\n' ) . map ( line => {
151+ try {
152+ return JSON . parse ( line ) ;
153+ } catch ( error ) {
154+ // If we fail to parse a line, we _might_ have found a compressed payload,
155+ // so let's check if this is actually the case.
156+ // This is quite hacky but we can't go through `line` because the prior operations
157+ // seem to have altered its binary content. Hence, we take the raw envelope and
158+ // look up the place where the zlib compression header(0x78 0x9c) starts
159+ for ( let i = 0 ; i < envelopeBytes . length ; i ++ ) {
160+ if ( envelopeBytes [ i ] === 0x78 && envelopeBytes [ i + 1 ] === 0x9c ) {
161+ try {
162+ // We found a zlib-compressed payload - let's decompress it
163+ const payload = envelopeBytes . slice ( i ) ;
164+ // now we return the decompressed payload as JSON
165+ const decompressedPayload = pako . inflate ( payload as unknown as Uint8Array , { to : 'string' } ) ;
166+ return JSON . parse ( decompressedPayload ) ;
167+ } catch {
168+ // Let's log that something went wrong
169+ return line ;
170+ }
171+ }
172+ }
173+
174+ return line ;
175+ }
176+ } ) ;
177+
178+ return lines ;
179+ } ;
0 commit comments