From 146f5506fc28e11e91af0d1bba3a299fc81e1a5e Mon Sep 17 00:00:00 2001 From: Sumeet Jha <109774891+O1sumitkumar@users.noreply.github.com> Date: Sun, 18 Oct 2026 09:22:47 +0530 Subject: [PATCH 1/2] fix(browser): keep json option and full context in child() --- src/browser.ts | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/browser.ts b/src/browser.ts index 5aec411..25821a3 100644 --- a/src/browser.ts +++ b/src/browser.ts @@ -379,7 +379,9 @@ export class BrowserRemoteTransport implements IBrowserTransport { * Node.js dependencies. It can be bundled for any runtime. */ export class BrowserLogger { - private readonly config: Required> & { + private readonly config: Required< + Pick + > & { context: Record | undefined; }; private readonly transports: IBrowserTransport[]; @@ -389,13 +391,14 @@ export class BrowserLogger { appName: config.appName ?? 'App', level: config.level ?? 'info', silent: config.silent ?? false, + json: config.json ?? false, context: config.context, }; if (config.silent) { this.transports = []; } else { - const builtIn = new BrowserConsoleTransport({ json: config.json ?? false }); + const builtIn = new BrowserConsoleTransport({ json: this.config.json }); this.transports = [builtIn, ...(config.transports ?? [])]; } } @@ -441,14 +444,14 @@ export class BrowserLogger { * ``` */ child(extraContext: Record): BrowserLogger { - // Exclude 'context' string label from data fields (used for identification only) - const { _, ...rest } = extraContext; - // _context (string label) is intentionally unused — it's stripped from the data payload + // Every key, including `context`, is kept and merged over the parent's + // context, as the example above shows. return new BrowserLogger({ appName: this.config.appName, level: this.config.level, silent: this.config.silent, - context: { ...this.config.context, ...rest }, + json: this.config.json, + context: { ...this.config.context, ...extraContext }, // Reuse existing transport list transports: this.transports.filter((t) => t.name !== 'browser-console'), }); From 6ebbef3dc3faecfbd3d644e07d313543b6a32f1c Mon Sep 17 00:00:00 2001 From: Sumeet Jha <109774891+O1sumitkumar@users.noreply.github.com> Date: Sun, 18 Oct 2026 09:22:52 +0530 Subject: [PATCH 2/2] test(browser): cover child() json and context inheritance --- src/__tests__/browser.test.ts | 62 +++++++++++++++++++++++++++++++++-- 1 file changed, 60 insertions(+), 2 deletions(-) diff --git a/src/__tests__/browser.test.ts b/src/__tests__/browser.test.ts index 40100f6..27215d2 100644 --- a/src/__tests__/browser.test.ts +++ b/src/__tests__/browser.test.ts @@ -1,17 +1,20 @@ /** - * Tests for the browser logger's remote transport. + * Tests for the browser logger and its remote transport. * * Regressions: * - flush() spliced only ONE batchSize chunk, leaving a tail when the batch * exceeded batchSize. It must drain the whole batch. * - destroy() cleared the timer but did NOT flush, losing buffered logs on page * unload / teardown. It must flush remaining entries. + * - BrowserLogger.child() dropped the parent's `json` option, so children of a + * JSON logger printed pretty lines, and it stripped a key named `_` instead + * of keeping every context field. * * fetch is mocked so no real network call is made. */ import type { BrowserLogEntry } from '../browser'; -import { BrowserRemoteTransport } from '../browser'; +import { BrowserLogger, BrowserRemoteTransport } from '../browser'; function makeEntry(i: number): BrowserLogEntry { return { timestamp: '2026-01-01T00:00:00.000Z', level: 'info', appName: 'a', message: `b-${i}` }; @@ -166,3 +169,58 @@ describe('BrowserRemoteTransport', () => { jest.useRealTimers(); }); }); + +describe('BrowserLogger.child()', () => { + let logSpy: jest.SpyInstance; + + beforeEach(() => { + logSpy = jest.spyOn(console, 'log').mockImplementation(() => {}); + }); + + afterEach(() => { + logSpy.mockRestore(); + }); + + function lastJsonEntry(): BrowserLogEntry { + const calls = logSpy.mock.calls; + return JSON.parse(calls[calls.length - 1]![0] as string) as BrowserLogEntry; + } + + it('keeps json output for a child of a json: true logger', () => { + const parent = new BrowserLogger({ appName: 'app', json: true }); + const child = parent.child({ requestId: 'r-1' }); + + child.info('hello'); + + expect(logSpy).toHaveBeenCalledTimes(1); + const entry = lastJsonEntry(); + expect(entry.message).toBe('hello'); + expect(entry.payload).toEqual({ requestId: 'r-1' }); + }); + + it('merges the parent context with the new fields, including context', () => { + const parent = new BrowserLogger({ + appName: 'app', + json: true, + context: { service: 'checkout' }, + }); + const child = parent.child({ context: 'AuthService', userId: '42' }); + + child.info('Login succeeded'); + + expect(lastJsonEntry().payload).toEqual({ + service: 'checkout', + context: 'AuthService', + userId: '42', + }); + }); + + it('keeps both levels of context in a grandchild', () => { + const parent = new BrowserLogger({ appName: 'app', json: true }); + const grandchild = parent.child({ requestId: 'r-1' }).child({ userId: '42' }); + + grandchild.info('nested'); + + expect(lastJsonEntry().payload).toEqual({ requestId: 'r-1', userId: '42' }); + }); +});