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..211befa2c933 100644 --- a/packages/browser/src/client.ts +++ b/packages/browser/src/client.ts @@ -86,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) { 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..0056484d01e4 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'; @@ -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); @@ -1254,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; } } @@ -1274,14 +1283,14 @@ 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 && crashed); + const shouldUpdateAndSend = (sessionNonTerminal && session.errors === 0) || (sessionNonTerminal && unhandled); if (shouldUpdateAndSend) { updateSession(session, { - ...(crashed && { status: 'crashed' }), - errors: session.errors || Number(errored || crashed), + ...(unhandled && { status: this._unhandledSessionStatus }), + errors: session.errors || Number(errored || unhandled), }); this.captureSession(session); } 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 {