From 71a5e2027d262d4341d33f4970fc1fda935cff27 Mon Sep 17 00:00:00 2001 From: Martin Sonnberger Date: Mon, 27 Jul 2026 15:42:52 +0200 Subject: [PATCH 1/6] feat(ember)!: Use `function` span op for route hooks Route hooks are functions, not their own op category. The hook name moves to `code.function.name` and `ember.route.hook`. Ref: JS-3105 Co-Authored-By: Claude Opus 5 (1M context) --- packages/ember/addon/index.ts | 28 +++++++------------ .../acceptance/sentry-performance-test.ts | 28 +++++++++---------- packages/ember/tests/helpers/utils.ts | 4 ++- 3 files changed, 27 insertions(+), 33 deletions(-) diff --git a/packages/ember/addon/index.ts b/packages/ember/addon/index.ts index c6da31c431f0..2a0cc9fc54ec 100644 --- a/packages/ember/addon/index.ts +++ b/packages/ember/addon/index.ts @@ -4,6 +4,8 @@ import { assert } from '@ember/debug'; import type Route from '@ember/routing/route'; import { getOwnConfig } from '@embroider/macros'; +import { CODE_FUNCTION_NAME } from '@sentry/conventions/attributes'; +import { GENERAL_FUNCTION_SPAN_OP } from '@sentry/conventions/op'; import type { BrowserOptions } from '@sentry/browser'; import { startSpan } from '@sentry/browser'; import * as Sentry from '@sentry/browser'; @@ -54,7 +56,7 @@ type RouteConstructor = new (...args: ConstructorParameters) => Ro export const instrumentRoutePerformance = (BaseRoute: T): T => { // eslint-disable-next-line @typescript-eslint/no-explicit-any const instrumentFunction = async any>( - op: string, + hookName: string, name: string, fn: X, args: Parameters, @@ -65,8 +67,10 @@ export const instrumentRoutePerformance = (BaseRoute attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: source, [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.ember', + [CODE_FUNCTION_NAME]: hookName, + 'ember.route.hook': hookName, }, - op, + op: GENERAL_FUNCTION_SPAN_OP, name, onlyIfParent: true, }, @@ -82,32 +86,20 @@ export const instrumentRoutePerformance = (BaseRoute // @ts-expect-error TS2545 We do not need to redefine a constructor here [routeName]: class extends BaseRoute { public beforeModel(...args: unknown[]): void | Promise { - return instrumentFunction( - 'ui.ember.route.before_model', - this.fullRouteName, - super.beforeModel.bind(this), - args, - 'custom', - ); + return instrumentFunction('beforeModel', this.fullRouteName, super.beforeModel.bind(this), args, 'custom'); } public async model(...args: unknown[]): Promise { - return instrumentFunction('ui.ember.route.model', this.fullRouteName, super.model.bind(this), args, 'custom'); + return instrumentFunction('model', this.fullRouteName, super.model.bind(this), args, 'custom'); } public afterModel(...args: unknown[]): void | Promise { - return instrumentFunction( - 'ui.ember.route.after_model', - this.fullRouteName, - super.afterModel.bind(this), - args, - 'custom', - ); + return instrumentFunction('afterModel', this.fullRouteName, super.afterModel.bind(this), args, 'custom'); } public setupController(...args: unknown[]): void | Promise { return instrumentFunction( - 'ui.ember.route.setup_controller', + 'setupController', this.fullRouteName, super.setupController.bind(this), args, diff --git a/packages/ember/tests/acceptance/sentry-performance-test.ts b/packages/ember/tests/acceptance/sentry-performance-test.ts index 424f4663484d..0407fd9bd618 100644 --- a/packages/ember/tests/acceptance/sentry-performance-test.ts +++ b/packages/ember/tests/acceptance/sentry-performance-test.ts @@ -36,14 +36,14 @@ module('Acceptance | Sentry Performance', function (hooks) { assertSentryTransactions(assert, 1, { spans: [ 'ui.ember.transition | route:tracing -> route:slow-loading-route.index', - 'ui.ember.route.before_model | slow-loading-route', - 'ui.ember.route.model | slow-loading-route', - 'ui.ember.route.after_model | slow-loading-route', - 'ui.ember.route.before_model | slow-loading-route.index', - 'ui.ember.route.model | slow-loading-route.index', - 'ui.ember.route.after_model | slow-loading-route.index', - 'ui.ember.route.setup_controller | slow-loading-route', - 'ui.ember.route.setup_controller | slow-loading-route.index', + 'function:beforeModel | slow-loading-route', + 'function:model | slow-loading-route', + 'function:afterModel | slow-loading-route', + 'function:beforeModel | slow-loading-route.index', + 'function:model | slow-loading-route.index', + 'function:afterModel | slow-loading-route.index', + 'function:setupController | slow-loading-route', + 'function:setupController | slow-loading-route.index', 'ui.ember.component.render | component:slow-loading-list', 'ui.ember.component.render | component:slow-loading-list', ], @@ -63,10 +63,10 @@ module('Acceptance | Sentry Performance', function (hooks) { assertSentryTransactions(assert, 0, { spans: [ 'ui.ember.transition | route:undefined -> route:with-loading.index', - 'ui.ember.route.before_model | with-loading.index', - 'ui.ember.route.model | with-loading.index', - 'ui.ember.route.after_model | with-loading.index', - 'ui.ember.route.setup_controller | with-loading.index', + 'function:beforeModel | with-loading.index', + 'function:model | with-loading.index', + 'function:afterModel | with-loading.index', + 'function:setupController | with-loading.index', ], transaction: 'route:with-loading.index', attributes: { @@ -86,8 +86,8 @@ module('Acceptance | Sentry Performance', function (hooks) { assertSentryTransactions(assert, 0, { spans: [ 'ui.ember.transition | route:undefined -> route:with-error.index', - 'ui.ember.route.before_model | with-error.index', - 'ui.ember.route.model | with-error.index', + 'function:beforeModel | with-error.index', + 'function:model | with-error.index', ], transaction: 'route:with-error.index', attributes: { diff --git a/packages/ember/tests/helpers/utils.ts b/packages/ember/tests/helpers/utils.ts index bce62a85dea1..f90c07f60c9e 100644 --- a/packages/ember/tests/helpers/utils.ts +++ b/packages/ember/tests/helpers/utils.ts @@ -75,7 +75,9 @@ export function assertSentryTransactions( ); }) .map(spanJson => { - return `${spanJson.op} | ${spanJson.description}`; + // Route hooks all share the `function` op, so the hook name is what distinguishes them + const hook = spanJson.data?.['ember.route.hook']; + return hook ? `${spanJson.op}:${hook} | ${spanJson.description}` : `${spanJson.op} | ${spanJson.description}`; }); assert.true( From 019a544f4e54ee48ce06c47d6a949423ae85ce7f Mon Sep 17 00:00:00 2001 From: Martin Sonnberger Date: Mon, 27 Jul 2026 15:42:52 +0200 Subject: [PATCH 2/6] test(e2e): Update ember route hook span assertions Match the `function` op and the new `code.function.name` / `ember.route.hook` attributes. Span finders discriminate on the hook name, since all hooks now share one op. Co-Authored-By: Claude Opus 5 (1M context) --- .../ember-classic/tests/performance.test.ts | 46 +++++++++++++------ .../ember-embroider/tests/performance.test.ts | 46 +++++++++++++------ 2 files changed, 62 insertions(+), 30 deletions(-) diff --git a/dev-packages/e2e-tests/test-applications/ember-classic/tests/performance.test.ts b/dev-packages/e2e-tests/test-applications/ember-classic/tests/performance.test.ts index 11006836751b..73edd45437e9 100644 --- a/dev-packages/e2e-tests/test-applications/ember-classic/tests/performance.test.ts +++ b/dev-packages/e2e-tests/test-applications/ember-classic/tests/performance.test.ts @@ -168,9 +168,13 @@ test('captures correct spans for navigation', async ({ page }) => { }); const transitionSpans = spans.filter(span => span.op === 'ui.ember.transition'); - const beforeModelSpans = spans.filter(span => span.op === 'ui.ember.route.before_model'); - const modelSpans = spans.filter(span => span.op === 'ui.ember.route.model'); - const afterModelSpans = spans.filter(span => span.op === 'ui.ember.route.after_model'); + const beforeModelSpans = spans.filter( + span => span.op === 'function' && span.data?.['ember.route.hook'] === 'beforeModel', + ); + const modelSpans = spans.filter(span => span.op === 'function' && span.data?.['ember.route.hook'] === 'model'); + const afterModelSpans = spans.filter( + span => span.op === 'function' && span.data?.['ember.route.hook'] === 'afterModel', + ); const renderSpans = spans.filter(span => span.op === 'ui.ember.runloop.render'); expect(transitionSpans).toHaveLength(1); @@ -202,12 +206,14 @@ test('captures correct spans for navigation', async ({ page }) => { expect(beforeModelSpans).toEqual([ { data: { - 'sentry.op': 'ui.ember.route.before_model', + 'code.function.name': 'beforeModel', + 'ember.route.hook': 'beforeModel', + 'sentry.op': 'function', 'sentry.origin': 'auto.ui.ember', 'sentry.source': 'custom', }, description: 'slow-loading-route', - op: 'ui.ember.route.before_model', + op: 'function', origin: 'auto.ui.ember', status: 'ok', parent_span_id: spanId, @@ -218,12 +224,14 @@ test('captures correct spans for navigation', async ({ page }) => { }, { data: { - 'sentry.op': 'ui.ember.route.before_model', + 'code.function.name': 'beforeModel', + 'ember.route.hook': 'beforeModel', + 'sentry.op': 'function', 'sentry.origin': 'auto.ui.ember', 'sentry.source': 'custom', }, description: 'slow-loading-route.index', - op: 'ui.ember.route.before_model', + op: 'function', origin: 'auto.ui.ember', status: 'ok', parent_span_id: spanId, @@ -237,12 +245,14 @@ test('captures correct spans for navigation', async ({ page }) => { expect(modelSpans).toEqual([ { data: { - 'sentry.op': 'ui.ember.route.model', + 'code.function.name': 'model', + 'ember.route.hook': 'model', + 'sentry.op': 'function', 'sentry.origin': 'auto.ui.ember', 'sentry.source': 'custom', }, description: 'slow-loading-route', - op: 'ui.ember.route.model', + op: 'function', origin: 'auto.ui.ember', status: 'ok', parent_span_id: spanId, @@ -253,12 +263,14 @@ test('captures correct spans for navigation', async ({ page }) => { }, { data: { - 'sentry.op': 'ui.ember.route.model', + 'code.function.name': 'model', + 'ember.route.hook': 'model', + 'sentry.op': 'function', 'sentry.origin': 'auto.ui.ember', 'sentry.source': 'custom', }, description: 'slow-loading-route.index', - op: 'ui.ember.route.model', + op: 'function', origin: 'auto.ui.ember', status: 'ok', parent_span_id: spanId, @@ -272,12 +284,14 @@ test('captures correct spans for navigation', async ({ page }) => { expect(afterModelSpans).toEqual([ { data: { - 'sentry.op': 'ui.ember.route.after_model', + 'code.function.name': 'afterModel', + 'ember.route.hook': 'afterModel', + 'sentry.op': 'function', 'sentry.origin': 'auto.ui.ember', 'sentry.source': 'custom', }, description: 'slow-loading-route', - op: 'ui.ember.route.after_model', + op: 'function', origin: 'auto.ui.ember', status: 'ok', parent_span_id: spanId, @@ -288,12 +302,14 @@ test('captures correct spans for navigation', async ({ page }) => { }, { data: { - 'sentry.op': 'ui.ember.route.after_model', + 'code.function.name': 'afterModel', + 'ember.route.hook': 'afterModel', + 'sentry.op': 'function', 'sentry.origin': 'auto.ui.ember', 'sentry.source': 'custom', }, description: 'slow-loading-route.index', - op: 'ui.ember.route.after_model', + op: 'function', origin: 'auto.ui.ember', status: 'ok', parent_span_id: spanId, diff --git a/dev-packages/e2e-tests/test-applications/ember-embroider/tests/performance.test.ts b/dev-packages/e2e-tests/test-applications/ember-embroider/tests/performance.test.ts index dbdee99717f5..2ff8ca4d14aa 100644 --- a/dev-packages/e2e-tests/test-applications/ember-embroider/tests/performance.test.ts +++ b/dev-packages/e2e-tests/test-applications/ember-embroider/tests/performance.test.ts @@ -168,9 +168,13 @@ test('captures correct spans for navigation', async ({ page }) => { }); const transitionSpans = spans.filter(span => span.op === 'ui.ember.transition'); - const beforeModelSpans = spans.filter(span => span.op === 'ui.ember.route.before_model'); - const modelSpans = spans.filter(span => span.op === 'ui.ember.route.model'); - const afterModelSpans = spans.filter(span => span.op === 'ui.ember.route.after_model'); + const beforeModelSpans = spans.filter( + span => span.op === 'function' && span.data?.['ember.route.hook'] === 'beforeModel', + ); + const modelSpans = spans.filter(span => span.op === 'function' && span.data?.['ember.route.hook'] === 'model'); + const afterModelSpans = spans.filter( + span => span.op === 'function' && span.data?.['ember.route.hook'] === 'afterModel', + ); const renderSpans = spans.filter(span => span.op === 'ui.ember.runloop.render'); expect(transitionSpans).toHaveLength(1); @@ -202,12 +206,14 @@ test('captures correct spans for navigation', async ({ page }) => { expect(beforeModelSpans).toEqual([ { data: { - 'sentry.op': 'ui.ember.route.before_model', + 'code.function.name': 'beforeModel', + 'ember.route.hook': 'beforeModel', + 'sentry.op': 'function', 'sentry.origin': 'auto.ui.ember', 'sentry.source': 'custom', }, description: 'slow-loading-route', - op: 'ui.ember.route.before_model', + op: 'function', origin: 'auto.ui.ember', status: 'ok', parent_span_id: spanId, @@ -218,12 +224,14 @@ test('captures correct spans for navigation', async ({ page }) => { }, { data: { - 'sentry.op': 'ui.ember.route.before_model', + 'code.function.name': 'beforeModel', + 'ember.route.hook': 'beforeModel', + 'sentry.op': 'function', 'sentry.origin': 'auto.ui.ember', 'sentry.source': 'custom', }, description: 'slow-loading-route.index', - op: 'ui.ember.route.before_model', + op: 'function', origin: 'auto.ui.ember', status: 'ok', parent_span_id: spanId, @@ -237,12 +245,14 @@ test('captures correct spans for navigation', async ({ page }) => { expect(modelSpans).toEqual([ { data: { - 'sentry.op': 'ui.ember.route.model', + 'code.function.name': 'model', + 'ember.route.hook': 'model', + 'sentry.op': 'function', 'sentry.origin': 'auto.ui.ember', 'sentry.source': 'custom', }, description: 'slow-loading-route', - op: 'ui.ember.route.model', + op: 'function', origin: 'auto.ui.ember', status: 'ok', parent_span_id: spanId, @@ -253,12 +263,14 @@ test('captures correct spans for navigation', async ({ page }) => { }, { data: { - 'sentry.op': 'ui.ember.route.model', + 'code.function.name': 'model', + 'ember.route.hook': 'model', + 'sentry.op': 'function', 'sentry.origin': 'auto.ui.ember', 'sentry.source': 'custom', }, description: 'slow-loading-route.index', - op: 'ui.ember.route.model', + op: 'function', origin: 'auto.ui.ember', status: 'ok', parent_span_id: spanId, @@ -272,12 +284,14 @@ test('captures correct spans for navigation', async ({ page }) => { expect(afterModelSpans).toEqual([ { data: { - 'sentry.op': 'ui.ember.route.after_model', + 'code.function.name': 'afterModel', + 'ember.route.hook': 'afterModel', + 'sentry.op': 'function', 'sentry.origin': 'auto.ui.ember', 'sentry.source': 'custom', }, description: 'slow-loading-route', - op: 'ui.ember.route.after_model', + op: 'function', origin: 'auto.ui.ember', status: 'ok', parent_span_id: spanId, @@ -288,12 +302,14 @@ test('captures correct spans for navigation', async ({ page }) => { }, { data: { - 'sentry.op': 'ui.ember.route.after_model', + 'code.function.name': 'afterModel', + 'ember.route.hook': 'afterModel', + 'sentry.op': 'function', 'sentry.origin': 'auto.ui.ember', 'sentry.source': 'custom', }, description: 'slow-loading-route.index', - op: 'ui.ember.route.after_model', + op: 'function', origin: 'auto.ui.ember', status: 'ok', parent_span_id: spanId, From 9dcb64d1780127b7996651ad07f18f950c72995c Mon Sep 17 00:00:00 2001 From: Martin Sonnberger Date: Mon, 27 Jul 2026 15:42:53 +0200 Subject: [PATCH 3/6] feat(ember)!: Use `ui.task` span op for runloop queues Queue values are phase detail, and Ember allows custom queues. The queue moves to the `ember.runloop.queue` attribute. Ref: JS-3105 Co-Authored-By: Claude Opus 5 (1M context) --- .../ember-classic/tests/performance.test.ts | 7 ++++--- .../ember-embroider/tests/performance.test.ts | 7 ++++--- .../ember/addon/utils/instrumentEmberGlobals.ts | 4 +++- packages/ember/tests/helpers/utils.ts | 16 +++++++--------- 4 files changed, 18 insertions(+), 16 deletions(-) diff --git a/dev-packages/e2e-tests/test-applications/ember-classic/tests/performance.test.ts b/dev-packages/e2e-tests/test-applications/ember-classic/tests/performance.test.ts index 73edd45437e9..ef86faed21b8 100644 --- a/dev-packages/e2e-tests/test-applications/ember-classic/tests/performance.test.ts +++ b/dev-packages/e2e-tests/test-applications/ember-classic/tests/performance.test.ts @@ -175,7 +175,7 @@ test('captures correct spans for navigation', async ({ page }) => { const afterModelSpans = spans.filter( span => span.op === 'function' && span.data?.['ember.route.hook'] === 'afterModel', ); - const renderSpans = spans.filter(span => span.op === 'ui.ember.runloop.render'); + const renderSpans = spans.filter(span => span.op === 'ui.task' && span.data?.['ember.runloop.queue'] === 'render'); expect(transitionSpans).toHaveLength(1); @@ -322,11 +322,12 @@ test('captures correct spans for navigation', async ({ page }) => { expect(renderSpans).toContainEqual({ data: { - 'sentry.op': 'ui.ember.runloop.render', + 'ember.runloop.queue': 'render', + 'sentry.op': 'ui.task', 'sentry.origin': 'auto.ui.ember', }, description: 'runloop', - op: 'ui.ember.runloop.render', + op: 'ui.task', origin: 'auto.ui.ember', status: 'ok', parent_span_id: spanId, diff --git a/dev-packages/e2e-tests/test-applications/ember-embroider/tests/performance.test.ts b/dev-packages/e2e-tests/test-applications/ember-embroider/tests/performance.test.ts index 2ff8ca4d14aa..cbac7d30e8c2 100644 --- a/dev-packages/e2e-tests/test-applications/ember-embroider/tests/performance.test.ts +++ b/dev-packages/e2e-tests/test-applications/ember-embroider/tests/performance.test.ts @@ -175,7 +175,7 @@ test('captures correct spans for navigation', async ({ page }) => { const afterModelSpans = spans.filter( span => span.op === 'function' && span.data?.['ember.route.hook'] === 'afterModel', ); - const renderSpans = spans.filter(span => span.op === 'ui.ember.runloop.render'); + const renderSpans = spans.filter(span => span.op === 'ui.task' && span.data?.['ember.runloop.queue'] === 'render'); expect(transitionSpans).toHaveLength(1); @@ -322,11 +322,12 @@ test('captures correct spans for navigation', async ({ page }) => { expect(renderSpans).toContainEqual({ data: { - 'sentry.op': 'ui.ember.runloop.render', + 'ember.runloop.queue': 'render', + 'sentry.op': 'ui.task', 'sentry.origin': 'auto.ui.ember', }, description: 'runloop', - op: 'ui.ember.runloop.render', + op: 'ui.task', origin: 'auto.ui.ember', status: 'ok', parent_span_id: spanId, diff --git a/packages/ember/addon/utils/instrumentEmberGlobals.ts b/packages/ember/addon/utils/instrumentEmberGlobals.ts index 02e365e27c23..3627376ed4ea 100644 --- a/packages/ember/addon/utils/instrumentEmberGlobals.ts +++ b/packages/ember/addon/utils/instrumentEmberGlobals.ts @@ -1,6 +1,7 @@ import { subscribe } from '@ember/instrumentation'; import { scheduleOnce } from '@ember/runloop'; import type { EmberRunQueues } from '@ember/runloop/-private/types'; +import { BROWSER_UI_TASK_SPAN_OP } from '@sentry/conventions/op'; import { getActiveSpan, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, startInactiveSpan } from '@sentry/browser'; import type { Span } from '@sentry/core'; import { browserPerformanceTimeOrigin, timestampInSeconds } from '@sentry/core'; @@ -90,9 +91,10 @@ function _instrumentEmberRunloop(config: { minimumRunloopQueueDuration?: number startInactiveSpan({ attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.ember', + 'ember.runloop.queue': queue, }, name: 'runloop', - op: `ui.ember.runloop.${queue}`, + op: BROWSER_UI_TASK_SPAN_OP, startTime: currentQueueStart, onlyIfParent: true, })?.end(now); diff --git a/packages/ember/tests/helpers/utils.ts b/packages/ember/tests/helpers/utils.ts index f90c07f60c9e..b2f3a891111d 100644 --- a/packages/ember/tests/helpers/utils.ts +++ b/packages/ember/tests/helpers/utils.ts @@ -44,6 +44,11 @@ export function assertSentryErrors( }); } +// Runloop spans share the generic `ui.task` op, so the queue attribute is what identifies them +function isRunloopSpan(span: NonNullable[number]): boolean { + return span.op === 'ui.task' && span.data?.['ember.runloop.queue'] !== undefined; +} + export function assertSentryTransactions( assert: Assert, callNumber: number, @@ -68,11 +73,7 @@ export function assertSentryTransactions( const filteredSpans = spans .filter(span => { const op = span.op; - return ( - !op?.startsWith('ui.ember.runloop.') && - !op?.startsWith('ui.long-task') && - !op?.startsWith('ui.long-animation-frame') - ); + return !isRunloopSpan(span) && !op?.startsWith('ui.long-task') && !op?.startsWith('ui.long-animation-frame'); }) .map(spanJson => { // Route hooks all share the `function` op, so the hook name is what distinguishes them @@ -80,10 +81,7 @@ export function assertSentryTransactions( return hook ? `${spanJson.op}:${hook} | ${spanJson.description}` : `${spanJson.op} | ${spanJson.description}`; }); - assert.true( - spans.some(span => span.op?.startsWith('ui.ember.runloop.')), - 'it captures runloop spans', - ); + assert.true(spans.some(isRunloopSpan), 'it captures runloop spans'); assert.deepEqual(filteredSpans, options.spans, 'Has correct spans'); assert.equal(event.transaction, options.transaction); From 8960d79b146c202f5d808aa17b0344e055897b74 Mon Sep 17 00:00:00 2001 From: Martin Sonnberger Date: Mon, 27 Jul 2026 15:42:53 +0200 Subject: [PATCH 4/6] feat(ember)!: Use cross-framework span ops for component and init spans Component renders use `ui.render`, component definition lookups use `function`, and the initial load uses `ui.mount`. Both component spans set `ui.component_name`. Ref: JS-3105 Co-Authored-By: Claude Opus 5 (1M context) --- .../ember/addon/utils/instrumentEmberGlobals.ts | 15 ++++++++++----- .../tests/acceptance/sentry-performance-test.ts | 9 +++------ 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/packages/ember/addon/utils/instrumentEmberGlobals.ts b/packages/ember/addon/utils/instrumentEmberGlobals.ts index 3627376ed4ea..fd944d42c7ee 100644 --- a/packages/ember/addon/utils/instrumentEmberGlobals.ts +++ b/packages/ember/addon/utils/instrumentEmberGlobals.ts @@ -1,7 +1,8 @@ import { subscribe } from '@ember/instrumentation'; import { scheduleOnce } from '@ember/runloop'; import type { EmberRunQueues } from '@ember/runloop/-private/types'; -import { BROWSER_UI_TASK_SPAN_OP } from '@sentry/conventions/op'; +import { UI_COMPONENT_NAME } from '@sentry/conventions/attributes'; +import { BROWSER_UI_RENDER_SPAN_OP, BROWSER_UI_TASK_SPAN_OP, GENERAL_FUNCTION_SPAN_OP } from '@sentry/conventions/op'; import { getActiveSpan, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, startInactiveSpan } from '@sentry/browser'; import type { Span } from '@sentry/core'; import { browserPerformanceTimeOrigin, timestampInSeconds } from '@sentry/core'; @@ -149,13 +150,16 @@ function processComponentRenderAfter( const now = timestampInSeconds(); const componentRenderDuration = now - begin.now; + const name = payload.containerKey || payload.object; + if (componentRenderDuration * 1000 >= minComponentDuration) { startInactiveSpan({ - name: payload.containerKey || payload.object, + name, op, startTime: begin.now, attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.ember', + [UI_COMPONENT_NAME]: name, }, onlyIfParent: true, })?.end(now); @@ -180,7 +184,7 @@ function _instrumentComponents(config: { }, after(_name: string, _timestamp: number, payload: Payload, _beganIndex: number) { - processComponentRenderAfter(payload, beforeEntries, 'ui.ember.component.render', minComponentDuration); + processComponentRenderAfter(payload, beforeEntries, BROWSER_UI_RENDER_SPAN_OP, minComponentDuration); }, }); if (enableComponentDefinitions) { @@ -190,7 +194,7 @@ function _instrumentComponents(config: { }, after(_name: string, _timestamp: number, payload: Payload, _beganIndex: number) { - processComponentRenderAfter(payload, beforeComponentDefinitionEntries, 'ui.ember.component.definition', 0); + processComponentRenderAfter(payload, beforeComponentDefinitionEntries, GENERAL_FUNCTION_SPAN_OP, 0); }, }); } @@ -232,7 +236,8 @@ function _instrumentInitialLoad(): void { const endTime = startTime + measure.duration / 1000; startInactiveSpan({ - op: 'ui.ember.init', + // TODO(v11): Replace with the `ui.mount` constant from `@sentry/conventions/op` once it is registered there. + op: 'ui.mount', name: 'init', attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.ember', diff --git a/packages/ember/tests/acceptance/sentry-performance-test.ts b/packages/ember/tests/acceptance/sentry-performance-test.ts index 0407fd9bd618..9e8e19b87884 100644 --- a/packages/ember/tests/acceptance/sentry-performance-test.ts +++ b/packages/ember/tests/acceptance/sentry-performance-test.ts @@ -15,10 +15,7 @@ module('Acceptance | Sentry Performance', function (hooks) { assertSentryTransactionCount(assert, 1); assertSentryTransactions(assert, 0, { - spans: [ - 'ui.ember.transition | route:undefined -> route:tracing', - 'ui.ember.component.render | component:test-section', - ], + spans: ['ui.ember.transition | route:undefined -> route:tracing', 'ui.render | component:test-section'], transaction: 'route:tracing', attributes: { fromRoute: undefined, @@ -44,8 +41,8 @@ module('Acceptance | Sentry Performance', function (hooks) { 'function:afterModel | slow-loading-route.index', 'function:setupController | slow-loading-route', 'function:setupController | slow-loading-route.index', - 'ui.ember.component.render | component:slow-loading-list', - 'ui.ember.component.render | component:slow-loading-list', + 'ui.render | component:slow-loading-list', + 'ui.render | component:slow-loading-list', ], transaction: 'route:slow-loading-route.index', durationCheck: duration => duration > SLOW_TRANSITION_WAIT, From d9b3e6c2a5706370b6a8166358a7b48861294395 Mon Sep 17 00:00:00 2001 From: Martin Sonnberger Date: Tue, 28 Jul 2026 15:51:39 +0200 Subject: [PATCH 5/6] use sentry.op --- packages/ember/addon/utils/instrumentEmberGlobals.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/ember/addon/utils/instrumentEmberGlobals.ts b/packages/ember/addon/utils/instrumentEmberGlobals.ts index fd944d42c7ee..edcddbea93f6 100644 --- a/packages/ember/addon/utils/instrumentEmberGlobals.ts +++ b/packages/ember/addon/utils/instrumentEmberGlobals.ts @@ -1,7 +1,7 @@ import { subscribe } from '@ember/instrumentation'; import { scheduleOnce } from '@ember/runloop'; import type { EmberRunQueues } from '@ember/runloop/-private/types'; -import { UI_COMPONENT_NAME } from '@sentry/conventions/attributes'; +import { SENTRY_OP, UI_COMPONENT_NAME } from '@sentry/conventions/attributes'; import { BROWSER_UI_RENDER_SPAN_OP, BROWSER_UI_TASK_SPAN_OP, GENERAL_FUNCTION_SPAN_OP } from '@sentry/conventions/op'; import { getActiveSpan, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, startInactiveSpan } from '@sentry/browser'; import type { Span } from '@sentry/core'; @@ -155,9 +155,9 @@ function processComponentRenderAfter( if (componentRenderDuration * 1000 >= minComponentDuration) { startInactiveSpan({ name, - op, startTime: begin.now, attributes: { + [SENTRY_OP]: op, [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.ember', [UI_COMPONENT_NAME]: name, }, @@ -236,10 +236,10 @@ function _instrumentInitialLoad(): void { const endTime = startTime + measure.duration / 1000; startInactiveSpan({ - // TODO(v11): Replace with the `ui.mount` constant from `@sentry/conventions/op` once it is registered there. - op: 'ui.mount', name: 'init', attributes: { + // TODO(v11): Replace with the `ui.mount` constant from `@sentry/conventions/op` once it is registered there. + [SENTRY_OP]: 'ui.mount', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.ember', }, startTime, From 2ca3aa3d86e1e553272fd0fe94c218fd2699176f Mon Sep 17 00:00:00 2001 From: Martin Sonnberger Date: Tue, 28 Jul 2026 16:09:07 +0200 Subject: [PATCH 6/6] more sentry.op --- packages/ember/addon/index.ts | 4 ++-- packages/ember/addon/utils/instrumentEmberGlobals.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/ember/addon/index.ts b/packages/ember/addon/index.ts index 2a0cc9fc54ec..fedf69a0214d 100644 --- a/packages/ember/addon/index.ts +++ b/packages/ember/addon/index.ts @@ -4,7 +4,7 @@ import { assert } from '@ember/debug'; import type Route from '@ember/routing/route'; import { getOwnConfig } from '@embroider/macros'; -import { CODE_FUNCTION_NAME } from '@sentry/conventions/attributes'; +import { CODE_FUNCTION_NAME, SENTRY_OP } from '@sentry/conventions/attributes'; import { GENERAL_FUNCTION_SPAN_OP } from '@sentry/conventions/op'; import type { BrowserOptions } from '@sentry/browser'; import { startSpan } from '@sentry/browser'; @@ -67,10 +67,10 @@ export const instrumentRoutePerformance = (BaseRoute attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: source, [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.ember', + [SENTRY_OP]: GENERAL_FUNCTION_SPAN_OP, [CODE_FUNCTION_NAME]: hookName, 'ember.route.hook': hookName, }, - op: GENERAL_FUNCTION_SPAN_OP, name, onlyIfParent: true, }, diff --git a/packages/ember/addon/utils/instrumentEmberGlobals.ts b/packages/ember/addon/utils/instrumentEmberGlobals.ts index edcddbea93f6..291c18faf56b 100644 --- a/packages/ember/addon/utils/instrumentEmberGlobals.ts +++ b/packages/ember/addon/utils/instrumentEmberGlobals.ts @@ -91,11 +91,11 @@ function _instrumentEmberRunloop(config: { minimumRunloopQueueDuration?: number if ((now - currentQueueStart) * 1000 >= minQueueDuration) { startInactiveSpan({ attributes: { + [SENTRY_OP]: BROWSER_UI_TASK_SPAN_OP, [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.ember', 'ember.runloop.queue': queue, }, name: 'runloop', - op: BROWSER_UI_TASK_SPAN_OP, startTime: currentQueueStart, onlyIfParent: true, })?.end(now);