diff --git a/packages/middleware/__tests__/unit/integrations/helpers/getInitConfig.spec.ts b/packages/middleware/__tests__/unit/integrations/helpers/getInitConfig.spec.ts index 3ea4866c69..6df9e00e6e 100644 --- a/packages/middleware/__tests__/unit/integrations/helpers/getInitConfig.spec.ts +++ b/packages/middleware/__tests__/unit/integrations/helpers/getInitConfig.spec.ts @@ -41,7 +41,11 @@ describe("[getInitConfig]", () => { }); it("should return an empty object when apiClient is not defined", async () => { - const params = {} as unknown as LoadInitConfigProps; + const params = { + alokai: { + logger, + }, + } as unknown as LoadInitConfigProps; const result = await getInitConfig(params); expect(result).toEqual({}); diff --git a/packages/middleware/src/apiClientFactory/applyContextToApi.ts b/packages/middleware/src/apiClientFactory/applyContextToApi.ts index 731ac57d1c..d987371ec1 100644 --- a/packages/middleware/src/apiClientFactory/applyContextToApi.ts +++ b/packages/middleware/src/apiClientFactory/applyContextToApi.ts @@ -10,6 +10,7 @@ import { import { createExtendQuery } from "./createExtendQuery"; import { markExtensionNameHelpers } from "./markExtensionNameHelpers"; import { getLogger, injectMetadata } from "../logger"; +import { wrapFnWithErrorBoundary } from "./wrapFnWithErrorBoundary"; const nopBefore = ({ args }: BeforeCallParams): ARGS => args; const nopAfter = ({ @@ -48,32 +49,6 @@ function injectHandlerMetadata( }); } -async function injectHandlerErrorMetadata( - fn: Function, - args: any[], - context: CONTEXT, - callName: string -) { - try { - const response = await fn(...args); - return response; - } catch (err) { - const errorBoundary: LogScope = err.errorBoundary || { - type: "endpoint" as const, - functionName: callName, - integrationName: context.integrationTag, - }; - - if (!err.errorBoundary && markExtensionNameHelpers.has(fn)) { - errorBoundary.extensionName = markExtensionNameHelpers.get(fn); - } - - Object.assign(err, { errorBoundary }); - - throw err; - } -} - /** * Wraps api methods with context and hooks triggers */ @@ -94,27 +69,51 @@ const applyContextToApi = < (prev, [callName, fn]) => ({ ...prev, [callName]: (() => { - const newFn = async (...args: Parameters) => { + const logger = injectHandlerMetadata(context, fn, callName); + /** + * Raw endpoint's handler decorated with: + * - hooks from every extension (hooks.before contains merged beforeCall's, + * hooks.after contains merged afterCall's), + * - logger with metadata about scope of call (covers orchestrated parallely called endpoints), + * - support for building error boundary (covers orchestrated parallely called endpoints), + */ + const handler = async (...args: Parameters) => { const transformedArgs = await hooks.before({ callName, args, }); + const fnWithErrorBoundary = wrapFnWithErrorBoundary(fn, (err) => { + /** + * Handlers can call different handlers, so error could be already bubbling. + * That's why we check for presence of err.errorBoundary at first. + * + * ```ts + * async function myEndpoint(context) { + * const int = await context.getApiClient("some_integration"); + * return await int.api.throwError(); // Bubbling from other integration's endpoint + * } + * ``` + */ + const errorBoundary: LogScope = err.errorBoundary || { + type: "endpoint" as const, + functionName: callName, + integrationName: context.integrationTag, + ...(markExtensionNameHelpers.has(fn) + ? { extensionName: markExtensionNameHelpers.get(fn) } + : {}), + }; + + return errorBoundary; + }); const apiClientContext = { ...context, extendQuery: createExtendQuery(context), + logger, }; - const response = await injectHandlerErrorMetadata( - fn, - [ - { - ...apiClientContext, - logger: injectHandlerMetadata(context, fn, callName), - }, - ...transformedArgs, - ], - context, - callName + const response = await fnWithErrorBoundary( + apiClientContext, + ...transformedArgs ); const transformedResponse = await hooks.after({ @@ -125,13 +124,18 @@ const applyContextToApi = < return transformedResponse; }; + + /** + * Marks decorated handler with name of integration's extension + * from which original handler was defined, if any + */ if (markExtensionNameHelpers.has(fn)) { markExtensionNameHelpers.mark( - newFn, + handler, markExtensionNameHelpers.get(fn) ); } - return newFn; + return handler; })(), }), {} as any diff --git a/packages/middleware/src/apiClientFactory/index.ts b/packages/middleware/src/apiClientFactory/index.ts index 3dd2998754..88ef26cb86 100644 --- a/packages/middleware/src/apiClientFactory/index.ts +++ b/packages/middleware/src/apiClientFactory/index.ts @@ -16,6 +16,10 @@ import { } from "../types"; import { applyContextToApi } from "./applyContextToApi"; import { markExtensionNameHelpers } from "./markExtensionNameHelpers"; +import { + wrapFnWithErrorBoundary, + wrapFnWithErrorBoundarySync, +} from "./wrapFnWithErrorBoundary"; /** * Utility function faciliating building Logger with injected metadata about currently called hook @@ -46,32 +50,6 @@ function injectHookMetadata( })); } -async function injectHandlerErrorMetadata( - fn: Function, - args: any[], - integrationName: string, - hookName: LogScope["hookName"] -) { - try { - const response = await fn(...args); - return response; - } catch (err) { - const errorBoundary: LogScope = { - type: "endpoint" as const, - integrationName, - hookName, - }; - - if (!err.errorBoundary && markExtensionNameHelpers.has(fn)) { - errorBoundary.extensionName = markExtensionNameHelpers.get(fn); - } - - Object.assign(err, { errorBoundary }); - - throw err; - } -} - const apiClientFactory = < ALL_SETTINGS extends ApiClientConfig, ALL_FUNCTIONS extends ApiMethods @@ -110,27 +88,26 @@ const apiClientFactory = < hookName: "hooks", extensionName: name, }); - - try { - return { - ...hooks(this?.middleware?.req, this?.middleware?.res, { - logger: loggerWithMetadata, - }), - name, - }; - } catch (err) { - const errorBoundary: LogScope = { + const hooksWithErrorBoundary = wrapFnWithErrorBoundarySync( + hooks, + () => ({ type: "requestHook" as const, hookName: "hooks", extensionName: name, - // functionName: callName, todo: HOW? integrationName: this?.middleware?.integrationTag, - }; - - Object.assign(err, { errorBoundary }); - - throw err; - } + }) + ); + + return { + ...hooksWithErrorBoundary( + this?.middleware?.req, + this?.middleware?.res, + { + logger: loggerWithMetadata, + } + ), + name, + }; }) ); @@ -144,38 +121,37 @@ const apiClientFactory = < extensionName: extension.name, hookName: "beforeCreate", }); - try { - return await extension.beforeCreate({ - configuration: resolvedConfig, - logger: loggerWithMetadata, - }); - } catch (err) { - const errorBoundary: LogScope = { + const beforeCreateWithErrorBoundary = wrapFnWithErrorBoundary( + extension.beforeCreate, + () => ({ type: "requestHook" as const, hookName: "beforeCreate", extensionName: extension.name, - // functionName: callName, todo: HOW? integrationName: this?.middleware?.integrationTag, - }; - - Object.assign(err, { errorBoundary }); - - throw err; - } + }) + ); + return beforeCreateWithErrorBoundary({ + configuration: resolvedConfig, + logger: loggerWithMetadata, + }); }, Promise.resolve(config)); const loggerWithMetadata = injectHookMetadata(logger, { hookName: "onCreate", integrationName: this.middleware?.integrationTag, }); - // TODO: Wrap onCreate + const onCreateWithErrorBoundadry = wrapFnWithErrorBoundary( + factoryParams.onCreate, + () => ({ + type: "endpoint" as const, + hookName: "onCreate", + integrationName: this.middleware?.integrationTag, + }) + ); const settings = factoryParams.onCreate - ? await injectHandlerErrorMetadata( - factoryParams.onCreate, - [_config, { logger: loggerWithMetadata }], - this.middleware?.integrationTag, - "onCreate" - ) + ? await onCreateWithErrorBoundadry(_config, { + logger: loggerWithMetadata, + }) : { config, client: config.client }; settings.config = await lifecycles @@ -188,24 +164,21 @@ const apiClientFactory = < extensionName: extension.name, hookName: "afterCreate", }); - try { - return await extension.afterCreate({ - configuration: resolvedConfig, - logger: loggerWithMetadata, - }); - } catch (err) { - const errorBoundary: LogScope = { + + const afterCreateWithErrorBoundary = wrapFnWithErrorBoundary( + extension.afterCreate, + () => ({ type: "requestHook" as const, hookName: "afterCreate", extensionName: extension.name, - // functionName: callName, todo: HOW? integrationName: this?.middleware?.integrationTag, - }; - - Object.assign(err, { errorBoundary }); + }) + ); - throw err; - } + return afterCreateWithErrorBoundary({ + configuration: resolvedConfig, + logger: loggerWithMetadata, + }); }, Promise.resolve(settings.config)); const extensionHooks: ApplyingContextHooks = { @@ -223,26 +196,23 @@ const apiClientFactory = < hookName: "beforeCall", }); - try { - return extension.beforeCall({ - ...params, - configuration: resolvedSettings.config, - args: resolvedArgs, - logger: loggerWithMetadata, - }); - } catch (err) { - const errorBoundary: LogScope = { + const beforeCallWithErrorBoundary = wrapFnWithErrorBoundary( + extension.beforeCall, + () => ({ type: "requestHook" as const, hookName: "beforeCall", extensionName: extension.name, functionName: params.callName, integrationName: this?.middleware?.integrationTag, - }; - - Object.assign(err, { errorBoundary }); - - throw err; - } + }) + ); + + return beforeCallWithErrorBoundary({ + ...params, + configuration: resolvedSettings.config, + args: resolvedArgs, + logger: loggerWithMetadata, + }); }, Promise.resolve(params.args)); }, after: async (params) => { @@ -259,26 +229,23 @@ const apiClientFactory = < hookName: "afterCall", }); - try { - return extension.afterCall({ - ...params, - configuration: resolvedSettings.config, - response: resolvedResponse, - logger: loggerWithMetadata, - }); - } catch (err) { - const errorBoundary: LogScope = { + const afterCallWithErrorBoundary = wrapFnWithErrorBoundary( + extension.afterCall, + () => ({ type: "requestHook" as const, hookName: "afterCall", extensionName: extension.name, functionName: params.callName, integrationName: this?.middleware?.integrationTag, - }; - - Object.assign(err, { errorBoundary }); - - throw err; - } + }) + ); + + return afterCallWithErrorBoundary({ + ...params, + configuration: resolvedSettings.config, + response: resolvedResponse, + logger: loggerWithMetadata, + }); }, Promise.resolve(params.response)); }, }; @@ -317,16 +284,10 @@ const apiClientFactory = < } } - const integrationApi = applyContextToApi( - api, - // @ts-expect-error see above - context, - extensionHooks - ); + const integrationApi = applyContextToApi(api, context, extensionHooks); const sharedExtensionsApi = applyContextToApi( sharedExtensions, - // @ts-expect-error see above context, extensionHooks ); @@ -338,7 +299,6 @@ const apiClientFactory = < )) { namespacedApi[namespace] = applyContextToApi( extension, - // @ts-expect-error see above context, extensionHooks ); diff --git a/packages/middleware/src/apiClientFactory/wrapFnWithErrorBoundary.ts b/packages/middleware/src/apiClientFactory/wrapFnWithErrorBoundary.ts new file mode 100644 index 0000000000..27a8c6df7d --- /dev/null +++ b/packages/middleware/src/apiClientFactory/wrapFnWithErrorBoundary.ts @@ -0,0 +1,47 @@ +import { LogScope } from "../types"; + +type ErrorWithBoundary = Error & { errorBoundary: LogScope }; + +/** + * @param fn Function to be wraped. + * @param errorBoundaryProvider Factory function providing value of errorBoundary to be injected + * @returns Decorated provided async function with catch block extending error with + * "errorBoundary" property equal value derived from errorBoundaryProvider factory. + */ +export function wrapFnWithErrorBoundary( + fn: Function, + errorBoundaryProvider: (err: ErrorWithBoundary) => LogScope +) { + return async (...args: any[]) => { + try { + const response = await fn(...args); + return response; + } catch (err) { + Object.assign(err, { errorBoundary: errorBoundaryProvider(err) }); + + throw err; + } + }; +} + +/** + * @param fn Function to be wraped. + * @param errorBoundaryProvider Factory function providing value of errorBoundary to be injected + * @returns Decorated provided sync function with catch block extending error with + * "errorBoundary" property equal value derived from errorBoundaryProvider factory. + */ +export function wrapFnWithErrorBoundarySync( + fn: Function, + errorBoundaryProvider: (err: ErrorWithBoundary) => LogScope +) { + return (...args: any[]) => { + try { + const response = fn(...args); + return response; + } catch (err) { + Object.assign(err, { errorBoundary: errorBoundaryProvider(err) }); + + throw err; + } + }; +} diff --git a/packages/middleware/src/handlers/callApiFunction/index.ts b/packages/middleware/src/handlers/callApiFunction/index.ts index 5ac79a4b96..f4b34ea1e3 100644 --- a/packages/middleware/src/handlers/callApiFunction/index.ts +++ b/packages/middleware/src/handlers/callApiFunction/index.ts @@ -1,5 +1,5 @@ import type { Request, Response } from "express"; -import { injectMetadata, getLogger } from "../../logger"; +import { getLogger } from "../../logger"; export async function callApiFunction(req: Request, res: Response) { const { apiFunction, args, errorHandler } = res.locals; @@ -8,19 +8,21 @@ export async function callApiFunction(req: Request, res: Response) { const platformResponse = await apiFunction(...args); res.send(platformResponse); } catch (error) { - const apiFunctionFromExtension = !!res.locals.apiHandlerExtension; - const logger = injectMetadata(getLogger(res), (metadata) => ({ - ...metadata, + const logger = getLogger(res); + const additionalScope = res.locals.fnOrigin + ? { extensionName: res.locals.fnOrigin } + : {}; + const errorBoundary = error.errorBoundary + ? { errorBoundary: error.errorBoundary } + : {}; + + logger.error(error, { scope: { - ...metadata?.scope, - ...(apiFunctionFromExtension - ? { extensionName: res.locals.apiHandlerExtension } - : {}), type: "endpoint", + ...additionalScope, }, - ...(error.errorBoundary ? { errorBoundary: error.errorBoundary } : {}), - })); - logger.error(error); + ...errorBoundary, + }); errorHandler(error, req, res); } } diff --git a/packages/middleware/src/handlers/prepareApiFunction/getApiFunction.ts b/packages/middleware/src/handlers/prepareApiFunction/getApiFunction.ts index a6944d4391..50b443cd73 100644 --- a/packages/middleware/src/handlers/prepareApiFunction/getApiFunction.ts +++ b/packages/middleware/src/handlers/prepareApiFunction/getApiFunction.ts @@ -5,6 +5,8 @@ import { markExtensionNameHelpers } from "../../apiClientFactory/markExtensionNa * * @param apiClientPromise * @param reqParams + * + * @returns Tuple containing resolved function and name of extension it comes from if any */ export const getApiFunction = async ( apiClientPromise: Promise | any, diff --git a/packages/middleware/src/handlers/prepareApiFunction/index.ts b/packages/middleware/src/handlers/prepareApiFunction/index.ts index 6aa681aadd..62c8608877 100644 --- a/packages/middleware/src/handlers/prepareApiFunction/index.ts +++ b/packages/middleware/src/handlers/prepareApiFunction/index.ts @@ -1,6 +1,6 @@ import { IntegrationsLoaded, MiddlewareContext } from "../../types"; import { getApiFunction } from "./getApiFunction"; -import { injectMetadata, getLogger } from "../../logger"; +import { getLogger } from "../../logger"; export function prepareApiFunction( integrations: IntegrationsLoaded @@ -78,24 +78,22 @@ export function prepareApiFunction( // Pick the function from the namespaced if it exists, otherwise pick it from the shared integration try { - const [fn, apiHandlerExtension] = await getApiFunction( + const [fn, fnOrigin] = await getApiFunction( apiClientInstance, functionName, extensionName ); res.locals.apiFunction = fn; - res.locals.apiHandlerExtension = apiHandlerExtension; + res.locals.fnOrigin = fnOrigin; } catch (e) { if (e.errorBoundary) { - const logger = injectMetadata(getLogger(res), (metadata) => ({ - ...metadata, + const logger = getLogger(res); + logger.error(e, { scope: { - ...metadata?.scope, type: "endpoint", }, - ...(e.errorBoundary ? { errorBoundary: e.errorBoundary } : {}), - })); - logger.error(e); + errorBoundary: e.errorBoundary, + }); } res.status(404); res.send(e.message); diff --git a/packages/middleware/src/integrations/helpers/getInitConfig.ts b/packages/middleware/src/integrations/helpers/getInitConfig.ts index da7d8410f8..4b054293fd 100644 --- a/packages/middleware/src/integrations/helpers/getInitConfig.ts +++ b/packages/middleware/src/integrations/helpers/getInitConfig.ts @@ -1,4 +1,4 @@ -import { injectMetadata } from "../../logger"; +import { getLogger } from "../../logger"; import { isFunction } from "../../helpers"; import { LoadInitConfigProps, TObject } from "../../types"; @@ -8,26 +8,25 @@ export async function getInitConfig({ integration, alokai, }: LoadInitConfigProps): Promise { + const logger = getLogger(alokai); if (isFunction(apiClient?.init)) { try { - alokai.logger.debug(`- Integration: ${tag} init function Start!`); + logger.debug(`- Integration: ${tag} init function Start!`); const initConfig = await apiClient.init( integration.configuration, alokai ); - alokai.logger.debug(`- Integration: ${tag} init function Done!`); + logger.debug(`- Integration: ${tag} init function Done!`); return initConfig; } catch (error) { - const logger = injectMetadata(alokai.logger, (metadata) => ({ - ...metadata, + logger.error(error, { errorBoundary: { integrationName: tag, type: "bootstrapHook", hookName: "init", }, - })); - logger.error(error); + }); throw Error( `Error during executing init function in ${tag} integration. Error message: ${error}` ); diff --git a/packages/middleware/src/integrations/registerIntegrations.ts b/packages/middleware/src/integrations/registerIntegrations.ts index a72618651b..39ab6fa0ae 100644 --- a/packages/middleware/src/integrations/registerIntegrations.ts +++ b/packages/middleware/src/integrations/registerIntegrations.ts @@ -51,16 +51,14 @@ async function triggerExtendAppHook( try { await extendApp({ app, configuration, logger: loggerWithMetadata }); } catch (e) { - const logger = injectMetadata(loggerWithMetadata, (metadata) => ({ - ...metadata, + loggerWithMetadata.error(e, { errorBoundary: { integrationName: tag, extensionName: name, type: "bootstrapHook", hookName: "extendApp", }, - })); - logger.error(e); + }); throw e; } }