diff --git a/bun.lock b/bun.lock index 8a19cf54..0d63b0ce 100644 --- a/bun.lock +++ b/bun.lock @@ -52,6 +52,7 @@ "version": "1.1.0", "dependencies": { "@nuxt/kit": "^4.3.0", + "defu": "^6.1.4", }, "devDependencies": { "@nuxt/devtools": "^3.1.1", diff --git a/packages/evlog/package.json b/packages/evlog/package.json index a1d41ca1..7bf1f054 100644 --- a/packages/evlog/package.json +++ b/packages/evlog/package.json @@ -67,7 +67,8 @@ "typecheck": "echo 'Typecheck handled by build'" }, "dependencies": { - "@nuxt/kit": "^4.3.0" + "@nuxt/kit": "^4.3.0", + "defu": "^6.1.4" }, "devDependencies": { "@nuxt/devtools": "^3.1.1", diff --git a/packages/evlog/src/logger.ts b/packages/evlog/src/logger.ts index 5171fa04..fc491a73 100644 --- a/packages/evlog/src/logger.ts +++ b/packages/evlog/src/logger.ts @@ -1,3 +1,4 @@ +import { defu } from 'defu' import type { EnvironmentContext, Log, LogLevel, LoggerConfig, RequestLogger, RequestLoggerOptions, SamplingConfig, TailSamplingContext, WideEvent } from './types' import { colors, detectEnvironment, formatDuration, getConsoleMethod, getLevelColor, isDev, matchesPattern } from './utils' @@ -216,15 +217,14 @@ export function createRequestLogger(options: RequestLoggerOptions = {}): Request return { set>(data: T): void { - context = { ...context, ...data } + context = defu(data, context) as Record }, error(error: Error | string, errorContext?: Record): void { hasError = true const err = typeof error === 'string' ? new Error(error) : error - context = { - ...context, + const errorData = { ...errorContext, error: { name: err.name, @@ -232,6 +232,7 @@ export function createRequestLogger(options: RequestLoggerOptions = {}): Request stack: err.stack, }, } + context = defu(errorData, context) as Record }, emit(overrides?: Record & { _forceKeep?: boolean }): WideEvent | null { diff --git a/packages/evlog/src/types.ts b/packages/evlog/src/types.ts index 1512ab9b..94d9d7e9 100644 --- a/packages/evlog/src/types.ts +++ b/packages/evlog/src/types.ts @@ -204,7 +204,7 @@ export type WideEvent = BaseWideEvent & Record */ export interface RequestLogger { /** - * Add context to the wide event (shallow merge) + * Add context to the wide event (deep merge via defu) */ set: >(context: T) => void diff --git a/packages/evlog/test/logger.test.ts b/packages/evlog/test/logger.test.ts index 7770570b..e903da33 100644 --- a/packages/evlog/test/logger.test.ts +++ b/packages/evlog/test/logger.test.ts @@ -140,7 +140,7 @@ describe('createRequestLogger', () => { expect(context.cart).toEqual({ items: 3 }) }) - it('overwrites existing keys with set()', () => { + it('overwrites existing primitive keys with set()', () => { const logger = createRequestLogger({}) logger.set({ status: 'pending' }) @@ -150,6 +150,50 @@ describe('createRequestLogger', () => { expect(context.status).toBe('complete') }) + it('deep merges nested objects with set()', () => { + const logger = createRequestLogger({}) + + logger.set({ user: { name: 'Alice' } }) + logger.set({ user: { id: '123' } }) + + const context = logger.getContext() + expect(context.user).toEqual({ name: 'Alice', id: '123' }) + }) + + it('deep merges multiple levels of nesting', () => { + const logger = createRequestLogger({}) + + logger.set({ order: { customer: { name: 'Alice' } } }) + logger.set({ order: { customer: { email: 'alice@example.com' } } }) + logger.set({ order: { total: 99.99 } }) + + const context = logger.getContext() + expect(context.order).toEqual({ + customer: { name: 'Alice', email: 'alice@example.com' }, + total: 99.99, + }) + }) + + it('new values override existing values in nested objects', () => { + const logger = createRequestLogger({}) + + logger.set({ user: { status: 'pending' } }) + logger.set({ user: { status: 'active' } }) + + const context = logger.getContext() + expect(context.user).toEqual({ status: 'active' }) + }) + + it('handles arrays in nested objects', () => { + const logger = createRequestLogger({}) + + logger.set({ cart: { items: ['item1'] } }) + logger.set({ cart: { total: 50 } }) + + const context = logger.getContext() + expect(context.cart).toEqual({ items: ['item1'], total: 50 }) + }) + it('records error with error()', () => { const logger = createRequestLogger({}) const error = new Error('Payment failed') @@ -177,6 +221,25 @@ describe('createRequestLogger', () => { }) }) + it('deep merges errorContext with nested objects after set()', () => { + const logger = createRequestLogger({}) + + logger.set({ order: { id: '123', status: 'pending' } }) + logger.error(new Error('Payment failed'), { order: { payment: { method: 'card' } } }) + + const context = logger.getContext() + expect(context.order).toEqual({ + id: '123', + status: 'pending', + payment: { method: 'card' }, + }) + expect(context.error).toEqual({ + name: 'Error', + message: 'Payment failed', + stack: expect.any(String), + }) + }) + it('emits wide event on emit()', () => { const logger = createRequestLogger({ method: 'GET',