diff --git a/packages/node-core/src/integrations/processSession.ts b/packages/node-core/src/integrations/processSession.ts index f1c7f9b0dadf..d19948cbe6f9 100644 --- a/packages/node-core/src/integrations/processSession.ts +++ b/packages/node-core/src/integrations/processSession.ts @@ -18,11 +18,11 @@ export const processSessionIntegration = defineIntegration(() => { process.on('beforeExit', () => { const session = getIsolationScope().getSession(); - // Only call endSession, if the Session exists on Scope and SessionStatus is not a - // Terminal Status i.e. Exited or Crashed because - // "When a session is moved away from ok it must not be updated anymore." + // Only call endSession if a Session exists on the Scope and has not already reached a + // Terminal Status, because "When a session is moved away from ok it must not be updated + // anymore." `ok` is the only non-terminal status. // Ref: https://develop.sentry.dev/sdk/sessions/ - if (session?.status !== 'ok') { + if (session?.status === 'ok') { endSession(); } }); diff --git a/packages/node-core/test/integrations/processSession.test.ts b/packages/node-core/test/integrations/processSession.test.ts new file mode 100644 index 000000000000..4d511035b8f9 --- /dev/null +++ b/packages/node-core/test/integrations/processSession.test.ts @@ -0,0 +1,73 @@ +import { getIsolationScope, setCurrentClient } from '@sentry/core'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { processSessionIntegration } from '../../src/integrations/processSession'; +import { NodeClient } from '../../src/sdk/client'; +import { getDefaultNodeClientOptions } from '../helpers/getDefaultNodeClientOptions'; + +describe('processSessionIntegration', () => { + let client: NodeClient; + let sendSession: ReturnType; + let beforeExitHandler: () => void; + + beforeEach(() => { + getIsolationScope().setSession(undefined); + + client = new NodeClient(getDefaultNodeClientOptions({ release: '1.0.0' })); + setCurrentClient(client); + client.init(); + sendSession = vi.spyOn(client, 'sendSession').mockImplementation(() => undefined); + + const processOn = vi.spyOn(process, 'on').mockImplementation(((event: string, listener: () => void) => { + if (event === 'beforeExit') { + beforeExitHandler = listener; + } + return process; + }) as never); + + processSessionIntegration().setupOnce!(); + processOn.mockRestore(); + }); + + it('has a name', () => { + expect(processSessionIntegration().name).toBe('ProcessSession'); + }); + + it('starts a session on setup', () => { + expect(getIsolationScope().getSession()).toEqual(expect.objectContaining({ status: 'ok' })); + }); + + it('ends the session with status "exited" on a healthy exit', () => { + beforeExitHandler(); + + expect(sendSession).toHaveBeenCalledTimes(1); + expect(sendSession).toHaveBeenCalledWith(expect.objectContaining({ status: 'exited', errors: 0 })); + }); + + it('ends a session that recorded a handled error', () => { + const session = getIsolationScope().getSession()!; + session.errors = 1; + + beforeExitHandler(); + + expect(sendSession).toHaveBeenCalledWith(expect.objectContaining({ status: 'exited', errors: 1 })); + }); + + it.each(['exited', 'crashed', 'abnormal', 'unhandled'] as const)('does not update an already-%s session', status => { + const session = getIsolationScope().getSession()!; + session.status = status; + sendSession.mockClear(); + + beforeExitHandler(); + + expect(sendSession).not.toHaveBeenCalled(); + }); + + it('does nothing when no session is on the scope', () => { + getIsolationScope().setSession(undefined); + sendSession.mockClear(); + + beforeExitHandler(); + + expect(sendSession).not.toHaveBeenCalled(); + }); +});