agentcrumbs
Version:
Debug mode for any agent.
226 lines • 7.27 kB
JavaScript
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