diff --git a/.changeset/soft-tools-lay.md b/.changeset/soft-tools-lay.md new file mode 100644 index 0000000000..25b4a9a73b --- /dev/null +++ b/.changeset/soft-tools-lay.md @@ -0,0 +1,5 @@ +--- +'@shopify/hydrogen': patch +--- + +Custom loggers can return promises from their methods. Hydrogen will await for them after the current request is over but before the runtime instance ends. diff --git a/docs/framework/hydrogen-config.md b/docs/framework/hydrogen-config.md index 499faee71e..02d33c5f2b 100644 --- a/docs/framework/hydrogen-config.md +++ b/docs/framework/hydrogen-config.md @@ -204,7 +204,12 @@ export default defineConfig({ /* Overrides the default `log.trace` behavior. */ trace: (request, ...args) => console.log(request.url, ...args), /* Overrides the default `log.error` behavior. */ - error: (request, error) => myErrorTrackingService.send(error, {request}), + error: async (request, error) => { + console.error(error); + // Methods can return promises. Hydrogen won't block the current + // request but it will await for them before the runtime instance ends. + await myErrorTrackingService.send(request, error); + }, /* ... */ /* Logs the cache status of each stored entry: `PUT`, `HIT`, `MISS` or `STALE`. */ diff --git a/packages/hydrogen/src/utilities/log/__tests__/log.test.ts b/packages/hydrogen/src/utilities/log/__tests__/log.test.ts index 03302a51ee..21d09dd493 100644 --- a/packages/hydrogen/src/utilities/log/__tests__/log.test.ts +++ b/packages/hydrogen/src/utilities/log/__tests__/log.test.ts @@ -159,16 +159,35 @@ describe('log', () => { }); it('gets logger for a given context', () => { - const clog = getLoggerWithContext({some: 'data'}); + const clog = getLoggerWithContext({url: 'example.com'}); (clog as any)[method](`hydrogen: ${method}`); expect((mockLogger as any)[method]).toHaveBeenCalled(); expect(((mockLogger as any)[method] as any).mock.calls[0][0]).toEqual({ - some: 'data', + url: 'example.com', }); expect(((mockLogger as any)[method] as any).mock.calls[0][1]).toBe( `hydrogen: ${method}` ); }); + + it('marks async calls for waitUntil', () => { + const waitUntilPromises = [] as Array>; + + const clog = getLoggerWithContext({ + ctx: { + runtime: {waitUntil: (p: Promise) => waitUntilPromises.push(p)}, + } as unknown as ServerComponentRequest['ctx'], + }); + + (clog as any)[method]('no promise 1'); + (clog as any)[method]('no promise 2'); + expect(waitUntilPromises).toHaveLength(0); + + setLogger({[method]: async () => null}); + (clog as any)[method]('promise 1'); + (clog as any)[method]('promise 2'); + expect(waitUntilPromises).toHaveLength(2); + }); }); }); diff --git a/packages/hydrogen/src/utilities/log/log.ts b/packages/hydrogen/src/utilities/log/log.ts index dc72ec1b85..c480f869dc 100644 --- a/packages/hydrogen/src/utilities/log/log.ts +++ b/packages/hydrogen/src/utilities/log/log.ts @@ -9,12 +9,13 @@ import {parseUrl} from './utils'; * current request in progress. */ +type LoggerMethod = (...args: Array) => void | Promise; export interface Logger { - trace: (...args: Array) => void; - debug: (...args: Array) => void; - warn: (...args: Array) => void; - error: (...args: Array) => void; - fatal: (...args: Array) => void; + trace: LoggerMethod; + debug: LoggerMethod; + warn: LoggerMethod; + error: LoggerMethod; + fatal: LoggerMethod; options: () => LoggerOptions; } @@ -51,13 +52,26 @@ const defaultLogger: Logger = { let currentLogger = defaultLogger as Logger; -export function getLoggerWithContext(context: any): Logger { +function doLog( + method: keyof typeof defaultLogger, + request: Partial, + ...args: any[] +) { + const maybePromise = currentLogger[method](request, ...args); + if (maybePromise instanceof Promise) { + request?.ctx?.runtime?.waitUntil?.(maybePromise); + } +} + +export function getLoggerWithContext( + context: Partial +): Logger { return { - trace: (...args) => currentLogger.trace(context, ...args), - debug: (...args) => currentLogger.debug(context, ...args), - warn: (...args) => currentLogger.warn(context, ...args), - error: (...args) => currentLogger.error(context, ...args), - fatal: (...args) => currentLogger.fatal(context, ...args), + trace: (...args) => doLog('trace', context, ...args), + debug: (...args) => doLog('debug', context, ...args), + warn: (...args) => doLog('warn', context, ...args), + error: (...args) => doLog('error', context, ...args), + fatal: (...args) => doLog('fatal', context, ...args), options: () => currentLogger.options(), }; }