@@ -6,11 +6,41 @@ import type {
6
6
EventEnvelope ,
7
7
EventItem ,
8
8
InternalBaseTransportOptions ,
9
+ ReplayEnvelope ,
10
+ ReplayEvent ,
9
11
TransportMakeRequestResponse ,
10
12
} 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
+ } ) ;
12
42
13
- import { MIN_DELAY } from '../../../../core/src/transports/offline' ;
43
+ import { MIN_DELAY , START_DELAY } from '../../../../core/src/transports/offline' ;
14
44
import { createStore , makeBrowserOfflineTransport , pop , push , unshift } from '../../../src/transports/offline' ;
15
45
16
46
function deleteDatabase ( name : string ) : Promise < void > {
@@ -25,26 +55,62 @@ const ERROR_ENVELOPE = createEnvelope<EventEnvelope>({ event_id: 'aa3ff046696b4b
25
55
[ { type : 'event' } , { event_id : 'aa3ff046696b4bc6b609ce6d28fde9e2' } ] as EventItem ,
26
56
] ) ;
27
57
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
+
28
93
const transportOptions = {
29
94
recordDroppedEvent : ( ) => undefined , // noop
30
95
} ;
31
96
32
97
type MockResult < T > = T | Error ;
33
98
34
99
export const createTestTransport = ( ...sendResults : MockResult < TransportMakeRequestResponse > [ ] ) => {
35
- let sendCount = 0 ;
100
+ const envelopes : Array < string | Uint8Array > = [ ] ;
36
101
37
102
return {
38
- getSendCount : ( ) => sendCount ,
103
+ getSendCount : ( ) => envelopes . length ,
104
+ getSentEnvelopes : ( ) => envelopes ,
39
105
baseTransport : ( options : InternalBaseTransportOptions ) =>
40
- createTransport ( options , ( ) => {
106
+ createTransport ( options , ( { body } ) => {
41
107
return new Promise ( ( resolve , reject ) => {
42
108
const next = sendResults . shift ( ) ;
43
109
44
110
if ( next instanceof Error ) {
45
111
reject ( next ) ;
46
112
} else {
47
- sendCount += 1 ;
113
+ envelopes . push ( body ) ;
48
114
resolve ( next as TransportMakeRequestResponse ) ;
49
115
}
50
116
} ) ;
@@ -112,4 +178,37 @@ describe('makeOfflineTransport', () => {
112
178
expect ( queuedCount ) . toEqual ( 1 ) ;
113
179
expect ( getSendCount ( ) ) . toEqual ( 2 ) ;
114
180
} ) ;
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 ) ;
115
214
} ) ;
0 commit comments