diff --git a/packages/lib/src/utils.ts b/packages/lib/src/utils.ts index 48880fbb..7d908667 100644 --- a/packages/lib/src/utils.ts +++ b/packages/lib/src/utils.ts @@ -62,7 +62,7 @@ export function getModulePath(): string | undefined { */ const wrappedFunctionPath = stack.find((call) => { - if (call.name === "__decorateClass") return true; + if (call.name?.includes("__decorate")) return true; })?.file ?? stack[2]?.file; const containsFileProtocol = wrappedFunctionPath.includes("file://"); diff --git a/packages/lib/src/wrappers.ts b/packages/lib/src/wrappers.ts index c9db6e02..51cfb527 100644 --- a/packages/lib/src/wrappers.ts +++ b/packages/lib/src/wrappers.ts @@ -22,7 +22,7 @@ if (typeof window === "undefined") { // Function Wrapper // This seems to be the preferred way for defining functions in TypeScript // rome-ignore lint/suspicious/noExplicitAny: -type FunctionSig = (...args: any[]) => any; +export type FunctionSig = (...args: any[]) => any; type AnyFunction = ( ...params: Parameters @@ -36,7 +36,7 @@ type AnyFunction = ( breaks the language server plugin */ interface AutometricsWrapper> extends AnyFunction {} -export type AutometricsOptions = { +export type AutometricsOptions = { /** * Name of your function. Only necessary if using the decorator/wrapper on the * client side where builds get minified. @@ -59,8 +59,41 @@ export type AutometricsOptions = { * handler that passes requests off to other functions. (default: `false`) */ trackConcurrency?: boolean; + /** + * A custom callback function that determines whether a function return should + * be considered an error by Autometrics. This may be most useful in + * top-level functions such as the HTTP handler which would catch any errors + * thrown called from inside the handler. + * + * @example + * ```typescript + * async function createUser(payload: User) { + * // ... + * } + * + * // This will record an error if the handler response status is 4xx or 5xx + * const recordErrorIf = (res) => res.status >= 400; + * + * app.post("/users", autometrics({ recordErrorIf }, createUser) + * ``` + */ + recordErrorIf?: ReportErrorCondition; + /** + * A custom callback function that determines whether a function result + * should be considered a success (regardless if it threw an error). This + * may be most useful when you want to ignore certain errors that are thrown + * by the function. + * + */ + recordSuccessIf?: ReportSuccessCondition; }; +export type ReportErrorCondition = ( + result: Awaited>, +) => boolean; + +export type ReportSuccessCondition = (result: Error) => boolean; + /** * Autometrics wrapper for **functions** (requests handlers or database methods) * that automatically instruments the wrapped function with OpenTelemetry metrics. @@ -118,7 +151,7 @@ export type AutometricsOptions = { * */ export function autometrics( - functionOrOptions: F | AutometricsOptions, + functionOrOptions: F | AutometricsOptions, fnInput?: F, ): AutometricsWrapper { let functionName: string; @@ -126,6 +159,8 @@ export function autometrics( let fn: F; let objective: Objective | undefined; let trackConcurrency = false; + let recordErrorIf: ReportErrorCondition | undefined; + let recordSuccessIf: ReportSuccessCondition | undefined; if (typeof functionOrOptions === "function") { fn = functionOrOptions; @@ -150,6 +185,14 @@ export function autometrics( if ("trackConcurrency" in functionOrOptions) { trackConcurrency = functionOrOptions.trackConcurrency; } + + if ("recordErrorIf" in functionOrOptions) { + recordErrorIf = functionOrOptions.recordErrorIf; + } + + if ("recordSuccessIf" in functionOrOptions) { + recordSuccessIf = functionOrOptions.recordSuccessIf; + } } if (!functionName) { @@ -256,25 +299,50 @@ export function autometrics( } }; + const recordSuccess = (returnValue: Awaited>) => { + try { + if (recordErrorIf?.(returnValue)) { + onError(); + } else { + onSuccess(); + } + } catch (callbackError) { + onSuccess(); + console.trace("Error in recordErrorIf function: ", callbackError); + } + }; + + const recordError = (error: Error) => { + try { + if (recordSuccessIf?.(error)) { + onSuccess(); + } else { + onError(); + } + } catch (callbackError) { + onError(); + console.trace("Error in recordSuccessIf function: ", callbackError); + } + }; + function instrumentedFunction() { try { - const result = fn.apply(this, params); - if (isPromise>(result)) { - return result - .then((res: Awaited>) => { - onSuccess(); - return res; + const returnValue: ReturnType = fn.apply(this, params); + if (isPromise>(returnValue)) { + return returnValue + .then((result: Awaited>) => { + recordSuccess(result); + return result; }) - .catch((err: unknown) => { - onError(); - throw err; + .catch((error: Error) => { + recordError(error); + throw error; }); } - - onSuccess(); - return result; + recordSuccess(returnValue); + return returnValue; } catch (error) { - onError(); + recordError(error); throw error; } } @@ -291,13 +359,13 @@ export function autometrics( } export type AutometricsClassDecoratorOptions = Omit< - AutometricsOptions, + AutometricsOptions, "functionName" >; -type AutometricsDecoratorOptions = T extends Function +type AutometricsDecoratorOptions = F extends FunctionSig ? AutometricsClassDecoratorOptions - : AutometricsOptions; + : AutometricsOptions; /** * Autometrics decorator that can be applied to either a class or class method @@ -394,7 +462,7 @@ export function Autometrics( * @param autometricsOptions */ export function getAutometricsMethodDecorator( - autometricsOptions?: AutometricsOptions, + autometricsOptions?: AutometricsOptions, ) { return function ( _target: Object,