Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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),
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});

Expand Down
3 changes: 3 additions & 0 deletions packages/browser/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,9 @@ export class BrowserClient extends Client<BrowserClientOptions> {

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) {
Expand Down
46 changes: 46 additions & 0 deletions packages/browser/test/client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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', () => {
Expand Down
29 changes: 19 additions & 10 deletions packages/core/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -220,6 +220,14 @@ export abstract class Client<O extends ClientOptions = ClientOptions> {

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.
*
Expand All @@ -234,6 +242,7 @@ export abstract class Client<O extends ClientOptions = ClientOptions> {
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);
Expand Down Expand Up @@ -1254,34 +1263,34 @@ export abstract class Client<O extends ClientOptions = ClientOptions> {

/** 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;
}
}
}

// 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);
}
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/types/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export interface Session {

export type SessionContext = Partial<Session>;

export type SessionStatus = 'ok' | 'exited' | 'crashed' | 'abnormal';
export type SessionStatus = 'ok' | 'exited' | 'crashed' | 'abnormal' | 'unhandled';

/** JSDoc */
export interface SessionAggregates {
Expand Down
Loading