Skip to content

Commit 1ed3384

Browse files
Lms24claude
andauthored
feat(ember): Give component definition lookups their own span op (#24271)
Ember spans that resolve a component definition used the `function` op, which infers a description of "Function execution" and loses the component name. Switching them to `ui.render` would have collided with the render span that follows it: same name, same `ui.component_name`. So they now use `ui.resolve`, a new op proposed in getsentry/sentry-conventions#633. It is hard-coded with a TODO until that lands and we can import the const. This PR: - gives component definition lookups the `ui.resolve` op, in both trace lifecycles — unlike names, ops are not gated on span streaming - names route hook spans after the hook (`beforeModel`, `model`, …) when span streaming is enabled, which is the `function` op's `{{code.function.name}}` name template - keeps the route on `sentry.description`, but only when streaming: in the static lifecycle the span name is still the route, so the attribute would just duplicate it. Same call `@sentry/angular`'s `TraceMethod` makes. - turns on `enableComponentDefinitions` in the `ember-classic` E2E app, which is what covers `ui.resolve` — the flag was off in every app, so the code path had never run in CI - documents both changes in `MIGRATION.md` ref #23954 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 5c69b5f commit 1ed3384

9 files changed

Lines changed: 244 additions & 25 deletions

File tree

‎MIGRATION.md‎

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -896,13 +896,13 @@ These changes are not caught by TypeScript. If you filter, group, or alert on sp
896896

897897
**Frontend & UI:**
898898

899-
| Area | Before | After |
900-
| ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------- |
901-
| Frontend routing | `ui.angular.routing`, `ui.sveltekit.routing`, `ui.ember.transition` | `router` |
902-
| React, Vue & Svelte component lifecycles | `ui.react.mount`/`render`/`update`, `ui.svelte.init`/`update`, Vue `render`/`update`/`mount`/`create`/`activate`/`unmount`/`destroy` | `ui.mount`, `ui.render`, `ui.update`, `ui.unmount` |
903-
| Angular tracing decorators | `ui.angular.init` (`TraceDirective`/`TraceClass`), `ui.angular.<method>` (`TraceMethod`) | `ui.mount`, `function` |
904-
| Ember route hooks, runloop & components | `ui.ember.route.<hook>`, `ui.ember.runloop.<queue>`, `ui.ember.component.render`/`definition`/`init` | `function`, `ui.task`, `ui.render`/`function`/`ui.mount` |
905-
| Browser paint entries | `paint` | `browser.paint` |
899+
| Area | Before | After |
900+
| ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------- |
901+
| Frontend routing | `ui.angular.routing`, `ui.sveltekit.routing`, `ui.ember.transition` | `router` |
902+
| React, Vue & Svelte component lifecycles | `ui.react.mount`/`render`/`update`, `ui.svelte.init`/`update`, Vue `render`/`update`/`mount`/`create`/`activate`/`unmount`/`destroy` | `ui.mount`, `ui.render`, `ui.update`, `ui.unmount` |
903+
| Angular tracing decorators | `ui.angular.init` (`TraceDirective`/`TraceClass`), `ui.angular.<method>` (`TraceMethod`) | `ui.mount`, `function` |
904+
| Ember route hooks, runloop & components | `ui.ember.route.<hook>`, `ui.ember.runloop.<queue>`, `ui.ember.component.render`/`definition`/`init` | `function`, `ui.task`, `ui.render`/`ui.resolve`/`ui.mount` |
905+
| Browser paint entries | `paint` | `browser.paint` |
906906

907907
**Databases, cache & messaging:**
908908

@@ -1020,6 +1020,7 @@ The following span names were adjusted:
10201020
| `function` | Integration-specific, sometimes the segment span's name | `serverAction/updateUser`, `LOADER routes/users.$id` | The span's `code.function.name`. The previous name is kept as the span description | `updateUser`, `loader` |
10211021
| `function` (Angular `TraceMethod`) | The decorator's `name` option in angle brackets | `<getUser>`, `<unnamed>` | The decorator's `name` option, or `Function execution` if it has none | `Login.ngOnInit`, `getUsers`, `Function execution` |
10221022
| `function` (SvelteKit) | The route the wrapped function ran for, or the raw URL path if the SDK couldn't resolve one | `/users/[id]`, `/users/123`, `GET /api/users/[id]` | The name of the wrapped function | `load`, `GET` |
1023+
| `function` (Ember route hooks) | The full route name | `slow-loading-route.index` | The hook the span wraps, matching its `code.function.name`. The route moves to `sentry.description` | `beforeModel`, `model`, `setupController` |
10231024
| `function.gcp` | The request method and path for HTTP functions, otherwise the trigger's event or trigger type | `POST /users`, `google.pubsub.topic.publish`, `firebase.function.http.request` | The function name, or `Serverless function execution` if the SDK cannot resolve one | `myFunction`, `Serverless function execution` |
10241025
| `function.aws` | The Lambda function name | `my-function` | Unchanged, except that the SDK now falls back to `Serverless function execution` if it cannot resolve the function name | `my-function`, `Serverless function execution` |
10251026
| `graphql` | The graphql phase and, for operations, the operation name | `query GetUser`, `graphql.parse`, `graphql.resolve user.0.name` | The operation type, or the processing type where there is none | `GraphQL query`, `GraphQL parse`, `GraphQL resolve` |

‎dev-packages/e2e-tests/test-applications/ember-classic/app/instance-initializers/sentry-performance.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ export function initialize(appInstance: ApplicationInstance): void {
55
instrumentAppInstancePerformance(appInstance, {
66
minimumRunloopQueueDuration: 0,
77
minimumComponentRenderDuration: 0,
8+
// Off by default, enabled here so the suite covers `ui.resolve` spans.
9+
enableComponentDefinitions: true,
810
});
911
}
1012

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

Lines changed: 41 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -178,17 +178,19 @@ test('captures correct spans for navigation', async ({ page }) => {
178178
expect(beforeModelSpans).toEqual(
179179
expect.arrayContaining([
180180
expect.objectContaining({
181-
name: 'slow-loading-route',
181+
name: 'beforeModel',
182182
attributes: expect.objectContaining({
183183
'code.function.name': { type: 'string', value: 'beforeModel' },
184+
'sentry.description': { type: 'string', value: 'slow-loading-route' },
184185
'sentry.op': { type: 'string', value: 'function' },
185186
'sentry.origin': { type: 'string', value: 'auto.ui.ember' },
186187
}),
187188
}),
188189
expect.objectContaining({
189-
name: 'slow-loading-route.index',
190+
name: 'beforeModel',
190191
attributes: expect.objectContaining({
191192
'code.function.name': { type: 'string', value: 'beforeModel' },
193+
'sentry.description': { type: 'string', value: 'slow-loading-route.index' },
192194
'sentry.op': { type: 'string', value: 'function' },
193195
'sentry.origin': { type: 'string', value: 'auto.ui.ember' },
194196
}),
@@ -199,15 +201,17 @@ test('captures correct spans for navigation', async ({ page }) => {
199201
expect(modelSpans).toEqual(
200202
expect.arrayContaining([
201203
expect.objectContaining({
202-
name: 'slow-loading-route',
204+
name: 'model',
203205
attributes: expect.objectContaining({
204206
'code.function.name': { type: 'string', value: 'model' },
207+
'sentry.description': { type: 'string', value: 'slow-loading-route' },
205208
}),
206209
}),
207210
expect.objectContaining({
208-
name: 'slow-loading-route.index',
211+
name: 'model',
209212
attributes: expect.objectContaining({
210213
'code.function.name': { type: 'string', value: 'model' },
214+
'sentry.description': { type: 'string', value: 'slow-loading-route.index' },
211215
}),
212216
}),
213217
]),
@@ -216,15 +220,17 @@ test('captures correct spans for navigation', async ({ page }) => {
216220
expect(afterModelSpans).toEqual(
217221
expect.arrayContaining([
218222
expect.objectContaining({
219-
name: 'slow-loading-route',
223+
name: 'afterModel',
220224
attributes: expect.objectContaining({
221225
'code.function.name': { type: 'string', value: 'afterModel' },
226+
'sentry.description': { type: 'string', value: 'slow-loading-route' },
222227
}),
223228
}),
224229
expect.objectContaining({
225-
name: 'slow-loading-route.index',
230+
name: 'afterModel',
226231
attributes: expect.objectContaining({
227232
'code.function.name': { type: 'string', value: 'afterModel' },
233+
'sentry.description': { type: 'string', value: 'slow-loading-route.index' },
228234
}),
229235
}),
230236
]),
@@ -243,3 +249,32 @@ test('captures correct spans for navigation', async ({ page }) => {
243249
]),
244250
);
245251
});
252+
253+
test('captures a `ui.resolve` span alongside the `ui.render` span for a component', async ({ page }) => {
254+
const spansPromise = collectStreamedSpans('ember-classic', spans => {
255+
return spans.some(span => getSpanOp(span) === 'ui.resolve') && spans.some(span => getSpanOp(span) === 'ui.render');
256+
});
257+
258+
await page.goto(`/tracing`);
259+
260+
const spans = await spansPromise;
261+
262+
const resolveSpan = spans.find(span => getSpanOp(span) === 'ui.resolve')!;
263+
const renderSpan = spans.find(span => getSpanOp(span) === 'ui.render')!;
264+
265+
expect(resolveSpan).toMatchObject({
266+
attributes: expect.objectContaining({
267+
'sentry.op': { type: 'string', value: 'ui.resolve' },
268+
'sentry.origin': { type: 'string', value: 'auto.ui.ember' },
269+
'ui.component_name': { type: 'string', value: resolveSpan.name },
270+
}),
271+
});
272+
273+
expect(renderSpan).toMatchObject({
274+
attributes: expect.objectContaining({
275+
'sentry.op': { type: 'string', value: 'ui.render' },
276+
'sentry.origin': { type: 'string', value: 'auto.ui.ember' },
277+
'ui.component_name': { type: 'string', value: renderSpan.name },
278+
}),
279+
});
280+
});

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

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -178,17 +178,19 @@ test('captures correct spans for navigation', async ({ page }) => {
178178
expect(beforeModelSpans).toEqual(
179179
expect.arrayContaining([
180180
expect.objectContaining({
181-
name: 'slow-loading-route',
181+
name: 'beforeModel',
182182
attributes: expect.objectContaining({
183183
'code.function.name': { type: 'string', value: 'beforeModel' },
184+
'sentry.description': { type: 'string', value: 'slow-loading-route' },
184185
'sentry.op': { type: 'string', value: 'function' },
185186
'sentry.origin': { type: 'string', value: 'auto.ui.ember' },
186187
}),
187188
}),
188189
expect.objectContaining({
189-
name: 'slow-loading-route.index',
190+
name: 'beforeModel',
190191
attributes: expect.objectContaining({
191192
'code.function.name': { type: 'string', value: 'beforeModel' },
193+
'sentry.description': { type: 'string', value: 'slow-loading-route.index' },
192194
'sentry.op': { type: 'string', value: 'function' },
193195
'sentry.origin': { type: 'string', value: 'auto.ui.ember' },
194196
}),
@@ -199,15 +201,17 @@ test('captures correct spans for navigation', async ({ page }) => {
199201
expect(modelSpans).toEqual(
200202
expect.arrayContaining([
201203
expect.objectContaining({
202-
name: 'slow-loading-route',
204+
name: 'model',
203205
attributes: expect.objectContaining({
204206
'code.function.name': { type: 'string', value: 'model' },
207+
'sentry.description': { type: 'string', value: 'slow-loading-route' },
205208
}),
206209
}),
207210
expect.objectContaining({
208-
name: 'slow-loading-route.index',
211+
name: 'model',
209212
attributes: expect.objectContaining({
210213
'code.function.name': { type: 'string', value: 'model' },
214+
'sentry.description': { type: 'string', value: 'slow-loading-route.index' },
211215
}),
212216
}),
213217
]),
@@ -216,15 +220,17 @@ test('captures correct spans for navigation', async ({ page }) => {
216220
expect(afterModelSpans).toEqual(
217221
expect.arrayContaining([
218222
expect.objectContaining({
219-
name: 'slow-loading-route',
223+
name: 'afterModel',
220224
attributes: expect.objectContaining({
221225
'code.function.name': { type: 'string', value: 'afterModel' },
226+
'sentry.description': { type: 'string', value: 'slow-loading-route' },
222227
}),
223228
}),
224229
expect.objectContaining({
225-
name: 'slow-loading-route.index',
230+
name: 'afterModel',
226231
attributes: expect.objectContaining({
227232
'code.function.name': { type: 'string', value: 'afterModel' },
233+
'sentry.description': { type: 'string', value: 'slow-loading-route.index' },
228234
}),
229235
}),
230236
]),

‎dev-packages/e2e-tests/test-applications/ember-strict-resolver/tests/streamed-performance.test.ts‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,23 @@ test('names the transition span with the low cardinality fallback', async ({ pag
1717
expect(transitionSpan.name).toBe('Router');
1818
expect(transitionSpan.attributes['sentry.origin']).toEqual({ type: 'string', value: 'auto.ui.ember' });
1919
});
20+
21+
test('names route hook spans after the hook and keeps the route as the description', async ({ page }) => {
22+
const modelSpanPromise = waitForStreamedSpan(
23+
'ember-strict-resolver',
24+
span =>
25+
getSpanOp(span) === 'function' &&
26+
span.attributes['code.function.name']?.value === 'model' &&
27+
span.attributes['sentry.description']?.value === 'slow-loading-route.index',
28+
);
29+
30+
await page.goto('/tracing');
31+
await page.getByText('Transition to slow loading route').click();
32+
33+
const modelSpan = await modelSpanPromise;
34+
35+
expect(modelSpan.name).toBe('model');
36+
expect(modelSpan.attributes['code.function.name']).toEqual({ type: 'string', value: 'model' });
37+
expect(modelSpan.attributes['sentry.description']).toEqual({ type: 'string', value: 'slow-loading-route.index' });
38+
expect(modelSpan.attributes['sentry.origin']).toEqual({ type: 'string', value: 'auto.ui.ember' });
39+
});

‎packages/ember/src/utils/instrumentEmberGlobals.ts‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { subscribe } from '@ember/instrumentation';
22
import { scheduleOnce } from '@ember/runloop';
33
import { SENTRY_OP, UI_COMPONENT_NAME } from '@sentry/conventions/attributes';
4-
import { UI_MOUNT, UI_RENDER, UI_TASK, FUNCTION } from '@sentry/conventions/op';
4+
import { UI_MOUNT, UI_RENDER, UI_TASK } from '@sentry/conventions/op';
55
import { getActiveSpan, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, startInactiveSpan } from '@sentry/browser';
66
import type { Span } from '@sentry/core';
77
import { browserPerformanceTimeOrigin, timestampInSeconds } from '@sentry/core';
@@ -193,7 +193,9 @@ function _instrumentComponents(config: {
193193
},
194194

195195
after(_name: string, _timestamp: number, payload: object) {
196-
_processComponentRenderAfter(payload as Payload, beforeComponentDefinitionEntries, FUNCTION, 0);
196+
// TODO: Use the `UI_RESOLVE` const from `@sentry/conventions/op` once the op is released.
197+
// See https://github.com/getsentry/sentry-conventions/pull/633
198+
_processComponentRenderAfter(payload as Payload, beforeComponentDefinitionEntries, 'ui.resolve', 0);
197199
},
198200
});
199201
}

‎packages/ember/src/utils/instrumentRoutePerformance.ts‎

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { startSpan } from '@sentry/browser';
2-
import { CODE_FUNCTION_NAME, SENTRY_OP } from '@sentry/conventions/attributes';
2+
import { CODE_FUNCTION_NAME, SENTRY_DESCRIPTION, SENTRY_OP } from '@sentry/conventions/attributes';
33
import { FUNCTION } from '@sentry/conventions/op';
4-
import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core';
4+
import { getClient, hasSpanStreamingEnabled, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core';
55

66
import type Route from '@ember/routing/route';
77

@@ -33,19 +33,22 @@ type RouteConstructor = new (...args: ConstructorParameters<typeof Route>) => Ro
3333
export function instrumentRoutePerformance<T extends RouteConstructor>(BaseRoute: T): T {
3434
const instrumentFunction = async (
3535
hookName: string,
36-
name: string,
36+
fullRouteName: string,
3737
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Route hooks have varied signatures that can't be unified with unknown
3838
fn: (...args: any[]) => any,
3939
args: unknown[],
4040
): Promise<unknown> => {
41+
const client = getClient();
42+
const isStreaming = !!client && hasSpanStreamingEnabled(client);
4143
return startSpan(
4244
{
4345
attributes: {
4446
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.ember',
4547
[SENTRY_OP]: FUNCTION,
4648
[CODE_FUNCTION_NAME]: hookName,
49+
...(isStreaming && { [SENTRY_DESCRIPTION]: fullRouteName }),
4750
},
48-
name,
51+
name: isStreaming ? hookName : fullRouteName,
4952
onlyIfParent: true,
5053
},
5154
() => {

0 commit comments

Comments
 (0)