fix(observability): sample native runs independently of triggers (#16285)

Start native background run traces under the operator's root sampling policy and link them to valid trigger contexts. Preserve child parenting and silent unconfigured tracing.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip authored and GitHub committed 2026-10-14 09:49:08 -05:00
1 parent f69c6a8a35
commit d4892d4761
3 files changed
+85 -1

No files matched your search

@@ -0,0 +1,75 @@
import { context, trace, TraceFlags } from "@opentelemetry/api";
import { AsyncLocalStorageContextManager } from "@opentelemetry/context-async-hooks";
import { AlwaysOffSampler, ParentBasedSampler, TracerProvider, InMemorySpanExporter, SimpleSpanProcessor } from "@opentelemetry/sdk-trace";
import { expect, it, vi } from "vitest";
import { createNativeRunTrace } from "./native-run-trace.js";
it.each([TraceFlags.NONE, TraceFlags.SAMPLED])("exports a complete native run independently of trigger flags %i", async traceFlags => {
const manager = new AsyncLocalStorageContextManager().enable();
const exporter = new InMemorySpanExporter();
const provider = new TracerProvider({ spanProcessors: [new SimpleSpanProcessor({ exporter })] });
expect(context.setGlobalContextManager(manager)).toBe(true);
expect(trace.setGlobalTracerProvider(provider)).toBe(true);
const trigger = {
traceId: "11111111111111111111111111111111", spanId: "2222222222222222", traceFlags, isRemote: true,
};
try {
await context.with(trace.setSpanContext(context.active(), trigger), async () => {
const native = createNativeRunTrace({ runId: "diagnostic-native-run" });
await native.measure("native.session.execute", async () => undefined);
await native.finish("ok");
expect(trace.getActiveSpan()?.spanContext()).toEqual(trigger);
});
await provider.forceFlush();
const spans = exporter.getFinishedSpans();
expect(spans.map(span => span.name).sort()).toEqual(["native.session.execute", "task.provider_session"]);
const root = spans.find(span => span.name === "task.provider_session")!;
const child = spans.find(span => span.name === "native.session.execute")!;
expect(root.parentSpanContext).toBeUndefined();
expect(root.links.map(link => link.context)).toEqual([trigger]);
expect(root.spanContext().traceId).not.toBe(trigger.traceId);
expect(child.parentSpanContext?.spanId).toBe(root.spanContext().spanId);
expect(child.spanContext().traceId).toBe(root.spanContext().traceId);
} finally {
trace.disable(); context.disable(); manager.disable(); await provider.shutdown();
}
});
it("honors an operator who disables root sampling despite a sampled trigger", async () => {
const manager = new AsyncLocalStorageContextManager().enable();
const exporter = new InMemorySpanExporter();
const provider = new TracerProvider({
sampler: new ParentBasedSampler({ root: new AlwaysOffSampler() }),
spanProcessors: [new SimpleSpanProcessor({ exporter })],
});
expect(context.setGlobalContextManager(manager)).toBe(true);
expect(trace.setGlobalTracerProvider(provider)).toBe(true);
try {
await context.with(trace.setSpanContext(context.active(), {
traceId: "11111111111111111111111111111111", spanId: "2222222222222222", traceFlags: TraceFlags.SAMPLED, isRemote: true,
}), async () => {
const native = createNativeRunTrace({ runId: "disabled-native-run" });
await native.measure("native.session.execute", async () => undefined);
await native.finish("ok");
});
await provider.forceFlush();
expect(exporter.getFinishedSpans()).toEqual([]);
} finally {
trace.disable(); context.disable(); manager.disable(); await provider.shutdown();
}
});
it("stays silent without a tracer provider", async () => {
trace.disable(); context.disable();
const methods = ["log", "info", "warn", "error", "debug"] as const;
const spies = methods.map(method => vi.spyOn(console, method));
try {
const native = createNativeRunTrace({ runId: "unconfigured-native-run" });
const value = {};
await expect(native.measure("native.session.execute", async () => value)).resolves.toBe(value);
const error = new Error("original failure");
await expect(native.measure("native.session.execute", async () => { throw error; })).rejects.toBe(error);
await native.finish("ok");
for (const spy of spies) expect(spy).not.toHaveBeenCalled();
} finally { for (const spy of spies) spy.mockRestore(); }
});
@@ -1,5 +1,6 @@
import { AsyncLocalStorage } from "node:async_hooks";
import { createHash } from "node:crypto";
import { isSpanContextValid, ROOT_CONTEXT, trace } from "@opentelemetry/api";
import { runWithRuntimeParent } from "@paperclipai/adapter-utils/acpx-engine/startup-timing";
import type { AdapterRuntimeEvent } from "../../adapters/index.js";
@@ -210,6 +211,7 @@ export function createNativeRunTrace(input: {
const runIdHash = hashedId(input.runId);
let rootSpan: SpanHandle;
try {
const trigger = trace.getActiveSpan()?.spanContext();
rootSpan = tracing.tracer.startSpan("task.provider_session", {
startTime: traceStartedAtMs,
attributes: {
@@ -218,7 +220,8 @@ export function createNativeRunTrace(input: {
"paperclip.task.run.trace_schema_version":
NATIVE_RUN_TRACE_SCHEMA_VERSION,
},
});
...(trigger && isSpanContextValid(trigger) ? { links: [{ context: trigger }] } : {}),
}, ROOT_CONTEXT);
} catch {
rootSpan = NOOP_SPAN;
}