@@ -8,142 +8,174 @@ import {
8
8
shouldSkipTracingTest ,
9
9
} from '../../../utils/helpers' ;
10
10
11
- sentryTest ( 'does not send profile envelope when document-policy is not set' , async ( { page, getLocalTestUrl } ) => {
12
- if ( shouldSkipTracingTest ( ) ) {
13
- // Profiling only works when tracing is enabled
14
- sentryTest . skip ( ) ;
15
- }
16
-
17
- const url = await getLocalTestUrl ( { testDir : __dirname } ) ;
18
-
19
- // Assert that no profile_chunk envelope is sent without policy header
20
- const chunkCount = await countEnvelopes ( page , { url, envelopeType : 'profile_chunk' , timeout : 1500 } ) ;
21
- expect ( chunkCount ) . toBe ( 0 ) ;
22
- } ) ;
23
-
24
- sentryTest ( 'sends profile_chunk envelopes in trace mode (multiple chunks)' , async ( { page, getLocalTestUrl, browserName } ) => {
25
- if ( shouldSkipTracingTest ( ) || browserName !== 'chromium' ) {
26
- // Profiling only works when tracing is enabled
27
- sentryTest . skip ( ) ;
28
- }
29
-
30
- const url = await getLocalTestUrl ( { testDir : __dirname , responseHeaders : { 'Document-Policy' : 'js-profiling' } } ) ;
31
- await page . goto ( url ) ;
32
-
33
- // Expect at least 2 chunks because subject creates two separate root spans,
34
- // causing the profiler to stop and emit a chunk after each root span ends.
35
- const profileChunkEnvelopes = await getMultipleSentryEnvelopeRequests < ProfileChunkEnvelope > (
36
- page ,
37
- 2 ,
38
- { envelopeType : 'profile_chunk' , timeout : 5000 } ,
39
- properFullEnvelopeRequestParser ,
40
- ) ;
41
-
42
- expect ( profileChunkEnvelopes . length ) . toBeGreaterThanOrEqual ( 2 ) ;
43
-
44
- // Validate the first chunk thoroughly
45
- const profileChunkEnvelopeItem = profileChunkEnvelopes [ 0 ] [ 1 ] [ 0 ] ;
46
- const envelopeItemHeader = profileChunkEnvelopeItem [ 0 ] ;
47
- const envelopeItemPayload = profileChunkEnvelopeItem [ 1 ] ;
48
-
49
- expect ( envelopeItemHeader ) . toHaveProperty ( 'type' , 'profile_chunk' ) ;
50
-
51
- expect ( envelopeItemPayload . profile ) . toBeDefined ( ) ;
52
- expect ( envelopeItemPayload . version ) . toBe ( '2' ) ;
53
- expect ( envelopeItemPayload . platform ) . toBe ( 'javascript' ) ;
54
-
55
- const profile = envelopeItemPayload . profile ;
56
-
57
- expect ( profile . samples ) . toBeDefined ( ) ;
58
- expect ( profile . stacks ) . toBeDefined ( ) ;
59
- expect ( profile . frames ) . toBeDefined ( ) ;
60
- expect ( profile . thread_metadata ) . toBeDefined ( ) ;
61
-
62
- // Samples
63
- expect ( profile . samples . length ) . toBeGreaterThanOrEqual ( 2 ) ;
64
- let previousTimestamp = Number . NEGATIVE_INFINITY ;
65
- for ( const sample of profile . samples ) {
66
- expect ( typeof sample . stack_id ) . toBe ( 'number' ) ;
67
- expect ( sample . stack_id ) . toBeGreaterThanOrEqual ( 0 ) ;
68
- expect ( sample . stack_id ) . toBeLessThan ( profile . stacks . length ) ;
69
-
70
- // In trace lifecycle mode, samples carry a numeric timestamp (ms since epoch or similar clock)
71
- expect ( typeof ( sample as any ) . timestamp ) . toBe ( 'number' ) ;
72
- const ts = ( sample as any ) . timestamp as number ;
73
- expect ( Number . isFinite ( ts ) ) . toBe ( true ) ;
74
- expect ( ts ) . toBeGreaterThan ( 0 ) ;
75
- // Monotonic non-decreasing timestamps
76
- expect ( ts ) . toBeGreaterThanOrEqual ( previousTimestamp ) ;
77
- previousTimestamp = ts ;
78
-
79
- expect ( sample . thread_id ) . toBe ( '0' ) ; // Should be main thread
80
- }
81
-
82
- // Stacks
83
- expect ( profile . stacks . length ) . toBeGreaterThan ( 0 ) ;
84
- for ( const stack of profile . stacks ) {
85
- expect ( Array . isArray ( stack ) ) . toBe ( true ) ;
86
- for ( const frameIndex of stack ) {
87
- expect ( typeof frameIndex ) . toBe ( 'number' ) ;
88
- expect ( frameIndex ) . toBeGreaterThanOrEqual ( 0 ) ;
89
- expect ( frameIndex ) . toBeLessThan ( profile . frames . length ) ;
11
+ sentryTest (
12
+ 'does not send profile envelope when document-policy is not set' ,
13
+ async ( { page, getLocalTestUrl, browserName } ) => {
14
+ if ( shouldSkipTracingTest ( ) || browserName !== 'chromium' ) {
15
+ // Profiling only works when tracing is enabled
16
+ sentryTest . skip ( ) ;
90
17
}
91
- }
92
-
93
- // Frames
94
- expect ( profile . frames . length ) . toBeGreaterThan ( 0 ) ;
95
- for ( const frame of profile . frames ) {
96
- expect ( frame ) . toHaveProperty ( 'function' ) ;
97
- expect ( frame ) . toHaveProperty ( 'abs_path' ) ;
98
- expect ( frame ) . toHaveProperty ( 'lineno' ) ;
99
- expect ( frame ) . toHaveProperty ( 'colno' ) ;
100
-
101
- expect ( typeof frame . function ) . toBe ( 'string' ) ;
102
- expect ( typeof frame . abs_path ) . toBe ( 'string' ) ;
103
- expect ( typeof frame . lineno ) . toBe ( 'number' ) ;
104
- expect ( typeof frame . colno ) . toBe ( 'number' ) ;
105
- }
106
-
107
- const functionNames = profile . frames . map ( frame => frame . function ) . filter ( name => name !== '' ) ;
108
-
109
- if ( ( process . env . PW_BUNDLE || '' ) . endsWith ( 'min' ) ) {
110
- // In bundled mode, function names are minified
111
- expect ( functionNames . length ) . toBeGreaterThan ( 0 ) ;
112
- expect ( ( functionNames as string [ ] ) . every ( name => name ?. length > 0 ) ) . toBe ( true ) ; // Just make sure they're not empty strings
113
- } else {
114
- expect ( functionNames ) . toEqual (
115
- expect . arrayContaining ( [
116
- '_startRootSpan' ,
117
- 'withScope' ,
118
- 'createChildOrRootSpan' ,
119
- 'startSpanManual' ,
120
- 'startJSSelfProfile' ,
121
-
122
- // both functions are captured
123
- 'fibonacci' ,
124
- 'largeSum' ,
125
- ] ) ,
18
+
19
+ const url = await getLocalTestUrl ( { testDir : __dirname } ) ;
20
+
21
+ // Assert that no profile_chunk envelope is sent without policy header
22
+ const chunkCount = await countEnvelopes ( page , { url, envelopeType : 'profile_chunk' , timeout : 1500 } ) ;
23
+ expect ( chunkCount ) . toBe ( 0 ) ;
24
+ } ,
25
+ ) ;
26
+
27
+ sentryTest (
28
+ 'sends profile_chunk envelopes in trace mode (multiple chunks)' ,
29
+ async ( { page, getLocalTestUrl, browserName } ) => {
30
+ if ( shouldSkipTracingTest ( ) || browserName !== 'chromium' ) {
31
+ // Profiling only works when tracing is enabled
32
+ sentryTest . skip ( ) ;
33
+ }
34
+
35
+ const url = await getLocalTestUrl ( { testDir : __dirname , responseHeaders : { 'Document-Policy' : 'js-profiling' } } ) ;
36
+ await page . goto ( url ) ;
37
+
38
+ // Expect at least 2 chunks because subject creates two separate root spans,
39
+ // causing the profiler to stop and emit a chunk after each root span ends.
40
+ const profileChunkEnvelopes = await getMultipleSentryEnvelopeRequests < ProfileChunkEnvelope > (
41
+ page ,
42
+ 2 ,
43
+ { envelopeType : 'profile_chunk' , timeout : 5000 } ,
44
+ properFullEnvelopeRequestParser ,
126
45
) ;
127
- }
128
-
129
- expect ( profile . thread_metadata ) . toHaveProperty ( '0' ) ;
130
- expect ( profile . thread_metadata [ '0' ] ) . toHaveProperty ( 'name' ) ;
131
- expect ( profile . thread_metadata [ '0' ] . name ) . toBe ( 'main' ) ;
132
-
133
- // Test that profile duration makes sense (should be > 20ms based on test setup)
134
- const startTimeMs = ( profile . samples [ 0 ] as any ) . timestamp as number ;
135
- const endTimeMs = ( profile . samples [ profile . samples . length - 1 ] as any ) . timestamp as number ;
136
- const durationMs = endTimeMs - startTimeMs ;
137
-
138
- // Should be at least 20ms based on our setTimeout(21) in the test
139
- expect ( durationMs ) . toBeGreaterThan ( 20 ) ;
140
-
141
- // Basic sanity on the second chunk: has correct envelope type and structure
142
- const secondChunkItem = profileChunkEnvelopes [ 1 ] [ 1 ] [ 0 ] ;
143
- const secondHeader = secondChunkItem [ 0 ] ;
144
- const secondPayload = secondChunkItem [ 1 ] ;
145
- expect ( secondHeader ) . toHaveProperty ( 'type' , 'profile_chunk' ) ;
146
- expect ( secondPayload . profile ) . toBeDefined ( ) ;
147
- expect ( secondPayload . version ) . toBe ( '2' ) ;
148
- expect ( secondPayload . platform ) . toBe ( 'javascript' ) ;
149
- } ) ;
46
+
47
+ expect ( profileChunkEnvelopes . length ) . toBeGreaterThanOrEqual ( 2 ) ;
48
+
49
+ // Validate the first chunk thoroughly
50
+ const profileChunkEnvelopeItem = profileChunkEnvelopes [ 0 ] [ 1 ] [ 0 ] ;
51
+ const envelopeItemHeader = profileChunkEnvelopeItem [ 0 ] ;
52
+ const envelopeItemPayload1 = profileChunkEnvelopeItem [ 1 ] ;
53
+
54
+ expect ( envelopeItemHeader ) . toHaveProperty ( 'type' , 'profile_chunk' ) ;
55
+
56
+ expect ( envelopeItemPayload1 . profile ) . toBeDefined ( ) ;
57
+ expect ( envelopeItemPayload1 . version ) . toBe ( '2' ) ;
58
+ expect ( envelopeItemPayload1 . platform ) . toBe ( 'javascript' ) ;
59
+
60
+ const profile1 = envelopeItemPayload1 . profile ;
61
+
62
+ expect ( profile1 . samples ) . toBeDefined ( ) ;
63
+ expect ( profile1 . stacks ) . toBeDefined ( ) ;
64
+ expect ( profile1 . frames ) . toBeDefined ( ) ;
65
+ expect ( profile1 . thread_metadata ) . toBeDefined ( ) ;
66
+
67
+ // Samples
68
+ expect ( profile1 . samples . length ) . toBeGreaterThanOrEqual ( 2 ) ;
69
+ let previousTimestamp = Number . NEGATIVE_INFINITY ;
70
+ for ( const sample of profile1 . samples ) {
71
+ expect ( typeof sample . stack_id ) . toBe ( 'number' ) ;
72
+ expect ( sample . stack_id ) . toBeGreaterThanOrEqual ( 0 ) ;
73
+ expect ( sample . stack_id ) . toBeLessThan ( profile1 . stacks . length ) ;
74
+
75
+ // In trace lifecycle mode, samples carry a numeric timestamp (ms since epoch or similar clock)
76
+ expect ( typeof ( sample as any ) . timestamp ) . toBe ( 'number' ) ;
77
+ const ts = ( sample as any ) . timestamp as number ;
78
+ expect ( Number . isFinite ( ts ) ) . toBe ( true ) ;
79
+ expect ( ts ) . toBeGreaterThan ( 0 ) ;
80
+ // Monotonic non-decreasing timestamps
81
+ expect ( ts ) . toBeGreaterThanOrEqual ( previousTimestamp ) ;
82
+ previousTimestamp = ts ;
83
+
84
+ expect ( sample . thread_id ) . toBe ( '0' ) ; // Should be main thread
85
+ }
86
+
87
+ // Stacks
88
+ expect ( profile1 . stacks . length ) . toBeGreaterThan ( 0 ) ;
89
+ for ( const stack of profile1 . stacks ) {
90
+ expect ( Array . isArray ( stack ) ) . toBe ( true ) ;
91
+ for ( const frameIndex of stack ) {
92
+ expect ( typeof frameIndex ) . toBe ( 'number' ) ;
93
+ expect ( frameIndex ) . toBeGreaterThanOrEqual ( 0 ) ;
94
+ expect ( frameIndex ) . toBeLessThan ( profile1 . frames . length ) ;
95
+ }
96
+ }
97
+
98
+ // Frames
99
+ expect ( profile1 . frames . length ) . toBeGreaterThan ( 0 ) ;
100
+ for ( const frame of profile1 . frames ) {
101
+ expect ( frame ) . toHaveProperty ( 'function' ) ;
102
+ expect ( frame ) . toHaveProperty ( 'abs_path' ) ;
103
+ expect ( frame ) . toHaveProperty ( 'lineno' ) ;
104
+ expect ( frame ) . toHaveProperty ( 'colno' ) ;
105
+
106
+ expect ( typeof frame . function ) . toBe ( 'string' ) ;
107
+ expect ( typeof frame . abs_path ) . toBe ( 'string' ) ;
108
+ expect ( typeof frame . lineno ) . toBe ( 'number' ) ;
109
+ expect ( typeof frame . colno ) . toBe ( 'number' ) ;
110
+ }
111
+
112
+ const functionNames = profile1 . frames . map ( frame => frame . function ) . filter ( name => name !== '' ) ;
113
+
114
+ if ( ( process . env . PW_BUNDLE || '' ) . endsWith ( 'min' ) ) {
115
+ // In bundled mode, function names are minified
116
+ expect ( functionNames . length ) . toBeGreaterThan ( 0 ) ;
117
+ expect ( ( functionNames as string [ ] ) . every ( name => name ?. length > 0 ) ) . toBe ( true ) ; // Just make sure they're not empty strings
118
+ } else {
119
+ expect ( functionNames ) . toEqual (
120
+ expect . arrayContaining ( [
121
+ '_startRootSpan' ,
122
+ 'withScope' ,
123
+ 'createChildOrRootSpan' ,
124
+ 'startSpanManual' ,
125
+ 'startJSSelfProfile' ,
126
+
127
+ // first function is captured (other one is in other chunk)
128
+ 'fibonacci' ,
129
+ ] ) ,
130
+ ) ;
131
+ }
132
+
133
+ expect ( profile1 . thread_metadata ) . toHaveProperty ( '0' ) ;
134
+ expect ( profile1 . thread_metadata [ '0' ] ) . toHaveProperty ( 'name' ) ;
135
+ expect ( profile1 . thread_metadata [ '0' ] . name ) . toBe ( 'main' ) ;
136
+
137
+ // Test that profile duration makes sense (should be > 20ms based on test setup)
138
+ const startTimeMs = ( profile1 . samples [ 0 ] as any ) . timestamp as number ;
139
+ const endTimeMs = ( profile1 . samples [ profile1 . samples . length - 1 ] as any ) . timestamp as number ;
140
+ const durationMs = endTimeMs - startTimeMs ;
141
+
142
+ // Should be at least 20ms based on our setTimeout(21) in the test
143
+ expect ( durationMs ) . toBeGreaterThan ( 20 ) ;
144
+
145
+ // === PROFILE CHUNK 2 ===
146
+
147
+ const profileChunkEnvelopeItem2 = profileChunkEnvelopes [ 1 ] [ 1 ] [ 0 ] ;
148
+ const envelopeItemHeader2 = profileChunkEnvelopeItem2 [ 0 ] ;
149
+ const envelopeItemPayload2 = profileChunkEnvelopeItem2 [ 1 ] ;
150
+
151
+ // Basic sanity on the second chunk: has correct envelope type and structure
152
+ expect ( envelopeItemHeader2 ) . toHaveProperty ( 'type' , 'profile_chunk' ) ;
153
+ expect ( envelopeItemPayload2 . profile ) . toBeDefined ( ) ;
154
+ expect ( envelopeItemPayload2 . version ) . toBe ( '2' ) ;
155
+ expect ( envelopeItemPayload2 . platform ) . toBe ( 'javascript' ) ;
156
+ expect ( envelopeItemPayload2 ?. profile ) . toBeDefined ( ) ;
157
+
158
+ const profile2 = envelopeItemPayload2 . profile ;
159
+
160
+ const functionNames2 = profile2 . frames . map ( frame => frame . function ) . filter ( name => name !== '' ) ;
161
+
162
+ if ( ( process . env . PW_BUNDLE || '' ) . endsWith ( 'min' ) ) {
163
+ // In bundled mode, function names are minified
164
+ expect ( functionNames2 . length ) . toBeGreaterThan ( 0 ) ;
165
+ expect ( ( functionNames2 as string [ ] ) . every ( name => name ?. length > 0 ) ) . toBe ( true ) ; // Just make sure they're not empty strings
166
+ } else {
167
+ expect ( functionNames2 ) . toEqual (
168
+ expect . arrayContaining ( [
169
+ '_startRootSpan' ,
170
+ 'withScope' ,
171
+ 'createChildOrRootSpan' ,
172
+ 'startSpanManual' ,
173
+ 'startJSSelfProfile' ,
174
+
175
+ // second function is captured (other one is in other chunk)
176
+ 'largeSum' ,
177
+ ] ) ,
178
+ ) ;
179
+ }
180
+ } ,
181
+ ) ;
0 commit comments