@@ -5,6 +5,55 @@ import { collectStreamedSpansUntilSegment, getSpanOp, waitForStreamedSpan } from
55// This app runs on the default `traceLifecycle: 'stream'`, so it emits spans, not transactions.
66// `ember-embroider` and `ember-vite` cover the same routes on the static lifecycle.
77
8+ // The SDK stamps these onto every streamed span, whatever produced it. Stripping them lets the
9+ // instrumentation's own attributes be asserted exactly, the way the static suites assert `span.data`.
10+ const SDK_ATTRIBUTES = [
11+ 'sentry.trace_lifecycle' ,
12+ 'sentry.segment.name' ,
13+ 'sentry.segment.id' ,
14+ 'sentry.sdk.name' ,
15+ 'sentry.sdk.version' ,
16+ 'sentry.sdk.integrations' ,
17+ 'sentry.environment' ,
18+ 'sentry.release' ,
19+ 'sentry.sample_rate' ,
20+ 'user.id' ,
21+ 'user.email' ,
22+ 'user.ip_address' ,
23+ 'user.username' ,
24+ // `httpContextIntegration` sets this on every span; replay is enabled in this app, so it tags
25+ // every span too.
26+ 'user_agent.original' ,
27+ 'sentry.replay_id' ,
28+ 'sentry._internal.replay_is_buffering' ,
29+ ] ;
30+
31+ function instrumentationAttributes ( span : SerializedStreamedSpan ) : Record < string , unknown > {
32+ return Object . fromEntries (
33+ Object . entries ( span . attributes )
34+ . filter ( ( [ key ] ) => ! SDK_ATTRIBUTES . includes ( key ) )
35+ . map ( ( [ key , attribute ] ) => [ key , attribute . value ] ) ,
36+ ) ;
37+ }
38+
39+ function expectChildSpan (
40+ span : SerializedStreamedSpan | undefined ,
41+ segmentSpan : SerializedStreamedSpan ,
42+ expected : { name : string ; attributes : Record < string , unknown > } ,
43+ ) : void {
44+ expect ( span ) . toMatchObject ( {
45+ name : expected . name ,
46+ is_segment : false ,
47+ status : 'ok' ,
48+ parent_span_id : segmentSpan . span_id ,
49+ span_id : expect . stringMatching ( / ^ [ a - f 0 - 9 ] { 16 } $ / ) ,
50+ trace_id : segmentSpan . trace_id ,
51+ start_timestamp : expect . any ( Number ) ,
52+ end_timestamp : expect . any ( Number ) ,
53+ } ) ;
54+ expect ( instrumentationAttributes ( span ! ) ) . toEqual ( expected . attributes ) ;
55+ }
56+
857test ( 'sends a pageload span with a parameterized URL' , async ( { page } ) => {
958 const pageloadSpanPromise = waitForStreamedSpan (
1059 'ember-classic' ,
@@ -87,6 +136,8 @@ test('sends a navigation span even if the pageload span is still active', async
87136 'sentry.origin' : { type : 'string' , value : 'auto.pageload.ember' } ,
88137 'sentry.segment.name.source' : { type : 'string' , value : 'route' } ,
89138 'url.template' : { type : 'string' , value : '/' } ,
139+ 'url.path' : { type : 'string' , value : '/' } ,
140+ 'url.full' : { type : 'string' , value : expect . stringMatching ( / ^ h t t p s ? : \/ \/ l o c a l h o s t : \d + \/ $ / ) } ,
90141 } ,
91142 } ) ;
92143
@@ -98,6 +149,8 @@ test('sends a navigation span even if the pageload span is still active', async
98149 'sentry.origin' : { type : 'string' , value : 'auto.navigation.ember' } ,
99150 'sentry.segment.name.source' : { type : 'string' , value : 'route' } ,
100151 'url.template' : { type : 'string' , value : '/tracing' } ,
152+ 'url.path' : { type : 'string' , value : '/tracing' } ,
153+ 'url.full' : { type : 'string' , value : expect . stringMatching ( / ^ h t t p s ? : \/ \/ l o c a l h o s t : \d + \/ t r a c i n g $ / ) } ,
101154 } ,
102155 } ) ;
103156} ) ;
@@ -115,9 +168,15 @@ test('captures correct spans for navigation', async ({ page }) => {
115168 await page . getByText ( 'Measure Things!' ) . click ( ) ;
116169 const spans = await spansPromise ;
117170
118- const navigationSpan = spans . find ( span => span . is_segment ) ;
171+ const navigationSpan = spans . find ( span => span . is_segment ) ! ;
172+
173+ expect ( navigationSpan . trace_id ) . toEqual ( expect . stringMatching ( / ^ [ a - f 0 - 9 ] { 32 } $ / ) ) ;
174+ expect ( navigationSpan . span_id ) . toEqual ( expect . stringMatching ( / ^ [ a - f 0 - 9 ] { 16 } $ / ) ) ;
175+
119176 expect ( navigationSpan ) . toMatchObject ( {
120177 name : 'route:slow-loading-route.index' ,
178+ is_segment : true ,
179+ status : 'ok' ,
121180 attributes : {
122181 'sentry.op' : { type : 'string' , value : 'navigation' } ,
123182 'sentry.origin' : { type : 'string' , value : 'auto.navigation.ember' } ,
@@ -132,7 +191,7 @@ test('captures correct spans for navigation', async ({ page }) => {
132191 spans . filter (
133192 span =>
134193 ! span . is_segment &&
135- span . parent_span_id === navigationSpan ? .span_id &&
194+ span . parent_span_id === navigationSpan . span_id &&
136195 getSpanOp ( span ) === op &&
137196 ( functionName === undefined || span . attributes [ 'code.function.name' ] ?. value === functionName ) ,
138197 ) ;
@@ -141,9 +200,7 @@ test('captures correct spans for navigation', async ({ page }) => {
141200 const beforeModelSpans = childrenOf ( 'function' , 'beforeModel' ) ;
142201 const modelSpans = childrenOf ( 'function' , 'model' ) ;
143202 const afterModelSpans = childrenOf ( 'function' , 'afterModel' ) ;
144- const runloopRenderSpans = childrenOf ( 'ui.task' ) . filter (
145- span => span . attributes [ 'ember.runloop.queue' ] ?. value === 'render' ,
146- ) ;
203+ const renderSpans = childrenOf ( 'ui.task' ) . filter ( span => span . attributes [ 'ember.runloop.queue' ] ?. value === 'render' ) ;
147204
148205 expect ( transitionSpans ) . toHaveLength ( 1 ) ;
149206
@@ -153,15 +210,15 @@ test('captures correct spans for navigation', async ({ page }) => {
153210 expect ( afterModelSpans ) . toHaveLength ( 2 ) ;
154211
155212 // There may be many render spans...
156- expect ( runloopRenderSpans . length ) . toBeGreaterThan ( 1 ) ;
213+ expect ( renderSpans . length ) . toBeGreaterThan ( 1 ) ;
157214
158215 // Ember has no route template for the transition itself, so a streamed router span takes the
159216 // static fallback rather than the `route:a -> route:b` pair it uses on the static lifecycle.
160- expect ( transitionSpans [ 0 ] ) . toMatchObject ( {
217+ expectChildSpan ( transitionSpans [ 0 ] , navigationSpan , {
161218 name : 'Router' ,
162219 attributes : {
163- 'sentry.op' : { type : 'string' , value : ' router' } ,
164- 'sentry.origin' : { type : 'string' , value : ' auto.ui.ember' } ,
220+ 'sentry.op' : ' router',
221+ 'sentry.origin' : ' auto.ui.ember',
165222 } ,
166223 } ) ;
167224
@@ -172,25 +229,33 @@ test('captures correct spans for navigation', async ({ page }) => {
172229 [ 'model' , modelSpans ] ,
173230 [ 'afterModel' , afterModelSpans ] ,
174231 ] as const ) {
175- expect ( hookSpans . map ( span => span . name ) ) . toEqual ( [ hookName , hookName ] ) ;
176- expect ( hookSpans ) . toMatchObject (
177- [ 'slow-loading-route' , 'slow-loading-route.index' ] . map ( fullRouteName => ( {
178- attributes : {
179- 'code.function.name' : { type : 'string' , value : hookName } ,
180- 'sentry.description' : { type : 'string' , value : fullRouteName } ,
181- 'sentry.op' : { type : 'string' , value : 'function' } ,
182- 'sentry.origin' : { type : 'string' , value : 'auto.ui.ember' } ,
183- } ,
184- } ) ) ,
185- ) ;
232+ expectChildSpan ( hookSpans [ 0 ] , navigationSpan , {
233+ name : hookName ,
234+ attributes : {
235+ 'code.function.name' : hookName ,
236+ 'sentry.description' : 'slow-loading-route' ,
237+ 'sentry.op' : 'function' ,
238+ 'sentry.origin' : 'auto.ui.ember' ,
239+ } ,
240+ } ) ;
241+
242+ expectChildSpan ( hookSpans [ 1 ] , navigationSpan , {
243+ name : hookName ,
244+ attributes : {
245+ 'code.function.name' : hookName ,
246+ 'sentry.description' : 'slow-loading-route.index' ,
247+ 'sentry.op' : 'function' ,
248+ 'sentry.origin' : 'auto.ui.ember' ,
249+ } ,
250+ } ) ;
186251 }
187252
188- expect ( runloopRenderSpans [ 0 ] ) . toMatchObject ( {
253+ expectChildSpan ( renderSpans [ 0 ] , navigationSpan , {
189254 name : 'runloop' ,
190255 attributes : {
191- 'ember.runloop.queue' : { type : 'string' , value : ' render' } ,
192- 'sentry.op' : { type : 'string' , value : ' ui.task' } ,
193- 'sentry.origin' : { type : 'string' , value : ' auto.ui.ember' } ,
256+ 'ember.runloop.queue' : ' render',
257+ 'sentry.op' : ' ui.task',
258+ 'sentry.origin' : ' auto.ui.ember',
194259 } ,
195260 } ) ;
196261} ) ;
@@ -202,6 +267,7 @@ test('captures a `ui.resolve` span alongside the `ui.render` span for a componen
202267
203268 const spans = await spansPromise ;
204269
270+ const pageloadSpan = spans . find ( span => span . is_segment ) ! ;
205271 const resolveSpans = spans . filter ( span => getSpanOp ( span ) === 'ui.resolve' ) ;
206272 const renderSpans = spans . filter ( span => getSpanOp ( span ) === 'ui.render' ) ;
207273
@@ -211,9 +277,22 @@ test('captures a `ui.resolve` span alongside the `ui.render` span for a componen
211277 // Resolving a component definition and rendering it are separate steps, so they must not share
212278 // an op even though they carry the same component name.
213279 const resolveSpan = resolveSpans [ 0 ] ! ;
214- expect ( resolveSpan . attributes ) . toMatchObject ( {
215- 'sentry.op' : { type : 'string' , value : 'ui.resolve' } ,
216- 'sentry.origin' : { type : 'string' , value : 'auto.ui.ember' } ,
217- 'ui.component_name' : { type : 'string' , value : resolveSpan . name } ,
280+ expectChildSpan ( resolveSpan , pageloadSpan , {
281+ name : resolveSpan . name ,
282+ attributes : {
283+ 'sentry.op' : 'ui.resolve' ,
284+ 'sentry.origin' : 'auto.ui.ember' ,
285+ 'ui.component_name' : resolveSpan . name ,
286+ } ,
287+ } ) ;
288+
289+ const renderSpan = renderSpans [ 0 ] ! ;
290+ expectChildSpan ( renderSpan , pageloadSpan , {
291+ name : renderSpan . name ,
292+ attributes : {
293+ 'sentry.op' : 'ui.render' ,
294+ 'sentry.origin' : 'auto.ui.ember' ,
295+ 'ui.component_name' : renderSpan . name ,
296+ } ,
218297 } ) ;
219298} ) ;
0 commit comments