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
7 changes: 7 additions & 0 deletions .changeset/concatenate-arrays-set.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'evlog': minor
---

`log.set()` concatenates arrays when merging context for the same key. For example, `set({ items: [1, 2] })` followed by `set({ items: [3] })` yields `{ items: [1, 2, 3] }` instead of replacing with `[3]`. Plain objects are still deep-merged recursively; if either the existing or incoming value is not an array, the new value replaces the old one.

**Breaking change:** Call sites that relied on the last `set` overwriting an array now accumulate elements. To replace a value at emit time, use `emit({ ... })` overrides or a different field name.
2 changes: 2 additions & 0 deletions packages/evlog/src/logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ function mergeInto(target: Record<string, unknown>, source: Record<string, unkno
const targetVal = target[key]
if (isPlainObject(sourceVal) && isPlainObject(targetVal)) {
mergeInto(targetVal, sourceVal)
} else if (Array.isArray(targetVal) && Array.isArray(sourceVal)) {
target[key] = [...targetVal, ...sourceVal]
} else {
target[key] = sourceVal
}
Expand Down
5 changes: 4 additions & 1 deletion packages/evlog/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -466,7 +466,10 @@ export type FieldContext<T extends object = Record<string, unknown>> =
*/
export interface RequestLogger<T extends object = Record<string, unknown>> {
/**
* Add context to the wide event (deep merge via defu)
* Add context to the wide event. Plain objects are merged recursively.
* When both the existing and incoming values for a key are arrays, elements are
* concatenated (existing order preserved, new elements appended). Otherwise the
* new value replaces the old one (including when only one side is an array).
*/
set: (context: FieldContext<T>) => void

Expand Down
36 changes: 36 additions & 0 deletions packages/evlog/test/logger.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,42 @@ describe('createRequestLogger', () => {
expect(context.cart).toEqual({ items: ['item1'], total: 50 })
})

it('concatenates arrays on the same key with set()', () => {
const logger = createRequestLogger({})

logger.set({ array: [1, 2] })
logger.set({ array: [3] })

expect(logger.getContext().array).toEqual([1, 2, 3])
})

it('concatenates nested arrays on the same key with set()', () => {
const logger = createRequestLogger({})

logger.set({ job: { steps: ['a'] } })
logger.set({ job: { steps: ['b', 'c'] } })

expect(logger.getContext().job).toEqual({ steps: ['a', 'b', 'c'] })
})

it('replaces array with non-array on the same key with set()', () => {
const logger = createRequestLogger({})

logger.set({ tags: ['a', 'b'] })
logger.set({ tags: 'done' })

expect(logger.getContext().tags).toBe('done')
})

it('does not drop prior array elements when appending an empty array', () => {
const logger = createRequestLogger({})

logger.set({ ids: [1, 2] })
logger.set({ ids: [] })

expect(logger.getContext().ids).toEqual([1, 2])
})

it('records error with error()', () => {
const logger = createRequestLogger({})
const error = new Error('Payment failed')
Expand Down
Loading