@@ -6,11 +6,41 @@ import type {
66 EventEnvelope ,
77 EventItem ,
88 InternalBaseTransportOptions ,
9+ ReplayEnvelope ,
10+ ReplayEvent ,
911 TransportMakeRequestResponse ,
1012} from '@sentry/types' ;
11- import { createEnvelope } from '@sentry/utils' ;
13+ import {
14+ createEnvelope ,
15+ createEventEnvelopeHeaders ,
16+ dsnFromString ,
17+ getSdkMetadataForEnvelopeHeader ,
18+ parseEnvelope ,
19+ } from '@sentry/utils' ;
20+
21+ // Credit for this awful hack: https://github.com/vitest-dev/vitest/issues/4043#issuecomment-1905172846
22+ class JSDOMCompatibleTextEncoder extends TextEncoder {
23+ encode ( input : string ) {
24+ if ( typeof input !== 'string' ) {
25+ throw new TypeError ( '`input` must be a string' ) ;
26+ }
27+
28+ const decodedURI = decodeURIComponent ( encodeURIComponent ( input ) ) ;
29+ const arr = new Uint8Array ( decodedURI . length ) ;
30+ const chars = decodedURI . split ( '' ) ;
31+ for ( let i = 0 ; i < chars . length ; i ++ ) {
32+ arr [ i ] = decodedURI [ i ] . charCodeAt ( 0 ) ;
33+ }
34+ return arr ;
35+ }
36+ }
37+
38+ Object . defineProperty ( global , 'TextEncoder' , {
39+ value : JSDOMCompatibleTextEncoder ,
40+ writable : true ,
41+ } ) ;
1242
13- import { MIN_DELAY } from '../../../../core/src/transports/offline' ;
43+ import { MIN_DELAY , START_DELAY } from '../../../../core/src/transports/offline' ;
1444import { createStore , makeBrowserOfflineTransport , pop , push , unshift } from '../../../src/transports/offline' ;
1545
1646function deleteDatabase ( name : string ) : Promise < void > {
@@ -25,26 +55,62 @@ const ERROR_ENVELOPE = createEnvelope<EventEnvelope>({ event_id: 'aa3ff046696b4b
2555 [ { type : 'event' } , { event_id : 'aa3ff046696b4bc6b609ce6d28fde9e2' } ] as EventItem ,
2656] ) ;
2757
58+ function createReplayEnvelope ( message : string ) {
59+ const event : ReplayEvent = {
60+ type : 'replay_event' ,
61+ timestamp : 1670837008.634 ,
62+ error_ids : [ 'errorId' ] ,
63+ trace_ids : [ 'traceId' ] ,
64+ urls : [ 'https://example.com' ] ,
65+ replay_id : 'MY_REPLAY_ID' ,
66+ segment_id : 3 ,
67+ replay_type : 'buffer' ,
68+ message,
69+ } ;
70+
71+ const data = 'nothing' ;
72+
73+ return createEnvelope < ReplayEnvelope > (
74+ createEventEnvelopeHeaders (
75+ event ,
76+ getSdkMetadataForEnvelopeHeader ( event ) ,
77+ undefined ,
78+ dsnFromString ( 'https://[email protected] /1337' ) , 79+ ) ,
80+ [
81+ [ { type : 'replay_event' } , event ] ,
82+ [
83+ {
84+ type : 'replay_recording' ,
85+ length : data . length ,
86+ } ,
87+ data ,
88+ ] ,
89+ ] ,
90+ ) ;
91+ }
92+
2893const transportOptions = {
2994 recordDroppedEvent : ( ) => undefined , // noop
3095} ;
3196
3297type MockResult < T > = T | Error ;
3398
3499export const createTestTransport = ( ...sendResults : MockResult < TransportMakeRequestResponse > [ ] ) => {
35- let sendCount = 0 ;
100+ const envelopes : Array < string | Uint8Array > = [ ] ;
36101
37102 return {
38- getSendCount : ( ) => sendCount ,
103+ getSendCount : ( ) => envelopes . length ,
104+ getSentEnvelopes : ( ) => envelopes ,
39105 baseTransport : ( options : InternalBaseTransportOptions ) =>
40- createTransport ( options , ( ) => {
106+ createTransport ( options , ( { body } ) => {
41107 return new Promise ( ( resolve , reject ) => {
42108 const next = sendResults . shift ( ) ;
43109
44110 if ( next instanceof Error ) {
45111 reject ( next ) ;
46112 } else {
47- sendCount += 1 ;
113+ envelopes . push ( body ) ;
48114 resolve ( next as TransportMakeRequestResponse ) ;
49115 }
50116 } ) ;
@@ -112,4 +178,37 @@ describe('makeOfflineTransport', () => {
112178 expect ( queuedCount ) . toEqual ( 1 ) ;
113179 expect ( getSendCount ( ) ) . toEqual ( 2 ) ;
114180 } ) ;
181+
182+ it ( 'Retains order of replay envelopes' , async ( ) => {
183+ const { getSentEnvelopes, baseTransport } = createTestTransport (
184+ { statusCode : 200 } ,
185+ // We reject the second envelope to ensure the order is still retained
186+ new Error ( ) ,
187+ { statusCode : 200 } ,
188+ { statusCode : 200 } ,
189+ ) ;
190+
191+ const transport = makeBrowserOfflineTransport ( baseTransport ) ( {
192+ ...transportOptions ,
193+ url : 'http://localhost' ,
194+ } ) ;
195+
196+ await transport . send ( createReplayEnvelope ( '1' ) ) ;
197+ // This one will fail and get resent in order
198+ await transport . send ( createReplayEnvelope ( '2' ) ) ;
199+ await transport . send ( createReplayEnvelope ( '3' ) ) ;
200+
201+ await delay ( START_DELAY * 2 ) ;
202+
203+ const envelopes = getSentEnvelopes ( )
204+ . map ( buf => ( typeof buf === 'string' ? buf : new TextDecoder ( ) . decode ( buf ) ) )
205+ . map ( parseEnvelope ) ;
206+
207+ expect ( envelopes ) . toHaveLength ( 3 ) ;
208+
209+ // Ensure they're still in the correct order
210+ expect ( ( envelopes [ 0 ] [ 1 ] [ 0 ] [ 1 ] as ErrorEvent ) . message ) . toEqual ( '1' ) ;
211+ expect ( ( envelopes [ 1 ] [ 1 ] [ 0 ] [ 1 ] as ErrorEvent ) . message ) . toEqual ( '2' ) ;
212+ expect ( ( envelopes [ 2 ] [ 1 ] [ 0 ] [ 1 ] as ErrorEvent ) . message ) . toEqual ( '3' ) ;
213+ } , 25_000 ) ;
115214} ) ;
0 commit comments