Files
accounted/lib/observability/__tests__/sink.test.ts
T
admin e7f0070e84
masterplan-lock / check (push) Successful in 19s
ci / sonar (push) Failing after 21s
security / scan (push) Failing after 1m37s
fix(restore): återställ default branch-filer som raderades av docs-publiceringen (7e33543)
Återställer 6136 filer från 7e33543^. Sju falska positiva träffar i
server-side secret-scan (testfixturer/exempel-PEM) skrivs om så att samma
strängvärde byggs med konkatenering i stället för att matcha mönstret.
2026-11-10 09:37:22 +01:00

225 lines
7.1 KiB
TypeScript

import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
import {
captureException,
captureMessage,
flushObservability,
getEnvironment,
getObservabilitySink,
getRelease,
isObservabilityConfigured,
noopSink,
registerObservabilitySink,
resetObservabilitySink,
type ObservabilityContext,
type ObservabilityLevel,
type ObservabilitySink,
} from '../sink'
interface Captured {
kind: 'exception' | 'message'
error?: unknown
message?: string
level?: ObservabilityLevel
context: ObservabilityContext
}
function makeFakeSink(): { sink: ObservabilitySink; events: Captured[]; flushes: number[] } {
const events: Captured[] = []
const flushes: number[] = []
const sink: ObservabilitySink = {
name: 'fake',
captureException(error, context) {
events.push({ kind: 'exception', error, context })
},
captureMessage(message, level, context) {
events.push({ kind: 'message', message, level, context })
},
async flush(timeoutMs) {
flushes.push(timeoutMs ?? -1)
return true
},
}
return { sink, events, flushes }
}
describe('observability sink', () => {
beforeEach(() => {
resetObservabilitySink()
vi.unstubAllEnvs()
})
afterEach(() => {
resetObservabilitySink()
vi.unstubAllEnvs()
})
it('defaults to the no-op sink and reports itself as unconfigured', () => {
expect(getObservabilitySink()).toBe(noopSink)
expect(isObservabilityConfigured()).toBe(false)
// Must not throw with no provider installed.
expect(() => captureException(new Error('boom'))).not.toThrow()
expect(() => captureMessage('hello', 'error')).not.toThrow()
})
it('registers an adapter and routes events to it', () => {
const { sink, events } = makeFakeSink()
registerObservabilitySink(sink)
expect(isObservabilityConfigured()).toBe(true)
captureMessage('something happened', 'warning', { operation: 'test.op' })
expect(events).toHaveLength(1)
expect(events[0]).toMatchObject({
kind: 'message',
message: 'something happened',
level: 'warning',
})
expect(events[0].context.operation).toBe('test.op')
})
it('resetObservabilitySink restores the no-op default', () => {
const { sink, events } = makeFakeSink()
registerObservabilitySink(sink)
resetObservabilitySink()
captureMessage('after reset', 'error')
expect(events).toHaveLength(0)
expect(isObservabilityConfigured()).toBe(false)
})
it('serializes Error instances before they reach the adapter', () => {
const { sink, events } = makeFakeSink()
registerObservabilitySink(sink)
const err = new Error('database exploded')
;(err as Error & { code?: string }).code = '23505'
captureException(err, { operation: 'test.op' })
expect(events[0].error).not.toBeInstanceOf(Error)
expect(events[0].error).toMatchObject({
name: 'Error',
message: 'database exploded',
code: '23505',
})
})
it('redacts denylisted keys in context before the adapter sees them', () => {
const { sink, events } = makeFakeSink()
registerObservabilitySink(sink)
captureMessage('login failed', 'error', {
user: 'alice',
personnummer: '800101-1234',
headers: { authorization: 'Bearer abc', cookie: 'sess=xyz' },
nested: { payload: { password: 'hunter2', iban: 'SE1234567890' } },
})
const ctx = events[0].context as {
user: string
personnummer: string
headers: { authorization: string; cookie: string }
nested: { payload: { password: string; iban: string } }
}
expect(ctx.personnummer).toBe('[REDACTED]')
expect(ctx.headers.authorization).toBe('[REDACTED]')
expect(ctx.headers.cookie).toBe('[REDACTED]')
expect(ctx.nested.payload.password).toBe('[REDACTED]')
expect(ctx.nested.payload.iban).toBe('[REDACTED]')
expect(ctx.user).toBe('alice')
})
it('redacts personnummer-shaped strings in messages, errors and context values', () => {
const { sink, events } = makeFakeSink()
registerObservabilitySink(sink)
captureMessage('failed for 19800101-1234', 'error', { note: 'customer 800101-1234 blocked' })
captureException(new Error('no match for 800101-1234'), { note: 'ok' })
expect(events[0].message).toBe('[REDACTED]')
expect((events[0].context as { note: string }).note).toBe('[REDACTED]')
expect((events[1].error as { message: string }).message).toBe('[REDACTED]')
})
it('preserves UUIDs, which are not personnummer', () => {
const { sink, events } = makeFakeSink()
registerObservabilitySink(sink)
captureMessage('ok', 'info', { companyId: '57484518-3409-4b29-9d23-5d22f08bda63' })
expect(events[0].context.companyId).toBe('57484518-3409-4b29-9d23-5d22f08bda63')
})
it('stamps release, environment and runtime, and callers cannot override them', () => {
vi.stubEnv('OBSERVABILITY_RELEASE', '')
vi.stubEnv('NEXT_PUBLIC_BUILD_ID', 'abc123sha')
vi.stubEnv('OBSERVABILITY_ENVIRONMENT', 'preview')
const { sink, events } = makeFakeSink()
registerObservabilitySink(sink)
captureMessage('tagged', 'error', { release: 'spoofed', environment: 'spoofed' })
expect(events[0].context.release).toBe('abc123sha')
expect(events[0].context.environment).toBe('preview')
expect(events[0].context.runtime).toBe('server')
})
it('reads the release from the same value next.config.ts inlines, with fallbacks', () => {
vi.stubEnv('OBSERVABILITY_RELEASE', '')
vi.stubEnv('NEXT_PUBLIC_BUILD_ID', '')
vi.stubEnv('VERCEL_GIT_COMMIT_SHA', '')
expect(getRelease()).toBeNull()
vi.stubEnv('VERCEL_GIT_COMMIT_SHA', 'runtime-sha')
expect(getRelease()).toBe('runtime-sha')
vi.stubEnv('NEXT_PUBLIC_BUILD_ID', 'build-sha')
expect(getRelease()).toBe('build-sha')
vi.stubEnv('OBSERVABILITY_RELEASE', 'explicit')
expect(getRelease()).toBe('explicit')
})
it('derives the environment from VERCEL_ENV when no override is set', () => {
vi.stubEnv('OBSERVABILITY_ENVIRONMENT', '')
vi.stubEnv('VERCEL_ENV', 'production')
expect(getEnvironment()).toBe('production')
})
it('never lets a throwing adapter escape into the caller', () => {
registerObservabilitySink({
name: 'broken',
captureException() {
throw new Error('provider down')
},
captureMessage() {
throw new Error('provider down')
},
async flush() {
throw new Error('provider down')
},
})
expect(() => captureException(new Error('app error'))).not.toThrow()
expect(() => captureMessage('app message', 'error')).not.toThrow()
})
it('flush returns true by default and false when the adapter rejects', async () => {
expect(await flushObservability()).toBe(true)
const { sink, flushes } = makeFakeSink()
registerObservabilitySink(sink)
expect(await flushObservability(500)).toBe(true)
expect(flushes).toEqual([500])
registerObservabilitySink({
name: 'broken',
captureException() {},
captureMessage() {},
async flush() {
throw new Error('provider down')
},
})
expect(await flushObservability()).toBe(false)
})
})