diff --git a/dev-packages/browser-integration-tests/suites/public-api/captureException/aggregateError-custom/test.ts b/dev-packages/browser-integration-tests/suites/public-api/captureException/aggregateError-custom/test.ts index 9a14cb72ab0f..fa8076266f23 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/captureException/aggregateError-custom/test.ts +++ b/dev-packages/browser-integration-tests/suites/public-api/captureException/aggregateError-custom/test.ts @@ -39,7 +39,6 @@ sentryTest('captures custom AggregateErrors', async ({ getLocalTestUrl, page }) ]); }); -// fixme: the mechanism should be on the error sentryTest( 'keeps a custom capture mechanism on the captured error instead of its causes', async ({ getLocalTestUrl, page }) => { @@ -53,10 +52,10 @@ sentryTest( value: 'Failure 1', mechanism: { exception_id: 2, - handled: false, // true, + handled: true, parent_id: 1, source: 'cause', - type: 'auto.http.example', // 'chained', + type: 'chained', }, }), expect.objectContaining({ @@ -73,8 +72,8 @@ sentryTest( value: 'Failure 3', mechanism: { exception_id: 0, - handled: true, // false, - type: 'generic', // 'auto.http.example', + handled: false, + type: 'auto.http.example', }, }), ]); diff --git a/dev-packages/cloudflare-integration-tests/suites/d1/test.ts b/dev-packages/cloudflare-integration-tests/suites/d1/test.ts index 13fdea3aa8c0..97d708ba25a1 100644 --- a/dev-packages/cloudflare-integration-tests/suites/d1/test.ts +++ b/dev-packages/cloudflare-integration-tests/suites/d1/test.ts @@ -72,8 +72,8 @@ it('captures error event when a D1 query references a non-existent table', async value: 'no such table: non_existent_table: SQLITE_ERROR', stacktrace: expect.any(Object), mechanism: { - type: 'auto.http.cloudflare', - handled: false, + type: 'chained', + handled: true, source: 'cause', exception_id: 1, parent_id: 0, @@ -84,8 +84,8 @@ it('captures error event when a D1 query references a non-existent table', async value: 'D1_ERROR: no such table: non_existent_table: SQLITE_ERROR', stacktrace: expect.any(Object), mechanism: { - type: 'generic', - handled: true, + type: 'auto.http.cloudflare', + handled: false, exception_id: 0, }, }, diff --git a/dev-packages/e2e-tests/test-applications/hono-4/tests/errors.test.ts b/dev-packages/e2e-tests/test-applications/hono-4/tests/errors.test.ts index e3686032fff3..06ab4b4b42dc 100644 --- a/dev-packages/e2e-tests/test-applications/hono-4/tests/errors.test.ts +++ b/dev-packages/e2e-tests/test-applications/hono-4/tests/errors.test.ts @@ -52,10 +52,10 @@ test.describe('route handler errors', () => { expect(firstCause?.value).toBe('Failure 1'); expect(firstCause?.mechanism).toEqual({ exception_id: 2, - handled: false, + handled: true, parent_id: 1, source: 'cause', - type: 'auto.http.hono.context_error', // should be 'chained' (general issue with LinkedErrors) + type: 'chained', }); const secondCause = errorEvent.exception?.values?.[1]; @@ -72,8 +72,8 @@ test.describe('route handler errors', () => { expect(capturedError?.value).toBe('Failure 3'); expect(capturedError?.mechanism).toEqual({ exception_id: 0, - handled: true, - type: 'generic', // should be 'auto.http.hono.context_error' (general issue with LinkedErrors) + handled: false, + type: 'auto.http.hono.context_error', }); expect(errorEvent.transaction).toBe('GET /linked-error'); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-15-t3/tests/trpc-error.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-15-t3/tests/trpc-error.test.ts index 5960c0f6af8f..0e2b2769df36 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-15-t3/tests/trpc-error.test.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-15-t3/tests/trpc-error.test.ts @@ -17,13 +17,22 @@ test('should capture error with trpc context', async ({ page }) => { expect(trpcError.contexts?.trpc?.procedure_path).toBe('post.throwError'); expect(trpcError.contexts?.trpc?.input).toEqual({ name: 'I love dogs' }); - expect(trpcError.exception?.values?.[0]?.mechanism).toEqual({ - handled: false, - type: 'auto.rpc.trpc.middleware', + const exceptionValues = trpcError.exception?.values; + expect(exceptionValues).toHaveLength(2); + expect(exceptionValues?.[0]?.type).toBe('Error'); + expect(exceptionValues?.[0]?.value).toBe('Error thrown in trpc router'); + expect(exceptionValues?.[0]?.mechanism).toEqual({ + handled: true, + type: 'chained', exception_id: 1, parent_id: 0, source: 'cause', }); + expect(exceptionValues?.[1]?.mechanism).toEqual({ + handled: false, + type: 'auto.rpc.trpc.middleware', + exception_id: 0, + }); }); test('should create transaction with trpc input for error', async ({ page }) => { diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/tests/errors.server.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/tests/errors.server.test.ts index 53d89e5b937f..90771e4174ad 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/tests/errors.server.test.ts +++ b/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/tests/errors.server.test.ts @@ -20,8 +20,8 @@ test.describe('server-side errors', async () => { expect(exception0.type).toEqual('Error'); expect(exception0.value).toEqual('Nuxt 3 Server error'); expect(exception0.mechanism).toEqual({ - handled: false, - type: 'auto.function.nuxt.nitro', + handled: true, + type: 'chained', exception_id: 1, parent_id: 0, source: 'cause', @@ -29,8 +29,11 @@ test.describe('server-side errors', async () => { expect(exception1.type).toEqual('Error'); expect(exception1.value).toEqual('Nuxt 3 Server error'); - // TODO: This isn't correct but requires adjustment in the core SDK - expect(exception1.mechanism).toEqual({ handled: true, type: 'generic', exception_id: 0 }); + expect(exception1.mechanism).toEqual({ + handled: false, + type: 'auto.function.nuxt.nitro', + exception_id: 0, + }); }); test('captures api fetch error (fetched on click) with parametrized route', async ({ page }) => { @@ -51,8 +54,8 @@ test.describe('server-side errors', async () => { expect(exception0.type).toEqual('Error'); expect(exception0.value).toEqual('Nuxt 3 Param Server error'); expect(exception0.mechanism).toEqual({ - handled: false, - type: 'auto.function.nuxt.nitro', + handled: true, + type: 'chained', exception_id: 1, parent_id: 0, source: 'cause', @@ -60,7 +63,10 @@ test.describe('server-side errors', async () => { expect(exception1.type).toEqual('Error'); expect(exception1.value).toEqual('Nuxt 3 Param Server error'); - // TODO: This isn't correct but requires adjustment in the core SDK - expect(exception1.mechanism).toEqual({ handled: true, type: 'generic', exception_id: 0 }); + expect(exception1.mechanism).toEqual({ + handled: false, + type: 'auto.function.nuxt.nitro', + exception_id: 0, + }); }); }); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-min/tests/errors.server.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-3-min/tests/errors.server.test.ts index 8b182fec69c7..e108a25ea9b1 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-3-min/tests/errors.server.test.ts +++ b/dev-packages/e2e-tests/test-applications/nuxt-3-min/tests/errors.server.test.ts @@ -20,17 +20,20 @@ test.describe('server-side errors', async () => { expect(exception0.type).toEqual('Error'); expect(exception0.value).toEqual('Nuxt 3 Server error'); expect(exception0.mechanism).toEqual({ - handled: false, - type: 'auto.function.nuxt.nitro', + handled: true, + type: 'chained', exception_id: 1, parent_id: 0, source: 'cause', }); - // TODO: This isn't correct but requires adjustment in the core SDK expect(exception1.type).toEqual('Error'); expect(exception1.value).toEqual('Nuxt 3 Server error'); - expect(exception1.mechanism).toEqual({ handled: true, type: 'generic', exception_id: 0 }); + expect(exception1.mechanism).toEqual({ + handled: false, + type: 'auto.function.nuxt.nitro', + exception_id: 0, + }); }); test('captures api fetch error (fetched on click) with parametrized route', async ({ page }) => { @@ -51,16 +54,19 @@ test.describe('server-side errors', async () => { expect(exception0.type).toEqual('Error'); expect(exception0.value).toEqual('Nuxt 3 Param Server error'); expect(exception0.mechanism).toEqual({ - handled: false, - type: 'auto.function.nuxt.nitro', + handled: true, + type: 'chained', exception_id: 1, parent_id: 0, source: 'cause', }); - // TODO: This isn't correct but requires adjustment in the core SDK expect(exception1.type).toEqual('Error'); expect(exception1.value).toEqual('Nuxt 3 Param Server error'); - expect(exception1.mechanism).toEqual({ handled: true, type: 'generic', exception_id: 0 }); + expect(exception1.mechanism).toEqual({ + handled: false, + type: 'auto.function.nuxt.nitro', + exception_id: 0, + }); }); }); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/tests/errors.server.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/tests/errors.server.test.ts index d7aafde49d55..b6558f7c87ee 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/tests/errors.server.test.ts +++ b/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/tests/errors.server.test.ts @@ -25,8 +25,8 @@ test.describe('server-side errors', async () => { expect(exception0.type).toEqual('Error'); expect(exception0.value).toEqual('Nuxt 3 Server error'); expect(exception0.mechanism).toEqual({ - handled: false, - type: 'auto.function.nuxt.nitro', + handled: true, + type: 'chained', exception_id: 1, parent_id: 0, source: 'cause', @@ -34,8 +34,11 @@ test.describe('server-side errors', async () => { expect(exception1.type).toEqual('Error'); expect(exception1.value).toEqual('Nuxt 3 Server error'); - // TODO: This isn't correct but requires adjustment in the core SDK - expect(exception1.mechanism).toEqual({ handled: true, type: 'generic', exception_id: 0 }); + expect(exception1.mechanism).toEqual({ + handled: false, + type: 'auto.function.nuxt.nitro', + exception_id: 0, + }); expect(error.tags?.['my-isolated-tag']).toBe(true); expect(error.tags?.['my-global-scope-isolated-tag']).not.toBeDefined(); @@ -82,8 +85,8 @@ test.describe('server-side errors', async () => { expect(exception0.type).toEqual('Error'); expect(exception0.value).toEqual('Nuxt 3 Param Server error'); expect(exception0.mechanism).toEqual({ - handled: false, - type: 'auto.function.nuxt.nitro', + handled: true, + type: 'chained', exception_id: 1, parent_id: 0, source: 'cause', @@ -91,7 +94,10 @@ test.describe('server-side errors', async () => { expect(exception1.type).toEqual('Error'); expect(exception1.value).toEqual('Nuxt 3 Param Server error'); - // TODO: This isn't correct but requires adjustment in the core SDK - expect(exception1.mechanism).toEqual({ handled: true, type: 'generic', exception_id: 0 }); + expect(exception1.mechanism).toEqual({ + handled: false, + type: 'auto.function.nuxt.nitro', + exception_id: 0, + }); }); }); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3/tests/errors.server.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-3/tests/errors.server.test.ts index 338ee6e3f78a..a360209ec1ce 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-3/tests/errors.server.test.ts +++ b/dev-packages/e2e-tests/test-applications/nuxt-3/tests/errors.server.test.ts @@ -20,7 +20,7 @@ test.describe('server-side errors', async () => { expect(exception0.type).toEqual('Error'); expect(exception0.value).toEqual('Nuxt 3 Server error'); expect(exception0.mechanism).toEqual({ - handled: false, + handled: true, type: 'chained', exception_id: 1, parent_id: 0, @@ -29,8 +29,11 @@ test.describe('server-side errors', async () => { expect(exception1.type).toEqual('Error'); expect(exception1.value).toEqual('Nuxt 3 Server error'); - // TODO: This isn't correct but requires adjustment in the core SDK - expect(exception1.mechanism).toEqual({ handled: true, type: 'generic', exception_id: 0 }); + expect(exception1.mechanism).toEqual({ + handled: false, + type: 'generic', + exception_id: 0, + }); }); test('captures api fetch error (fetched on click) with parametrized route', async ({ page }) => { @@ -51,7 +54,7 @@ test.describe('server-side errors', async () => { expect(exception0.type).toEqual('Error'); expect(exception0.value).toEqual('Nuxt 3 Param Server error'); expect(exception0.mechanism).toEqual({ - handled: false, + handled: true, type: 'chained', exception_id: 1, parent_id: 0, @@ -60,7 +63,10 @@ test.describe('server-side errors', async () => { expect(exception1.type).toEqual('Error'); expect(exception1.value).toEqual('Nuxt 3 Param Server error'); - // TODO: This isn't correct but requires adjustment in the core SDK - expect(exception1.mechanism).toEqual({ handled: true, type: 'generic', exception_id: 0 }); + expect(exception1.mechanism).toEqual({ + handled: false, + type: 'generic', + exception_id: 0, + }); }); }); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/tests/errors.server.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-4/tests/errors.server.test.ts index e0873c1f496e..8f7bf451a1f6 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-4/tests/errors.server.test.ts +++ b/dev-packages/e2e-tests/test-applications/nuxt-4/tests/errors.server.test.ts @@ -20,8 +20,8 @@ test.describe('server-side errors', async () => { expect(exception0.type).toEqual('Error'); expect(exception0.value).toEqual('Nuxt 4 Server error'); expect(exception0.mechanism).toEqual({ - handled: false, - type: 'auto.function.nuxt.nitro', + handled: true, + type: 'chained', exception_id: 1, parent_id: 0, source: 'cause', @@ -29,8 +29,11 @@ test.describe('server-side errors', async () => { expect(exception1.type).toEqual('Error'); expect(exception1.value).toEqual('Nuxt 4 Server error'); - // TODO: This isn't correct but requires adjustment in the core SDK - expect(exception1.mechanism).toEqual({ handled: true, type: 'generic', exception_id: 0 }); + expect(exception1.mechanism).toEqual({ + handled: false, + type: 'auto.function.nuxt.nitro', + exception_id: 0, + }); }); test('captures api fetch error (fetched on click) with parametrized route', async ({ page }) => { @@ -51,8 +54,8 @@ test.describe('server-side errors', async () => { expect(exception0.type).toEqual('Error'); expect(exception0.value).toEqual('Nuxt 4 Param Server error'); expect(exception0.mechanism).toEqual({ - handled: false, - type: 'auto.function.nuxt.nitro', + handled: true, + type: 'chained', exception_id: 1, parent_id: 0, source: 'cause', @@ -60,7 +63,10 @@ test.describe('server-side errors', async () => { expect(exception1.type).toEqual('Error'); expect(exception1.value).toEqual('Nuxt 4 Param Server error'); - // TODO: This isn't correct but requires adjustment in the core SDK - expect(exception1.mechanism).toEqual({ handled: true, type: 'generic', exception_id: 0 }); + expect(exception1.mechanism).toEqual({ + handled: false, + type: 'auto.function.nuxt.nitro', + exception_id: 0, + }); }); }); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-5/tests/errors.server.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-5/tests/errors.server.test.ts index 163dfd28c80a..fe17f262b0ae 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-5/tests/errors.server.test.ts +++ b/dev-packages/e2e-tests/test-applications/nuxt-5/tests/errors.server.test.ts @@ -20,8 +20,8 @@ test.describe('server-side errors', async () => { expect(exception0.type).toEqual('Error'); expect(exception0.value).toEqual('Nuxt 4 Server error'); expect(exception0.mechanism).toEqual({ - handled: false, - type: 'auto.function.nuxt.nitro', + handled: true, + type: 'chained', exception_id: 1, parent_id: 0, source: 'cause', @@ -29,8 +29,11 @@ test.describe('server-side errors', async () => { expect(exception1.type).toEqual('HTTPError'); expect(exception1.value).toEqual('Nuxt 4 Server error'); - // TODO: This isn't correct but requires adjustment in the core SDK - expect(exception1.mechanism).toEqual({ handled: true, type: 'generic', exception_id: 0 }); + expect(exception1.mechanism).toEqual({ + handled: false, + type: 'auto.function.nuxt.nitro', + exception_id: 0, + }); }); test('captures api fetch error (fetched on click) with parametrized route', async ({ page }) => { @@ -51,8 +54,8 @@ test.describe('server-side errors', async () => { expect(exception0.type).toEqual('Error'); expect(exception0.value).toEqual('Nuxt 4 Param Server error'); expect(exception0.mechanism).toEqual({ - handled: false, - type: 'auto.function.nuxt.nitro', + handled: true, + type: 'chained', exception_id: 1, parent_id: 0, source: 'cause', @@ -60,7 +63,10 @@ test.describe('server-side errors', async () => { expect(exception1.type).toEqual('HTTPError'); expect(exception1.value).toEqual('Nuxt 4 Param Server error'); - // TODO: This isn't correct but requires adjustment in the core SDK - expect(exception1.mechanism).toEqual({ handled: true, type: 'generic', exception_id: 0 }); + expect(exception1.mechanism).toEqual({ + handled: false, + type: 'auto.function.nuxt.nitro', + exception_id: 0, + }); }); }); diff --git a/dev-packages/e2e-tests/test-applications/react-19/tests/errors.test.ts b/dev-packages/e2e-tests/test-applications/react-19/tests/errors.test.ts index 496c47e417d2..1750611761b0 100644 --- a/dev-packages/e2e-tests/test-applications/react-19/tests/errors.test.ts +++ b/dev-packages/e2e-tests/test-applications/react-19/tests/errors.test.ts @@ -20,8 +20,8 @@ test('Catches errors caught by error boundary', async ({ page }) => { expect(errorEvent.exception?.values).toHaveLength(2); expect(errorEvent.exception?.values?.[0]?.value).toBe('caught error'); expect(errorEvent.exception?.values?.[0]?.mechanism).toEqual({ - type: 'auto.function.react.error_handler', - handled: true, // true because a callback was provided + type: 'chained', + handled: true, exception_id: 1, parent_id: 0, source: 'cause', @@ -29,7 +29,7 @@ test('Catches errors caught by error boundary', async ({ page }) => { expect(errorEvent.exception?.values?.[1]?.value).toBe('caught error'); expect(errorEvent.exception?.values?.[1]?.mechanism).toEqual({ - type: 'generic', + type: 'auto.function.react.error_handler', handled: true, // true because a callback was provided exception_id: 0, }); @@ -54,8 +54,8 @@ test('Catches errors uncaught by error boundary', async ({ page }) => { expect(errorEvent.exception?.values).toHaveLength(2); expect(errorEvent.exception?.values?.[0]?.value).toBe('uncaught error'); expect(errorEvent.exception?.values?.[0]?.mechanism).toEqual({ - type: 'auto.function.react.error_handler', - handled: true, // true because a callback was provided + type: 'chained', + handled: true, exception_id: 1, parent_id: 0, source: 'cause', @@ -63,7 +63,7 @@ test('Catches errors uncaught by error boundary', async ({ page }) => { expect(errorEvent.exception?.values?.[1]?.value).toBe('uncaught error'); expect(errorEvent.exception?.values?.[1]?.mechanism).toEqual({ - type: 'generic', + type: 'auto.function.react.error_handler', handled: true, // true because a callback was provided exception_id: 0, }); diff --git a/packages/browser/test/tracing/request.test.ts b/packages/browser/test/tracing/request.test.ts index f3bda9599399..c5e2b9a859dd 100644 --- a/packages/browser/test/tracing/request.test.ts +++ b/packages/browser/test/tracing/request.test.ts @@ -1,6 +1,7 @@ import type { Client } from '@sentry/core/browser'; import * as utils from '@sentry/core/browser'; import * as browserUtils from '@sentry/browser-utils'; +import { HTTP_METHOD } from '@sentry/conventions/attributes'; import type { MockInstance } from 'vitest'; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'; import { BrowserClient } from '../../src/client'; @@ -83,8 +84,9 @@ describe('instrumentOutgoingRequests', () => { expect(fetchHandler).toBeDefined(); expect(requestSpan).toBeDefined(); - expect(utils.spanToJSON(requestSpan!).description).toBe('QUERY https://example.com/rest/v1/users'); - expect(utils.spanToJSON(requestSpan!).data['http.method']).toBe('QUERY'); + const requestSpanJson = utils.spanToJSON(requestSpan!); + expect(requestSpanJson.name).toBe('QUERY https://example.com/rest/v1/users'); + expect(requestSpanJson.attributes[HTTP_METHOD]).toBe('QUERY'); }); it('creates a QUERY XHR span with the QUERY method attribute', () => { @@ -119,8 +121,9 @@ describe('instrumentOutgoingRequests', () => { expect(xhrHandler).toBeDefined(); expect(requestSpan).toBeDefined(); - expect(utils.spanToJSON(requestSpan!).description).toBe('QUERY https://example.com/rest/v1/users'); - expect(utils.spanToJSON(requestSpan!).data['http.method']).toBe('QUERY'); + const requestSpanJson = utils.spanToJSON(requestSpan!); + expect(requestSpanJson.name).toBe('QUERY https://example.com/rest/v1/users'); + expect(requestSpanJson.attributes[HTTP_METHOD]).toBe('QUERY'); }); describe('XHR trace header span', () => { diff --git a/packages/bun/test/integrations/bunHttpServer.test.ts b/packages/bun/test/integrations/bunHttpServer.test.ts index 3630b221614a..07157b9dd12a 100644 --- a/packages/bun/test/integrations/bunHttpServer.test.ts +++ b/packages/bun/test/integrations/bunHttpServer.test.ts @@ -1,5 +1,6 @@ import http from 'node:http'; -import { getActiveSpan, getCurrentScope, getTraceData, spanToJSON } from '@sentry/core'; +import { HTTP_METHOD } from '@sentry/conventions/attributes'; +import { getActiveSpan, getCurrentScope, getTraceData, SEMANTIC_ATTRIBUTE_SENTRY_OP, spanToJSON } from '@sentry/core'; import { afterAll, beforeAll, describe, expect, test } from 'bun:test'; import { init } from '../../src'; @@ -50,7 +51,7 @@ describe('Bun HTTP Server Integration', () => { await close(); expect(span).toBeDefined(); - expect(span?.attributes['sentry.op']).toBe('http.server'); + expect(span?.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toBe('http.server'); expect(span?.name).toBe('GET /users'); expect(span?.attributes['sentry.origin']).toBe('auto.http.server'); }); @@ -77,9 +78,9 @@ describe('Bun HTTP Server Integration', () => { await close(); expect(span).toBeDefined(); - expect(span?.op).toBe('http.server'); - expect(span?.description).toBe('QUERY /search'); - expect(span?.data['http.method']).toBe('QUERY'); + expect(span?.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toBe('http.server'); + expect(span?.name).toBe('QUERY /search'); + expect(span?.attributes[HTTP_METHOD]).toBe('QUERY'); }); test('isolates each incoming request with a distinct trace id', async () => { diff --git a/packages/core/src/utils/misc.ts b/packages/core/src/utils/misc.ts index 818f2cd9e0e8..445fe1b12e10 100644 --- a/packages/core/src/utils/misc.ts +++ b/packages/core/src/utils/misc.ts @@ -112,13 +112,34 @@ export function addExceptionMechanism(event: Event, newMechanism?: Partial): void { + const exceptions = event.exception?.values; + + // Exception groups identify the originally captured (root) exception with exception_id 0. + const capturedException = exceptions?.find(exception => exception.mechanism?.exception_id === 0) ?? exceptions?.[0]; + + if (!capturedException) { + return; + } + + applyExceptionMechanism(capturedException, newMechanism); +} + +function applyExceptionMechanism(exception: Exception, newMechanism?: Partial): void { const defaultMechanism = { type: 'generic', handled: true }; - const currentMechanism = firstException.mechanism; - firstException.mechanism = { ...defaultMechanism, ...currentMechanism, ...newMechanism }; + const currentMechanism = exception.mechanism; + exception.mechanism = { ...defaultMechanism, ...currentMechanism, ...newMechanism }; if (newMechanism && 'data' in newMechanism) { - const mergedData = { ...currentMechanism?.data, ...newMechanism.data }; - firstException.mechanism.data = mergedData; + exception.mechanism.data = { ...currentMechanism?.data, ...newMechanism.data }; } } diff --git a/packages/core/src/utils/prepareEvent.ts b/packages/core/src/utils/prepareEvent.ts index f24e6895b068..2af0eed8233f 100644 --- a/packages/core/src/utils/prepareEvent.ts +++ b/packages/core/src/utils/prepareEvent.ts @@ -7,7 +7,7 @@ import type { Event, EventHint } from '../types/event'; import type { ClientOptions } from '../types/options'; import type { StackParser } from '../types/stacktrace'; import { getFilenameToDebugIdMap } from './debug-ids'; -import { addExceptionMechanism, uuid4 } from './misc'; +import { addExceptionMechanismToCapturedException, uuid4 } from './misc'; import { normalize } from './normalize'; import { applyScopeDataToEvent, applySpanToEvent, getCombinedScopeData } from './scopeData'; import { getActiveSpan } from './spanUtils'; @@ -72,7 +72,7 @@ export function prepareEvent( const finalScope = getFinalScope(scope, hint.captureContext); if (hint.mechanism) { - addExceptionMechanism(prepared, hint.mechanism); + addExceptionMechanismToCapturedException(prepared, hint.mechanism); } const clientEventProcessors = client ? client.getEventProcessors() : []; diff --git a/packages/core/test/lib/client.test.ts b/packages/core/test/lib/client.test.ts index ab3bad953e29..bda4459c6a82 100644 --- a/packages/core/test/lib/client.test.ts +++ b/packages/core/test/lib/client.test.ts @@ -6,6 +6,7 @@ import { getCurrentScope, getIsolationScope, lastEventId, + linkedErrorsIntegration, makeSession, Scope, setCurrentClient, @@ -451,6 +452,61 @@ describe('Client', () => { ); }); + test('keeps a custom mechanism on the captured error when linked errors are prepended', () => { + const options = getDefaultTestClientOptions({ + dsn: PUBLIC_DSN, + integrations: [linkedErrorsIntegration()], + }); + const client = new TestClient(options); + client.init(); + const session = makeSession(); + getCurrentScope().setSession(session); + + const cause = new Error('Failure 1'); + const errorCause = Object.assign(new Error('Failure 2'), { cause }); + const error = Object.assign(new Error('Failure 3'), { cause: errorCause }); + + client.captureException(error, { + originalException: error, + mechanism: { type: 'auto.http.example', handled: false }, + }); + + expect(client.event?.exception?.values).toEqual([ + expect.objectContaining({ + type: 'Error', + value: 'Failure 1', + mechanism: { + exception_id: 2, + handled: true, + parent_id: 1, + source: 'cause', + type: 'chained', + }, + }), + expect.objectContaining({ + type: 'Error', + value: 'Failure 2', + mechanism: { + exception_id: 1, + handled: true, + parent_id: 0, + source: 'cause', + type: 'chained', + }, + }), + expect.objectContaining({ + type: 'Error', + value: 'Failure 3', + mechanism: { + exception_id: 0, + handled: false, + type: 'auto.http.example', + }, + }), + ]); + expect(client.session?.status).toBe('crashed'); + }); + test('does not truncate exception values by default', () => { const exceptionMessageLength = 10_000; const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN }); diff --git a/packages/core/test/lib/utils/aggregate-errors.test.ts b/packages/core/test/lib/utils/aggregate-errors.test.ts index a9c2bf79b153..86f796d2d8e0 100644 --- a/packages/core/test/lib/utils/aggregate-errors.test.ts +++ b/packages/core/test/lib/utils/aggregate-errors.test.ts @@ -5,7 +5,7 @@ import type { Event, EventHint } from '../../../src/types/event'; import type { Exception } from '../../../src/types/exception'; import type { StackParser } from '../../../src/types/stacktrace'; import { applyAggregateErrorsToEvent } from '../../../src/utils/aggregate-errors'; -import { addExceptionMechanism } from '../../../src/utils/misc'; +import { addExceptionMechanismToCapturedException } from '../../../src/utils/misc'; import { createStackParser } from '../../../src/utils/stacktrace'; const stackParser = createStackParser([0, line => ({ filename: line })]); @@ -117,11 +117,10 @@ describe('applyAggregateErrorsToEvent()', () => { }); }); - // fixme: the mechanism should be on the error test('keeps a capture mechanism on the captured error instead of its causes', () => { const cause = new Error('Failure 1'); - const errorCause = new Error('Failure 2', { cause }); - const error = new Error('Failure 3', { cause: errorCause }); + const errorCause = Object.assign(new Error('Failure 2'), { cause }); + const error = Object.assign(new Error('Failure 3'), { cause: errorCause }); const event: Event = { exception: { values: [exceptionFromError(stackParser, error)] } }; const eventHint: EventHint = { originalException: error, @@ -129,7 +128,7 @@ describe('applyAggregateErrorsToEvent()', () => { }; applyAggregateErrorsToEvent(exceptionFromError, stackParser, 'cause', 100, event, eventHint); - addExceptionMechanism(event, eventHint.mechanism); + addExceptionMechanismToCapturedException(event, eventHint.mechanism); expect(event.exception?.values).toStrictEqual([ { @@ -137,10 +136,10 @@ describe('applyAggregateErrorsToEvent()', () => { value: 'Failure 1', mechanism: { exception_id: 2, - handled: false, // true, + handled: true, parent_id: 1, source: 'cause', - type: 'auto.http.example', // 'chained', + type: 'chained', }, }, { @@ -159,8 +158,44 @@ describe('applyAggregateErrorsToEvent()', () => { value: 'Failure 3', mechanism: { exception_id: 0, - handled: true, //false, - type: 'instrument', //'auto.http.example', + handled: false, + type: 'auto.http.example', + }, + }, + ]); + }); + + test('keeps exception group metadata when applying a capture mechanism to an AggregateError', () => { + const error = new FakeAggregateError([new Error('Child Error')], 'Aggregate Error'); + const event: Event = { exception: { values: [exceptionFromError(stackParser, error)] } }; + const eventHint: EventHint = { + originalException: error, + mechanism: { handled: false, type: 'auto.http.example' }, + }; + + applyAggregateErrorsToEvent(exceptionFromError, stackParser, 'cause', 100, event, eventHint); + addExceptionMechanismToCapturedException(event, eventHint.mechanism); + + expect(event.exception?.values).toStrictEqual([ + { + type: 'Error', + value: 'Child Error', + mechanism: { + exception_id: 1, + handled: true, + parent_id: 0, + source: 'errors[0]', + type: 'chained', + }, + }, + { + type: 'AggregateError', + value: 'Aggregate Error', + mechanism: { + exception_id: 0, + handled: false, + is_exception_group: true, + type: 'auto.http.example', }, }, ]); diff --git a/packages/core/test/lib/utils/misc.test.ts b/packages/core/test/lib/utils/misc.test.ts index 62b37e8a520a..9099e4d2a1da 100644 --- a/packages/core/test/lib/utils/misc.test.ts +++ b/packages/core/test/lib/utils/misc.test.ts @@ -5,6 +5,7 @@ import type { StackFrame } from '../../../src/types/stackframe'; import { addContextToFrame, addExceptionMechanism, + addExceptionMechanismToCapturedException, checkOrSetAlreadyCaught, getEventDescription, uuid4, @@ -243,6 +244,63 @@ describe('addExceptionMechanism', () => { expect(event.exception.values[0].mechanism).toEqual({ type: 'instrument', handled: true, synthetic: true }); }); + it('adds the mechanism to the captured exception in an exception group', () => { + const event: Event = { + exception: { + values: [ + { + value: 'Cause', + mechanism: { type: 'chained', handled: true, exception_id: 1, parent_id: 0 }, + }, + { + value: 'Captured Error', + mechanism: { type: 'generic', handled: true, exception_id: 0, data: { source: 'existing' } }, + }, + ], + }, + }; + + addExceptionMechanismToCapturedException(event, { + type: 'auto.test.capture', + handled: false, + data: { handler: 'request' }, + }); + + expect(event.exception?.values).toEqual([ + { + value: 'Cause', + mechanism: { type: 'chained', handled: true, exception_id: 1, parent_id: 0 }, + }, + { + value: 'Captured Error', + mechanism: { + type: 'auto.test.capture', + handled: false, + exception_id: 0, + data: { source: 'existing', handler: 'request' }, + }, + }, + ]); + }); + + it('falls back to the first exception without exception group metadata', () => { + const event: Event = { + exception: { + values: [ + { value: 'First', mechanism: { type: 'generic', handled: true } }, + { value: 'Second', mechanism: { type: 'generic', handled: true } }, + ], + }, + }; + + addExceptionMechanismToCapturedException(event, { type: 'auto.test.capture', handled: false }); + + expect(event.exception?.values).toEqual([ + { value: 'First', mechanism: { type: 'auto.test.capture', handled: false } }, + { value: 'Second', mechanism: { type: 'generic', handled: true } }, + ]); + }); + it('merges data values', () => { const event = { ...baseEvent };