diff --git a/MIGRATION.md b/MIGRATION.md index 9140be4fe8ee..5bf8abb554ea 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -1001,6 +1001,7 @@ The following span names were adjusted: | `http.client`, `http.client.stream` | The request method and sanitized URL | `GET https://api.example.com/users/123` | The request method and the domain, or just the method if there is no domain | `GET api.example.com`, `GET` | | `router` | Framework-specific, sometimes containing the raw URL | `/users/123`, `SvelteKit Route Change` | The span's `http.route`, or `Router` if the SDK has none | `/users/:id`, `Router` | | `handler` | Framework-specific, often carrying the request method | `GET /users/:id`, `route-handler`, `getUser` | The span's `http.route`, or `Request handler` if the SDK has none | `/users/:id`, `Request handler` | +| `function` (Angular `TraceMethod`) | The decorator's `name` option in angle brackets | ``, `` | The decorator's `name` option, or `Function execution` if it has none | `Login.ngOnInit`, `getUsers`, `Function execution` | | `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` | | `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` | | `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` | diff --git a/dev-packages/e2e-tests/test-applications/angular-17/tests/performance.test.ts b/dev-packages/e2e-tests/test-applications/angular-17/tests/performance.test.ts index c346c41c6a25..adb440a59b57 100644 --- a/dev-packages/e2e-tests/test-applications/angular-17/tests/performance.test.ts +++ b/dev-packages/e2e-tests/test-applications/angular-17/tests/performance.test.ts @@ -356,11 +356,12 @@ test.describe('TraceMethod Decorator', () => { expect(ngInitSpan).toBeDefined(); expect(ngInitSpan).toEqual( expect.objectContaining({ - name: '', + name: 'ngOnInit', attributes: expect.objectContaining({ 'sentry.op': { type: 'string', value: 'function' }, 'sentry.origin': { type: 'string', value: 'auto.ui.angular.trace_method_decorator' }, 'code.function.name': { type: 'string', value: 'ngOnInit' }, + 'sentry.description': { type: 'string', value: '' }, }), }), ); @@ -393,10 +394,11 @@ test.describe('TraceMethod Decorator', () => { expect(ngAfterViewInitSpan).toBeDefined(); expect(ngAfterViewInitSpan).toEqual( expect.objectContaining({ - name: '', + name: 'Function execution', attributes: expect.objectContaining({ 'sentry.op': { type: 'string', value: 'function' }, 'sentry.origin': { type: 'string', value: 'auto.ui.angular.trace_method_decorator' }, + 'sentry.description': { type: 'string', value: '' }, 'code.function.name': { type: 'string', value: 'ngAfterViewInit' }, }), }), diff --git a/dev-packages/e2e-tests/test-applications/angular-18/tests/performance.test.ts b/dev-packages/e2e-tests/test-applications/angular-18/tests/performance.test.ts index f9dd339c01b1..15c5d1abe318 100644 --- a/dev-packages/e2e-tests/test-applications/angular-18/tests/performance.test.ts +++ b/dev-packages/e2e-tests/test-applications/angular-18/tests/performance.test.ts @@ -346,11 +346,12 @@ test.describe('TraceMethod Decorator', () => { expect(ngInitSpan).toBeDefined(); expect(ngInitSpan).toEqual( expect.objectContaining({ - name: '', + name: 'ngOnInit', attributes: expect.objectContaining({ 'sentry.op': { type: 'string', value: 'function' }, 'sentry.origin': { type: 'string', value: 'auto.ui.angular.trace_method_decorator' }, 'code.function.name': { type: 'string', value: 'ngOnInit' }, + 'sentry.description': { type: 'string', value: '' }, }), }), ); @@ -383,10 +384,11 @@ test.describe('TraceMethod Decorator', () => { expect(ngAfterViewInitSpan).toBeDefined(); expect(ngAfterViewInitSpan).toEqual( expect.objectContaining({ - name: '', + name: 'Function execution', attributes: expect.objectContaining({ 'sentry.op': { type: 'string', value: 'function' }, 'sentry.origin': { type: 'string', value: 'auto.ui.angular.trace_method_decorator' }, + 'sentry.description': { type: 'string', value: '' }, 'code.function.name': { type: 'string', value: 'ngAfterViewInit' }, }), }), diff --git a/dev-packages/e2e-tests/test-applications/angular-20/tests/performance.test.ts b/dev-packages/e2e-tests/test-applications/angular-20/tests/performance.test.ts index 291650eac6cc..e4516f724e7a 100644 --- a/dev-packages/e2e-tests/test-applications/angular-20/tests/performance.test.ts +++ b/dev-packages/e2e-tests/test-applications/angular-20/tests/performance.test.ts @@ -356,11 +356,12 @@ test.describe('TraceMethod Decorator', () => { expect(ngInitSpan).toBeDefined(); expect(ngInitSpan).toEqual( expect.objectContaining({ - name: '', + name: 'ngOnInit', attributes: expect.objectContaining({ 'sentry.op': { type: 'string', value: 'function' }, 'sentry.origin': { type: 'string', value: 'auto.ui.angular.trace_method_decorator' }, 'code.function.name': { type: 'string', value: 'ngOnInit' }, + 'sentry.description': { type: 'string', value: '' }, }), }), ); @@ -393,10 +394,11 @@ test.describe('TraceMethod Decorator', () => { expect(ngAfterViewInitSpan).toBeDefined(); expect(ngAfterViewInitSpan).toEqual( expect.objectContaining({ - name: '', + name: 'Function execution', attributes: expect.objectContaining({ 'sentry.op': { type: 'string', value: 'function' }, 'sentry.origin': { type: 'string', value: 'auto.ui.angular.trace_method_decorator' }, + 'sentry.description': { type: 'string', value: '' }, 'code.function.name': { type: 'string', value: 'ngAfterViewInit' }, }), }), diff --git a/dev-packages/e2e-tests/test-applications/angular-21/tests/performance.test.ts b/dev-packages/e2e-tests/test-applications/angular-21/tests/performance.test.ts index 5f5373758461..2e36b5700d6d 100644 --- a/dev-packages/e2e-tests/test-applications/angular-21/tests/performance.test.ts +++ b/dev-packages/e2e-tests/test-applications/angular-21/tests/performance.test.ts @@ -356,11 +356,12 @@ test.describe('TraceMethod Decorator', () => { expect(ngInitSpan).toBeDefined(); expect(ngInitSpan).toEqual( expect.objectContaining({ - name: '', + name: 'ngOnInit', attributes: expect.objectContaining({ 'sentry.op': { type: 'string', value: 'function' }, 'sentry.origin': { type: 'string', value: 'auto.ui.angular.trace_method_decorator' }, 'code.function.name': { type: 'string', value: 'ngOnInit' }, + 'sentry.description': { type: 'string', value: '' }, }), }), ); @@ -393,10 +394,11 @@ test.describe('TraceMethod Decorator', () => { expect(ngAfterViewInitSpan).toBeDefined(); expect(ngAfterViewInitSpan).toEqual( expect.objectContaining({ - name: '', + name: 'Function execution', attributes: expect.objectContaining({ 'sentry.op': { type: 'string', value: 'function' }, 'sentry.origin': { type: 'string', value: 'auto.ui.angular.trace_method_decorator' }, + 'sentry.description': { type: 'string', value: '' }, 'code.function.name': { type: 'string', value: 'ngAfterViewInit' }, }), }), diff --git a/dev-packages/e2e-tests/test-applications/angular-22/tests/performance.test.ts b/dev-packages/e2e-tests/test-applications/angular-22/tests/performance.test.ts index 2590de2f45f2..b763b72bf61e 100644 --- a/dev-packages/e2e-tests/test-applications/angular-22/tests/performance.test.ts +++ b/dev-packages/e2e-tests/test-applications/angular-22/tests/performance.test.ts @@ -356,11 +356,11 @@ test.describe('TraceMethod Decorator', () => { expect(ngInitSpan).toBeDefined(); expect(ngInitSpan).toEqual( expect.objectContaining({ - name: '', + name: 'ngOnInit', attributes: expect.objectContaining({ 'sentry.op': { type: 'string', value: 'function' }, 'sentry.origin': { type: 'string', value: 'auto.ui.angular.trace_method_decorator' }, - 'code.function.name': { type: 'string', value: 'ngOnInit' }, + 'sentry.description': { type: 'string', value: '' }, }), }), ); @@ -393,10 +393,11 @@ test.describe('TraceMethod Decorator', () => { expect(ngAfterViewInitSpan).toBeDefined(); expect(ngAfterViewInitSpan).toEqual( expect.objectContaining({ - name: '', + name: 'Function execution', attributes: expect.objectContaining({ 'sentry.op': { type: 'string', value: 'function' }, 'sentry.origin': { type: 'string', value: 'auto.ui.angular.trace_method_decorator' }, + 'sentry.description': { type: 'string', value: '' }, 'code.function.name': { type: 'string', value: 'ngAfterViewInit' }, }), }), diff --git a/packages/angular/src/tracing.ts b/packages/angular/src/tracing.ts index b0456e144c37..de2059ecdf8f 100644 --- a/packages/angular/src/tracing.ts +++ b/packages/angular/src/tracing.ts @@ -27,6 +27,7 @@ import { URL_FULL, URL_PATH, URL_TEMPLATE, + SENTRY_DESCRIPTION, } from '@sentry/conventions/attributes'; import { FUNCTION, ROUTER } from '@sentry/conventions/op'; import type { Integration, Span } from '@sentry/core'; @@ -39,6 +40,7 @@ import { stripUrlQueryAndFragment, timestampInSeconds, filterCollectedUrl, + FUNCTION_SPAN_NAME_FALLBACK, } from '@sentry/core'; import type { Observable } from 'rxjs'; import { Subscription } from 'rxjs'; @@ -404,15 +406,25 @@ export function TraceMethod(options?: TraceMethodOptions): MethodDecorator { descriptor.value = function (...args: unknown[]): ReturnType { const now = timestampInSeconds(); + const methodName = options?.name; + const description = `<${methodName || 'unnamed'}>`; + + const client = getClient(); + const hasSpanStreaming = client && hasSpanStreamingEnabled(client); + const name = hasSpanStreaming ? methodName || FUNCTION_SPAN_NAME_FALLBACK : description; + runOutsideAngular(() => { startInactiveSpan({ onlyIfParent: true, - name: `<${options?.name ? options.name : 'unnamed'}>`, + name, startTime: now, attributes: { [SENTRY_OP]: FUNCTION, [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.angular.trace_method_decorator', - [CODE_FUNCTION_NAME]: String(propertyKey), + // override description inference by Relay to preserve the original (transaction-based) description. + // sentry-conventions can't map the special case with the angle brackets. + ...(hasSpanStreaming && { [SENTRY_DESCRIPTION]: description }), + [CODE_FUNCTION_NAME]: methodName || String(propertyKey), }, }).end(now); }); diff --git a/packages/core/src/tracing/spans/spanNames.ts b/packages/core/src/tracing/spans/spanNames.ts index b0a0e29b9eb9..79ec92cf4e22 100644 --- a/packages/core/src/tracing/spans/spanNames.ts +++ b/packages/core/src/tracing/spans/spanNames.ts @@ -88,6 +88,12 @@ export const REQUEST_HANDLER_SPAN_NAME_FALLBACK = 'Request handler'; */ export const SERVERLESS_FUNCTION_SPAN_NAME_FALLBACK = 'Serverless function execution'; +/** + * Fallback name for function execution spans when no better-suited span name is available. + * @see https://getsentry.github.io/sentry-conventions/names/#function + */ +export const FUNCTION_SPAN_NAME_FALLBACK = 'Function execution'; + /** * The `cache.operation` attribute value each cache op carries. Cache span names are * `cache.{{cache.operation}}`, so the op constant itself doubles as the low-cardinality span name.