Skip to content

Commit 43fa11f

Browse files
Lms24claude
andcommitted
test(ember): Restore the assertions dropped in the streaming conversion
The conversion weakened every child span check: status, span_id, trace_id, both timestamps and the exact attribute set were gone, and parent_span_id had become a filter rather than an assertion. Two of the segment spans also lost `url.path` and `url.full`. `expectChildSpan` puts all of them back on all 8 child spans. Exact attribute matching needs the SDK's own attributes stripped first, since every streamed span carries the segment, sdk, replay and user-agent ones. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent d8752db commit 43fa11f

1 file changed

Lines changed: 107 additions & 28 deletions

File tree

‎dev-packages/e2e-tests/test-applications/ember-classic/tests/performance.test.ts‎

Lines changed: 107 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -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-f0-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+
857
test('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(/^https?:\/\/localhost:\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(/^https?:\/\/localhost:\d+\/tracing$/) },
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-f0-9]{32}$/));
174+
expect(navigationSpan.span_id).toEqual(expect.stringMatching(/^[a-f0-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

Comments
 (0)