UNPKG

agentcrumbs

Version:
226 lines 7.27 kB
import { NOOP } from "./noop.js"; import { isNamespaceEnabled, getCollectorUrl, getFormat, getApp } from "./env.js"; import { getContext, runWithContext } from "./context.js"; import { ConsoleSink } from "./sinks/console.js"; import { HttpSink } from "./sinks/socket.js"; const globalSinks = []; let sinksInitialized = false; function ensureSinks() { if (sinksInitialized) return; sinksInitialized = true; const url = getCollectorUrl(); globalSinks.push(new HttpSink(url)); const format = getFormat(); if (format === "json") { globalSinks.push({ write(crumb) { console.debug(JSON.stringify(crumb)); }, }); } else { globalSinks.push(new ConsoleSink()); } } export function addSink(sink) { globalSinks.push(sink); sinksInitialized = true; } export function removeSink(sink) { const idx = globalSinks.indexOf(sink); if (idx !== -1) globalSinks.splice(idx, 1); } /** Reset sinks — for testing */ export function resetSinks() { globalSinks.length = 0; sinksInitialized = false; } function emit(crumb) { ensureSinks(); for (const sink of globalSinks) { try { sink.write(crumb); } catch { // Never let a sink error affect the application } } } function createTrailFunction(namespace, parentCtx) { let lastTime = performance.now(); const timers = new Map(); function makeCrumb(msg, type, data, options, overrides) { const now = performance.now(); const dt = now - lastTime; lastTime = now; const asyncCtx = getContext(); const crumb = { app: getApp(), ts: new Date().toISOString(), ns: namespace, msg, type, dt: Math.round(dt * 100) / 100, pid: 0, ...overrides, }; if (data !== undefined) crumb.data = data; const mergedCtx = { ...parentCtx, ...asyncCtx?.contextData, }; if (Object.keys(mergedCtx).length > 0) crumb.ctx = mergedCtx; if (!crumb.traceId && asyncCtx?.traceId) crumb.traceId = asyncCtx.traceId; if (!crumb.depth && asyncCtx?.depth) crumb.depth = asyncCtx.depth; const sid = asyncCtx?.sessionId; if (sid) crumb.sid = sid; if (options?.tags && options.tags.length > 0) crumb.tags = options.tags; return crumb; } const fn = function trailFn(msg, data, options) { emit(makeCrumb(msg, "crumb", data, options)); }; fn.enabled = true; fn.namespace = namespace; fn.child = (ctx) => { return createTrailFunction(namespace, { ...parentCtx, ...ctx }); }; fn.scope = (name, scopeFn) => { const traceId = crypto.randomUUID().slice(0, 8); const asyncCtx = getContext(); const depth = (asyncCtx?.depth ?? 0) + 1; const newCtx = { namespace, contextData: { ...parentCtx, ...asyncCtx?.contextData }, traceId, depth, sessionId: asyncCtx?.sessionId, }; emit(makeCrumb(name, "scope:enter", undefined, undefined, { traceId, depth })); const startTime = performance.now(); const childTrail = createTrailFunction(namespace, newCtx.contextData); const finish = (error) => { const duration = Math.round((performance.now() - startTime) * 100) / 100; if (error) { const errorData = error instanceof Error ? { message: error.message, stack: error.stack, name: error.name } : { value: error }; emit(makeCrumb(name, "scope:error", { ...errorData, duration }, undefined, { traceId, depth, })); } else { emit(makeCrumb(name, "scope:exit", { duration }, undefined, { traceId, depth, })); } }; try { const result = runWithContext(newCtx, () => scopeFn({ crumb: childTrail, traceId })); if (result instanceof Promise) { return result.then((val) => { finish(); return val; }, (err) => { finish(err); throw err; }); } finish(); return result; } catch (err) { finish(err); throw err; } }; // eslint-disable-next-line @typescript-eslint/no-explicit-any fn.wrap = (name, wrappedFn) => { return ((...args) => { return fn.scope(name, () => wrappedFn(...args)); }); }; fn.time = (label) => { timers.set(label, performance.now()); }; fn.timeEnd = (label, data) => { const start = timers.get(label); if (start === undefined) return; timers.delete(label); const duration = Math.round((performance.now() - start) * 100) / 100; emit(makeCrumb(label, "time", { ...(data ?? {}), duration })); }; fn.snapshot = (label, obj) => { let cloned; try { cloned = structuredClone(obj); } catch { cloned = obj; } emit(makeCrumb(label, "snapshot", cloned)); }; fn.assert = (condition, msg) => { if (!condition) { emit(makeCrumb(msg, "assert", { passed: false })); } }; fn.session = ((name, sessionFn) => { const id = crypto.randomUUID().slice(0, 8); const asyncCtx = getContext(); const sessionCtx = { namespace, contextData: { ...parentCtx, ...asyncCtx?.contextData }, traceId: asyncCtx?.traceId ?? "", depth: asyncCtx?.depth ?? 0, sessionId: id, }; emit(makeCrumb(name, "session:start", undefined, undefined, { sid: id })); const session = { id, name, crumb: (msg, data, options) => { runWithContext(sessionCtx, () => { emit(makeCrumb(msg, "crumb", data, options, { sid: id })); }); }, end: () => { emit(makeCrumb(name, "session:end", undefined, undefined, { sid: id })); }, }; if (typeof sessionFn === "function") { const result = runWithContext(sessionCtx, () => sessionFn(session)); if (result instanceof Promise) { return result.then((val) => { session.end(); return val; }, (err) => { session.end(); throw err; }); } session.end(); return result; } return session; }); return fn; } export function trail(namespace) { if (!isNamespaceEnabled(namespace)) { return NOOP; } return createTrailFunction(namespace); } //# sourceMappingURL=trail-browser.mjs.map