From 6ba3cd5de4bc4106f384a9fac5f0b0e507a8f925 Mon Sep 17 00:00:00 2001 From: Martin Sonnberger Date: Tue, 21 Jul 2026 17:42:30 +0200 Subject: [PATCH 1/5] feat(browser)!: Send session status `unhandled` instead of `crashed` for unhandled errors --- .../suites/sessions/page-lifecycle/test.ts | 2 +- .../suites/sessions/update-session/test.ts | 2 +- packages/browser/src/client.ts | 9 ++++ packages/browser/test/client.test.ts | 46 +++++++++++++++++++ packages/core/src/client.ts | 14 +++++- packages/core/src/types/session.ts | 2 +- 6 files changed, 70 insertions(+), 5 deletions(-) diff --git a/dev-packages/browser-integration-tests/suites/sessions/page-lifecycle/test.ts b/dev-packages/browser-integration-tests/suites/sessions/page-lifecycle/test.ts index d98c8b377a4c..591a240f93d7 100644 --- a/dev-packages/browser-integration-tests/suites/sessions/page-lifecycle/test.ts +++ b/dev-packages/browser-integration-tests/suites/sessions/page-lifecycle/test.ts @@ -99,7 +99,7 @@ sentryTest('Updates the session when an error is thrown', async ({ getLocalTestU ...initialSession, errors: 1, init: false, - status: 'crashed', + status: 'unhandled', timestamp: expect.any(String), }); }); diff --git a/dev-packages/browser-integration-tests/suites/sessions/update-session/test.ts b/dev-packages/browser-integration-tests/suites/sessions/update-session/test.ts index 96c68858b361..5fce5c0535f3 100644 --- a/dev-packages/browser-integration-tests/suites/sessions/update-session/test.ts +++ b/dev-packages/browser-integration-tests/suites/sessions/update-session/test.ts @@ -19,7 +19,7 @@ sentryTest('should update session when an error is thrown.', async ({ getLocalTe expect(updatedSession.init).toBe(false); expect(updatedSession.errors).toBe(1); - expect(updatedSession.status).toBe('crashed'); + expect(updatedSession.status).toBe('unhandled'); expect(pageloadSession.sid).toBe(updatedSession.sid); }); diff --git a/packages/browser/src/client.ts b/packages/browser/src/client.ts index 5e9ab58b9121..20063fcaa35e 100644 --- a/packages/browser/src/client.ts +++ b/packages/browser/src/client.ts @@ -7,6 +7,7 @@ import type { Options as CoreOptions, ParameterizedString, Scope, + SessionStatus, SeverityLevel, } from '@sentry/core/browser'; import { addAutoIpAddressToSession, applySdkMetadata, Client, getSDKSource } from '@sentry/core/browser'; @@ -157,6 +158,14 @@ export class BrowserClient extends Client { return super._prepareEvent(event, hint, currentScope, isolationScope); } + + /** + * @inheritDoc + */ + protected _getUnhandledSessionStatus(): SessionStatus { + // Unhandled errors don't actually crash the browser, so we report `unhandled` rather than `crashed`. + return 'unhandled'; + } } /** Exported only for tests. */ diff --git a/packages/browser/test/client.test.ts b/packages/browser/test/client.test.ts index 7aac2c90d083..75a7fe63b775 100644 --- a/packages/browser/test/client.test.ts +++ b/packages/browser/test/client.test.ts @@ -2,6 +2,7 @@ * @vitest-environment jsdom */ +import { getCurrentScope, makeSession, setCurrentClient } from '@sentry/core/browser'; import { afterEach, describe, expect, it, vi } from 'vitest'; import { applyDefaultOptions, BrowserClient } from '../src/client'; import { WINDOW } from '../src/helpers'; @@ -49,6 +50,51 @@ describe('BrowserClient', () => { expect(flushOutcomesSpy).not.toHaveBeenCalled(); expect(flushSpy).toHaveBeenCalledTimes(1); }); + + describe('session status on unhandled errors', () => { + afterEach(() => { + getCurrentScope().setSession(undefined); + getCurrentScope().setClient(undefined); + }); + + it('sets the session status to "unhandled" for an unhandled exception', () => { + client = new BrowserClient(getDefaultBrowserClientOptions()); + setCurrentClient(client); + + const session = makeSession(); + getCurrentScope().setSession(session); + + client.captureException(new Error('test'), { mechanism: { handled: false } }); + + expect(session.status).toBe('unhandled'); + expect(session.errors).toBe(1); + }); + + it('sets the session status to "unhandled" for a fatal event', () => { + client = new BrowserClient(getDefaultBrowserClientOptions()); + setCurrentClient(client); + + const session = makeSession(); + getCurrentScope().setSession(session); + + client.captureEvent({ message: 'test', level: 'fatal' }); + + expect(session.status).toBe('unhandled'); + }); + + it('keeps the session status "ok" for a handled exception', () => { + client = new BrowserClient(getDefaultBrowserClientOptions()); + setCurrentClient(client); + + const session = makeSession(); + getCurrentScope().setSession(session); + + client.captureException(new Error('test')); + + expect(session.status).toBe('ok'); + expect(session.errors).toBe(1); + }); + }); }); describe('applyDefaultOptions', () => { diff --git a/packages/core/src/client.ts b/packages/core/src/client.ts index 87b62479d18e..dd4f9110c4de 100644 --- a/packages/core/src/client.ts +++ b/packages/core/src/client.ts @@ -32,7 +32,7 @@ import type { ParameterizedString } from './types/parameterize'; import type { ReplayEndEvent, ReplayStartEvent } from './types/replay'; import type { RequestEventData } from './types/request'; import type { SdkMetadata } from './types/sdkmetadata'; -import type { Session, SessionAggregates } from './types/session'; +import type { Session, SessionAggregates, SessionStatus } from './types/session'; import type { SeverityLevel } from './types/severity'; import type { Span, SpanAttributes, SpanContextData, SpanJSON, StreamedSpanJSON } from './types/span'; import type { StartSpanOptions } from './types/startSpanOptions'; @@ -1280,13 +1280,23 @@ export abstract class Client { if (shouldUpdateAndSend) { updateSession(session, { - ...(crashed && { status: 'crashed' }), + ...(crashed && { status: this._getUnhandledSessionStatus() }), errors: session.errors || Number(errored || crashed), }); this.captureSession(session); } } + /** + * The session status to set when an unhandled error terminates a session. + * + * Defaults to `'crashed'`. Browser SDKs override this to `'unhandled'` because unhandled errors + * don't actually crash the browser. + */ + protected _getUnhandledSessionStatus(): SessionStatus { + return 'crashed'; + } + /** * Determine if the client is finished processing. Returns a promise because it will wait `timeout` ms before saying * "no" (resolving to `false`) in order to give the client a chance to potentially finish first. diff --git a/packages/core/src/types/session.ts b/packages/core/src/types/session.ts index 1cdfef158af8..e5e1753dd8ef 100644 --- a/packages/core/src/types/session.ts +++ b/packages/core/src/types/session.ts @@ -30,7 +30,7 @@ export interface Session { export type SessionContext = Partial; -export type SessionStatus = 'ok' | 'exited' | 'crashed' | 'abnormal'; +export type SessionStatus = 'ok' | 'exited' | 'crashed' | 'abnormal' | 'unhandled'; /** JSDoc */ export interface SessionAggregates { From d8f97f4a7233b26c3bdbf1e5cf82e816ed6c53de Mon Sep 17 00:00:00 2001 From: Martin Sonnberger Date: Wed, 22 Jul 2026 16:33:15 +0200 Subject: [PATCH 2/5] refactor to use property instead of method --- packages/browser/src/client.ts | 12 +++--------- packages/core/src/client.ts | 21 ++++++++++----------- 2 files changed, 13 insertions(+), 20 deletions(-) diff --git a/packages/browser/src/client.ts b/packages/browser/src/client.ts index 20063fcaa35e..211befa2c933 100644 --- a/packages/browser/src/client.ts +++ b/packages/browser/src/client.ts @@ -7,7 +7,6 @@ import type { Options as CoreOptions, ParameterizedString, Scope, - SessionStatus, SeverityLevel, } from '@sentry/core/browser'; import { addAutoIpAddressToSession, applySdkMetadata, Client, getSDKSource } from '@sentry/core/browser'; @@ -87,6 +86,9 @@ export class BrowserClient extends Client { super(opts); + // Unhandled errors don't actually crash the browser, so we report `unhandled` rather than `crashed`. + this._unhandledSessionStatus = 'unhandled'; + const { userInfo } = this.getDataCollectionOptions(); if (opts._metadata?.sdk) { @@ -158,14 +160,6 @@ export class BrowserClient extends Client { return super._prepareEvent(event, hint, currentScope, isolationScope); } - - /** - * @inheritDoc - */ - protected _getUnhandledSessionStatus(): SessionStatus { - // Unhandled errors don't actually crash the browser, so we report `unhandled` rather than `crashed`. - return 'unhandled'; - } } /** Exported only for tests. */ diff --git a/packages/core/src/client.ts b/packages/core/src/client.ts index dd4f9110c4de..0f09a153669c 100644 --- a/packages/core/src/client.ts +++ b/packages/core/src/client.ts @@ -220,6 +220,14 @@ export abstract class Client { protected readonly _dataCollection: ResolvedDataCollection; + /** + * The session status to set when an unhandled error terminates a session. + * + * Defaults to `'crashed'`. Browser SDKs override this to `'unhandled'` because unhandled errors + * don't actually crash the browser. + */ + protected _unhandledSessionStatus: SessionStatus; + /** * Initializes this client instance. * @@ -234,6 +242,7 @@ export abstract class Client { this._eventProcessors = []; this._promiseBuffer = makePromiseBuffer(options.transportOptions?.bufferSize ?? DEFAULT_TRANSPORT_BUFFER_SIZE); this._dataCollection = resolveDataCollectionOptions(options); + this._unhandledSessionStatus = 'crashed'; if (options.dsn) { this._dsn = makeDsn(options.dsn); @@ -1280,23 +1289,13 @@ export abstract class Client { if (shouldUpdateAndSend) { updateSession(session, { - ...(crashed && { status: this._getUnhandledSessionStatus() }), + ...(crashed && { status: this._unhandledSessionStatus }), errors: session.errors || Number(errored || crashed), }); this.captureSession(session); } } - /** - * The session status to set when an unhandled error terminates a session. - * - * Defaults to `'crashed'`. Browser SDKs override this to `'unhandled'` because unhandled errors - * don't actually crash the browser. - */ - protected _getUnhandledSessionStatus(): SessionStatus { - return 'crashed'; - } - /** * Determine if the client is finished processing. Returns a promise because it will wait `timeout` ms before saying * "no" (resolving to `false`) in order to give the client a chance to potentially finish first. From 75c526f2fa12efdb08340ed395f9a6cabe644a4d Mon Sep 17 00:00:00 2001 From: Martin Sonnberger Date: Wed, 22 Jul 2026 16:52:10 +0200 Subject: [PATCH 3/5] rename variable --- packages/core/src/client.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/core/src/client.ts b/packages/core/src/client.ts index 0f09a153669c..a59eb53fe1e7 100644 --- a/packages/core/src/client.ts +++ b/packages/core/src/client.ts @@ -1263,19 +1263,19 @@ export abstract class Client { /** Updates existing session based on the provided event */ protected _updateSessionFromEvent(session: Session, event: Event): void { - // initially, set `crashed` based on the event level and update from exceptions if there are any later on - let crashed = event.level === 'fatal'; + // initially, set `unhandled` based on the event level and update from exceptions if there are any later on + let unhandled = event.level === 'fatal'; let errored = false; const exceptions = event.exception?.values; if (exceptions) { errored = true; - // reset crashed to false if there are exceptions, to ensure `mechanism.handled` is respected. - crashed = false; + // reset `unhandled` to false if there are exceptions, to ensure `mechanism.handled` is respected. + unhandled = false; for (const ex of exceptions) { if (ex.mechanism?.handled === false) { - crashed = true; + unhandled = true; break; } } @@ -1285,12 +1285,12 @@ export abstract class Client { // 1. Session with non terminal status and 0 errors + an error occurred -> Will set error count to 1 and send update // 2. Session with non terminal status and 1 error + a crash occurred -> Will set status crashed and send update const sessionNonTerminal = session.status === 'ok'; - const shouldUpdateAndSend = (sessionNonTerminal && session.errors === 0) || (sessionNonTerminal && crashed); + const shouldUpdateAndSend = (sessionNonTerminal && session.errors === 0) || (sessionNonTerminal && unhandled); if (shouldUpdateAndSend) { updateSession(session, { - ...(crashed && { status: this._unhandledSessionStatus }), - errors: session.errors || Number(errored || crashed), + ...(unhandled && { status: this._unhandledSessionStatus }), + errors: session.errors || Number(errored || unhandled), }); this.captureSession(session); } From 62ba930488bc66dbd15bb9ca06e2c9d23e1dee58 Mon Sep 17 00:00:00 2001 From: Martin Sonnberger Date: Wed, 22 Jul 2026 16:54:17 +0200 Subject: [PATCH 4/5] one more --- packages/core/src/client.ts | 2 +- paseo.json | 13 +++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) create mode 100644 paseo.json diff --git a/packages/core/src/client.ts b/packages/core/src/client.ts index a59eb53fe1e7..0056484d01e4 100644 --- a/packages/core/src/client.ts +++ b/packages/core/src/client.ts @@ -1283,7 +1283,7 @@ export abstract class Client { // A session is updated and that session update is sent in only one of the two following scenarios: // 1. Session with non terminal status and 0 errors + an error occurred -> Will set error count to 1 and send update - // 2. Session with non terminal status and 1 error + a crash occurred -> Will set status crashed and send update + // 2. Session with non terminal status and 1 error + a crash occurred -> Will set status unhandled and send update const sessionNonTerminal = session.status === 'ok'; const shouldUpdateAndSend = (sessionNonTerminal && session.errors === 0) || (sessionNonTerminal && unhandled); diff --git a/paseo.json b/paseo.json new file mode 100644 index 000000000000..52d5701da05a --- /dev/null +++ b/paseo.json @@ -0,0 +1,13 @@ +{ + "metadataGeneration": { + "branchName": { + "instructions": "Prefix branches with ms/" + }, + "commitMessage": { + "instructions": "Use Conventional Commits with a scope, usually the package name" + }, + "pullRequest": { + "instructions": "Don't be too wordy. Explain more the why behind the change, not the how that can be seen anyway in the diff" + } + } +} From 705fba801b9658804a435e71db2aa05ddde518b7 Mon Sep 17 00:00:00 2001 From: Martin Sonnberger Date: Wed, 22 Jul 2026 17:26:38 +0200 Subject: [PATCH 5/5] whoops --- paseo.json | 13 ------------- 1 file changed, 13 deletions(-) delete mode 100644 paseo.json diff --git a/paseo.json b/paseo.json deleted file mode 100644 index 52d5701da05a..000000000000 --- a/paseo.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "metadataGeneration": { - "branchName": { - "instructions": "Prefix branches with ms/" - }, - "commitMessage": { - "instructions": "Use Conventional Commits with a scope, usually the package name" - }, - "pullRequest": { - "instructions": "Don't be too wordy. Explain more the why behind the change, not the how that can be seen anyway in the diff" - } - } -}