neatlogs 1.1.5 → 1.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/README.md +16 -27
  2. package/dist/ai-sdk.cjs +160 -33
  3. package/dist/ai-sdk.cjs.map +1 -1
  4. package/dist/ai-sdk.mjs +163 -32
  5. package/dist/ai-sdk.mjs.map +1 -1
  6. package/dist/anthropic.cjs +65 -29
  7. package/dist/anthropic.cjs.map +1 -1
  8. package/dist/anthropic.mjs +73 -36
  9. package/dist/anthropic.mjs.map +1 -1
  10. package/dist/azure-openai.cjs +65 -29
  11. package/dist/azure-openai.cjs.map +1 -1
  12. package/dist/azure-openai.mjs +73 -36
  13. package/dist/azure-openai.mjs.map +1 -1
  14. package/dist/bedrock.cjs +65 -29
  15. package/dist/bedrock.cjs.map +1 -1
  16. package/dist/bedrock.mjs +73 -36
  17. package/dist/bedrock.mjs.map +1 -1
  18. package/dist/claude-agent-sdk.cjs +55 -15
  19. package/dist/claude-agent-sdk.cjs.map +1 -1
  20. package/dist/claude-agent-sdk.mjs +52 -8
  21. package/dist/claude-agent-sdk.mjs.map +1 -1
  22. package/dist/google-genai.cjs +69 -35
  23. package/dist/google-genai.cjs.map +1 -1
  24. package/dist/google-genai.mjs +77 -42
  25. package/dist/google-genai.mjs.map +1 -1
  26. package/dist/index.cjs +969 -661
  27. package/dist/index.cjs.map +1 -1
  28. package/dist/index.d.ts +44 -2
  29. package/dist/index.mjs +899 -588
  30. package/dist/index.mjs.map +1 -1
  31. package/dist/langchain.cjs +46 -12
  32. package/dist/langchain.cjs.map +1 -1
  33. package/dist/langchain.mjs +54 -19
  34. package/dist/langchain.mjs.map +1 -1
  35. package/dist/mastra-wrap.cjs +318 -46
  36. package/dist/mastra-wrap.cjs.map +1 -1
  37. package/dist/mastra-wrap.mjs +319 -41
  38. package/dist/mastra-wrap.mjs.map +1 -1
  39. package/dist/openai-agents.cjs +45 -9
  40. package/dist/openai-agents.cjs.map +1 -1
  41. package/dist/openai-agents.mjs +45 -5
  42. package/dist/openai-agents.mjs.map +1 -1
  43. package/dist/openai.cjs +65 -29
  44. package/dist/openai.cjs.map +1 -1
  45. package/dist/openai.mjs +73 -36
  46. package/dist/openai.mjs.map +1 -1
  47. package/dist/opencode-plugin.cjs +1 -1
  48. package/dist/opencode-plugin.cjs.map +1 -1
  49. package/dist/opencode-plugin.mjs +1 -1
  50. package/dist/opencode-plugin.mjs.map +1 -1
  51. package/dist/openrouter-agent.cjs +48 -12
  52. package/dist/openrouter-agent.cjs.map +1 -1
  53. package/dist/openrouter-agent.mjs +56 -19
  54. package/dist/openrouter-agent.mjs.map +1 -1
  55. package/dist/pi-agent.cjs +45 -9
  56. package/dist/pi-agent.cjs.map +1 -1
  57. package/dist/pi-agent.mjs +45 -6
  58. package/dist/pi-agent.mjs.map +1 -1
  59. package/dist/strands.cjs +4 -136
  60. package/dist/strands.cjs.map +1 -1
  61. package/dist/strands.d.ts +5 -24
  62. package/dist/strands.mjs +4 -136
  63. package/dist/strands.mjs.map +1 -1
  64. package/dist/vertex-ai.cjs +69 -35
  65. package/dist/vertex-ai.cjs.map +1 -1
  66. package/dist/vertex-ai.mjs +77 -42
  67. package/dist/vertex-ai.mjs.map +1 -1
  68. package/package.json +2 -7
package/README.md CHANGED
@@ -138,7 +138,9 @@ await init({
138
138
  | `metadata` | `Record<string, any>` | — | Custom metadata attached to all spans. |
139
139
  | `debug` | `boolean` | `false` | Enable debug logging. |
140
140
  | `disableExport` | `boolean` | `false` | Disable export to Neatlogs backend. |
141
- | `instrumentations` | `string[]` | — | Libraries to auto-instrument (e.g., `['openai']`). |
141
+ | `instrumentations` | `string[]` | — | Legacy manager path. Instrumentors that depend on global OTel context are rejected; use explicit wrappers below. |
142
+ | `tracerProvider` | `BasicTracerProvider` | Private SDK provider | Optional caller-owned private provider. It is never registered globally or shut down by Neatlogs. |
143
+ | `registerShutdownHandlers` | `boolean` | `true` for SDK-owned provider | Register process exit/signal handlers. Set `false` when the host application owns shutdown. |
142
144
  | `mask` | `MaskFunction` | — | Global mask function applied to all spans. |
143
145
  | `sampleRate` | `number` | `1.0` | Sampling rate (0.0 to 1.0). |
144
146
  | `captureLogs` | `boolean` | `false` | Capture log records via OTel LoggerProvider. |
@@ -431,26 +433,13 @@ registerCrewaiTask('research-task', 'Research the latest AI developments');
431
433
 
432
434
  ## Supported Instrumentations
433
435
 
434
- ### Auto-Instrumented (via OpenInference)
436
+ ### Isolation policy
435
437
 
436
- These libraries are automatically instrumented when listed in `instrumentations`:
437
-
438
- | Library | Package | Instrumentation |
439
- |---------|---------|-----------------|
440
- | `openai` | `openai` | `@arizeai/openinference-instrumentation-openai` |
441
- | `anthropic` | `@anthropic-ai/sdk` | `@arizeai/openinference-instrumentation-anthropic` |
442
- | `bedrock` | `@aws-sdk/client-bedrock-runtime` | `@arizeai/openinference-instrumentation-bedrock` |
443
- | `langchain` | `@langchain/core` | `@arizeai/openinference-instrumentation-langchain` |
444
- | `mcp` | `@modelcontextprotocol/sdk` | `@arizeai/openinference-instrumentation-mcp` |
445
- | `beeai` | `beeai-framework` | `@arizeai/openinference-instrumentation-beeai` |
446
- | `claude_agent_sdk` | `@anthropic-ai/claude-agent-sdk` | `@arizeai/openinference-instrumentation-claude-agent-sdk` |
447
-
448
- ### Custom Instrumentors (built into neatlogs)
449
-
450
- | Library | Package | Notes |
451
- |---------|---------|-------|
452
- | `google_genai` | `@google/genai` | Custom neatlogs instrumentor |
453
- | `crewai` | `crewai` | Custom neatlogs instrumentor; auto-loads `litellm` |
438
+ Neatlogs always runs on a private provider and private async context. Third-party
439
+ auto-instrumentors that call the global OpenTelemetry context API cannot provide
440
+ bidirectional isolation, so the manager rejects them at initialization before
441
+ creating any provider state. Use the explicit provider/framework wrappers
442
+ instead.
454
443
 
455
444
  ### Registry Entries (not yet instrumented in TypeScript)
456
445
 
@@ -460,21 +449,21 @@ The following libraries are registered in the instrumentation registry for futur
460
449
 
461
450
  ## Framework Integrations
462
451
 
463
- For frameworks that don't fit the auto-instrument-on-init pattern, neatlogs ships dedicated companion packages. Install only the ones you need:
452
+ For frameworks that don't fit the auto-instrument-on-init pattern, use the SDK's explicit wrappers. These wrappers use Neatlogs' private context and remain isolated from other tracing SDKs:
464
453
 
465
- | Framework | Package | Helper |
466
- |-----------|---------|--------|
467
- | Mastra (`@mastra/core`) | `@neatlogs/instrumentation-mastra` | `getMastraObservability()` pass to `new Mastra({ observability })` |
468
- | Vercel AI SDK (`ai`) | `@neatlogs/instrumentation-ai-sdk` | `getAISDKWrapper()` wraps `generateText` / `streamText` / `generateObject` / `streamObject` |
454
+ | Framework | Helper |
455
+ |-----------|--------|
456
+ | Mastra (`@mastra/core`) | `wrapMastra()` from `neatlogs/mastra` |
457
+ | Vercel AI SDK (`ai`) | `wrapAISDK()` from `neatlogs/ai` |
469
458
 
470
459
  ```typescript
471
460
  // Vercel AI SDK
472
- import { init, getAISDKWrapper, shutdown } from 'neatlogs';
461
+ import { init, shutdown } from 'neatlogs';
462
+ import { wrapAISDK } from 'neatlogs/ai';
473
463
  import * as ai from 'ai';
474
464
  import { openai } from '@ai-sdk/openai';
475
465
 
476
466
  await init({ apiKey: process.env.NEATLOGS_API_KEY });
477
- const wrapAISDK = await getAISDKWrapper();
478
467
  const { generateText } = wrapAISDK(ai);
479
468
 
480
469
  const { text } = await generateText({
package/dist/ai-sdk.cjs CHANGED
@@ -24,7 +24,74 @@ __export(ai_sdk_exports, {
24
24
  wrapAISDK: () => wrapAISDK
25
25
  });
26
26
  module.exports = __toCommonJS(ai_sdk_exports);
27
+ var import_api2 = require("@opentelemetry/api");
28
+
29
+ // src/core/provider.ts
30
+ var import_node_async_hooks = require("async_hooks");
27
31
  var import_api = require("@opentelemetry/api");
32
+ var PRIVATE_SPAN_STORAGE_KEY = /* @__PURE__ */ Symbol.for(
33
+ "neatlogs.private_span_async_local_storage"
34
+ );
35
+ var PRIVATE_PROVIDER_STATE_KEY = /* @__PURE__ */ Symbol.for(
36
+ "neatlogs.private_provider_state"
37
+ );
38
+ var neatlogsGlobal = globalThis;
39
+ var privateContextStorage = neatlogsGlobal[PRIVATE_SPAN_STORAGE_KEY] ?? (neatlogsGlobal[PRIVATE_SPAN_STORAGE_KEY] = new import_node_async_hooks.AsyncLocalStorage());
40
+ var providerState = neatlogsGlobal[PRIVATE_PROVIDER_STATE_KEY] ?? (neatlogsGlobal[PRIVATE_PROVIDER_STATE_KEY] = {
41
+ provider: null
42
+ });
43
+ var preInitTracer = {
44
+ startSpan() {
45
+ return import_api.trace.wrapSpanContext(import_api.INVALID_SPAN_CONTEXT);
46
+ },
47
+ startActiveSpan(_name, arg2, arg3, arg4) {
48
+ const fn = typeof arg2 === "function" ? arg2 : typeof arg3 === "function" ? arg3 : arg4;
49
+ return fn(import_api.trace.wrapSpanContext(import_api.INVALID_SPAN_CONTEXT));
50
+ }
51
+ };
52
+ function getNeatlogsTracer(name) {
53
+ return providerState.provider?.getTracer(name) ?? preInitTracer;
54
+ }
55
+ function isolateTracer(tracer) {
56
+ const facade = {
57
+ startSpan(name, options, context) {
58
+ const parent = context ?? getNeatlogsActiveContext();
59
+ return tracer.startSpan(name, options, parent);
60
+ },
61
+ startActiveSpan(name, arg2, arg3, arg4) {
62
+ let options;
63
+ let context;
64
+ let fn;
65
+ if (typeof arg2 === "function") {
66
+ fn = arg2;
67
+ } else if (typeof arg3 === "function") {
68
+ options = arg2;
69
+ fn = arg3;
70
+ } else {
71
+ options = arg2;
72
+ context = arg3;
73
+ fn = arg4;
74
+ }
75
+ const parent = context ?? getNeatlogsActiveContext();
76
+ const span = tracer.startSpan(name, options, parent);
77
+ return withNeatlogsSpan(span, () => fn(span), parent);
78
+ }
79
+ };
80
+ return facade;
81
+ }
82
+ function getNeatlogsActiveContext() {
83
+ return privateContextStorage.getStore() ?? import_api.ROOT_CONTEXT;
84
+ }
85
+ function getNeatlogsParentContext(baseContext) {
86
+ return baseContext ?? getNeatlogsActiveContext();
87
+ }
88
+ function withNeatlogsSpan(span, fn, baseContext) {
89
+ const base = baseContext ?? getNeatlogsActiveContext();
90
+ const ctx = import_api.trace.setSpan(base, span);
91
+ return privateContextStorage.run(ctx, fn);
92
+ }
93
+
94
+ // src/ai-sdk.ts
28
95
  var TRACER_NAME = "neatlogs.ai-sdk";
29
96
  function createAITelemetry(opts = {}) {
30
97
  const userMeta = opts.metadata ?? {};
@@ -32,7 +99,11 @@ function createAITelemetry(opts = {}) {
32
99
  isEnabled: true,
33
100
  recordInputs: true,
34
101
  recordOutputs: true,
35
- tracer: import_api.trace.getTracer(TRACER_NAME),
102
+ // Hand the AI SDK an isolation-aware tracer: it calls startActiveSpan()
103
+ // internally, which would otherwise parent its native spans from the foreign
104
+ // global context AND push them onto it (so a co-tenant's next span inherits
105
+ // ours). The facade routes both through the private Neatlogs context.
106
+ tracer: isolateTracer(getNeatlogsTracer(TRACER_NAME)),
36
107
  metadata: { ...userMeta, neatlogsWrapped: true }
37
108
  };
38
109
  }
@@ -70,12 +141,34 @@ function setOutputValue(span, result) {
70
141
  }
71
142
  return;
72
143
  }
144
+ if ("object" in r && "finishReason" in r) {
145
+ if (r.object !== void 0) {
146
+ span.setAttribute("output.value", safeStringify(r.object));
147
+ }
148
+ if (r.finishReason) {
149
+ span.setAttribute("gen_ai.finish_reason", String(r.finishReason));
150
+ }
151
+ return;
152
+ }
73
153
  }
74
154
  const stringified = safeStringify(result);
75
155
  if (stringified) {
76
156
  span.setAttribute("output.value", stringified);
77
157
  }
78
158
  }
159
+ function setStreamOutputValue(span, event) {
160
+ if (!event || typeof event !== "object") return;
161
+ const e = event;
162
+ if (typeof e.text === "string" && e.text) {
163
+ span.setAttribute("output.value", e.text);
164
+ } else if ("object" in e && e.object !== void 0) {
165
+ const stringified = safeStringify(e.object);
166
+ if (stringified) span.setAttribute("output.value", stringified);
167
+ }
168
+ if (e.finishReason) {
169
+ span.setAttribute("gen_ai.finish_reason", String(e.finishReason));
170
+ }
171
+ }
79
172
  var WRAPPED_FUNCTIONS = [
80
173
  "generateText",
81
174
  "streamText",
@@ -91,7 +184,7 @@ function wrapAISDK(aiModule) {
91
184
  const original = aiModule[name];
92
185
  if (typeof original !== "function") continue;
93
186
  if (name === "streamText" || name === "streamObject") {
94
- wrapped[name] = createSyncWrapper(name, original);
187
+ wrapped[name] = createStreamWrapper(name, original);
95
188
  } else {
96
189
  wrapped[name] = createAsyncWrapper(name, original);
97
190
  }
@@ -103,23 +196,20 @@ function rootSpanKind(name) {
103
196
  return "WORKFLOW";
104
197
  }
105
198
  function getParentContext() {
106
- const activeSpan = import_api.trace.getActiveSpan();
107
- if (activeSpan) {
108
- const instrScope = activeSpan.instrumentationLibrary?.name ?? "";
109
- if (instrScope === "next.js") {
110
- return import_api.ROOT_CONTEXT;
111
- }
112
- }
113
- return import_api.context.active();
199
+ return getNeatlogsParentContext();
114
200
  }
115
201
  function createAsyncWrapper(name, original) {
116
202
  return async function wrappedAsyncFn(opts) {
117
- const tracer = import_api.trace.getTracer(TRACER_NAME);
118
- return tracer.startActiveSpan(
203
+ const tracer = getNeatlogsTracer(TRACER_NAME);
204
+ const parentContext = getParentContext();
205
+ const span = tracer.startSpan(
119
206
  `ai.${name}`,
120
207
  { attributes: { "openinference.span.kind": rootSpanKind(name) } },
121
- getParentContext(),
122
- async (span) => {
208
+ parentContext
209
+ );
210
+ return withNeatlogsSpan(
211
+ span,
212
+ async () => {
123
213
  try {
124
214
  const isEmbedOrRerank = name === "embed" || name === "embedMany" || name === "rerank";
125
215
  if (!isEmbedOrRerank) {
@@ -140,26 +230,63 @@ function createAsyncWrapper(name, original) {
140
230
  } finally {
141
231
  span.end();
142
232
  }
143
- }
233
+ },
234
+ parentContext
144
235
  );
145
236
  };
146
237
  }
147
- function createSyncWrapper(name, original) {
148
- return function wrappedSyncFn(opts) {
149
- const tracer = import_api.trace.getTracer(TRACER_NAME);
150
- return tracer.startActiveSpan(`ai.${name}`, { attributes: { "openinference.span.kind": rootSpanKind(name) } }, getParentContext(), (span) => {
151
- try {
152
- setInputValue(span, opts);
153
- const merged = mergeTelemetry(opts);
154
- const result = original(merged);
155
- return result;
156
- } catch (err) {
157
- recordSpanError(span, err);
158
- throw err;
159
- } finally {
160
- span.end();
161
- }
162
- });
238
+ function createStreamWrapper(name, original) {
239
+ return function wrappedStreamFn(opts) {
240
+ const tracer = getNeatlogsTracer(TRACER_NAME);
241
+ const parentContext = getParentContext();
242
+ const span = tracer.startSpan(
243
+ `ai.${name}`,
244
+ { attributes: { "openinference.span.kind": rootSpanKind(name) } },
245
+ parentContext
246
+ );
247
+ return withNeatlogsSpan(
248
+ span,
249
+ () => {
250
+ let spanEnded = false;
251
+ const endOnce = () => {
252
+ if (spanEnded) return;
253
+ spanEnded = true;
254
+ span.end();
255
+ };
256
+ try {
257
+ setInputValue(span, opts);
258
+ const merged = mergeTelemetry(opts);
259
+ const userOnFinish = opts?.onFinish;
260
+ const userOnError = opts?.onError;
261
+ const wrappedOpts = {
262
+ ...merged,
263
+ onFinish: async (event) => {
264
+ try {
265
+ setStreamOutputValue(span, event);
266
+ } finally {
267
+ endOnce();
268
+ }
269
+ if (typeof userOnFinish === "function") {
270
+ return userOnFinish(event);
271
+ }
272
+ },
273
+ onError: (event) => {
274
+ recordSpanError(span, (event && event.error) ?? event);
275
+ endOnce();
276
+ if (typeof userOnError === "function") {
277
+ return userOnError(event);
278
+ }
279
+ }
280
+ };
281
+ return original(wrappedOpts);
282
+ } catch (err) {
283
+ recordSpanError(span, err);
284
+ endOnce();
285
+ throw err;
286
+ }
287
+ },
288
+ parentContext
289
+ );
163
290
  };
164
291
  }
165
292
  function mergeTelemetry(opts) {
@@ -176,10 +303,10 @@ function mergeTelemetry(opts) {
176
303
  }
177
304
  function recordSpanError(span, err) {
178
305
  if (err instanceof Error) {
179
- span.setStatus({ code: import_api.SpanStatusCode.ERROR, message: err.message });
306
+ span.setStatus({ code: import_api2.SpanStatusCode.ERROR, message: err.message });
180
307
  span.recordException(err);
181
308
  } else {
182
- span.setStatus({ code: import_api.SpanStatusCode.ERROR, message: String(err) });
309
+ span.setStatus({ code: import_api2.SpanStatusCode.ERROR, message: String(err) });
183
310
  }
184
311
  }
185
312
  // Annotate the CommonJS export names for ESM import in node:
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/ai-sdk.ts"],"sourcesContent":["/**\n * Vercel AI SDK wrapper — inline implementation.\n *\n * Wraps `generateText`, `streamText`, `generateObject`, `streamObject` from\n * the `ai` package with OTel parent spans + forced telemetry. Static export,\n * no dynamic imports — bundler-friendly (works with Turbopack, webpack, esbuild).\n *\n * Usage:\n * import { wrapAISDK } from 'neatlogs';\n * import * as ai from 'ai';\n * const { streamText, generateText } = wrapAISDK(ai);\n */\n\nimport { trace, context as otelContext, ROOT_CONTEXT, SpanStatusCode, type Span, type Tracer } from '@opentelemetry/api';\n\nconst TRACER_NAME = 'neatlogs.ai-sdk';\n\n// -- Telemetry config --------------------------------------------------------\n\nexport interface CreateAITelemetryOptions {\n metadata?: Record<string, unknown>;\n}\n\nexport interface AITelemetryConfig {\n isEnabled: true;\n recordInputs: true;\n recordOutputs: true;\n tracer: Tracer;\n metadata: Record<string, unknown>;\n}\n\nexport function createAITelemetry(\n opts: CreateAITelemetryOptions = {},\n): AITelemetryConfig {\n const userMeta = opts.metadata ?? {};\n return {\n isEnabled: true,\n recordInputs: true,\n recordOutputs: true,\n tracer: trace.getTracer(TRACER_NAME),\n metadata: { ...userMeta, neatlogsWrapped: true },\n };\n}\n\n// -- Span attributes ---------------------------------------------------------\n\nfunction safeStringify(value: unknown): string {\n try {\n return JSON.stringify(value);\n } catch {\n return '';\n }\n}\n\nfunction setInputValue(span: Span, opts: Record<string, unknown>): void {\n // For generateText/streamText — only capture prompt or messages, not full model config\n if (opts && ('prompt' in opts || 'messages' in opts)) {\n const input = opts.messages ?? opts.prompt;\n const stringified = safeStringify(input);\n if (stringified) {\n span.setAttribute('input.value', stringified);\n }\n return;\n }\n const stringified = safeStringify(opts);\n if (stringified) {\n span.setAttribute('input.value', stringified);\n }\n}\n\nfunction setOutputValue(span: Span, result: unknown): void {\n if (result && typeof result === 'object') {\n const r = result as Record<string, unknown>;\n // GenerateTextResult / StreamTextResult — extract meaningful fields\n if ('text' in r && 'finishReason' in r) {\n const text = String(r.text ?? '');\n if (text) {\n span.setAttribute('output.value', text);\n }\n if (r.finishReason) {\n span.setAttribute('gen_ai.finish_reason', String(r.finishReason));\n }\n return;\n }\n }\n const stringified = safeStringify(result);\n if (stringified) {\n span.setAttribute('output.value', stringified);\n }\n}\n\n// -- Wrapping ----------------------------------------------------------------\n\ntype WrappedFunctionName = 'generateText' | 'streamText' | 'generateObject' | 'streamObject' | 'embed' | 'embedMany' | 'rerank';\n\nconst WRAPPED_FUNCTIONS: readonly WrappedFunctionName[] = [\n 'generateText',\n 'streamText',\n 'generateObject',\n 'streamObject',\n 'embed',\n 'embedMany',\n 'rerank',\n] as const;\n\n/**\n * Wrap the `ai` module namespace so that every `generateText` / `streamText` /\n * `generateObject` / `streamObject` call:\n *\n * 1. Opens a parent OTel span on the active TracerProvider.\n * 2. Forces `experimental_telemetry: { isEnabled: true }`, merging user metadata.\n * 3. Records input/output on the parent span and propagates errors.\n *\n * Other exports (types, helpers) pass through unchanged.\n */\nexport function wrapAISDK<T extends Record<string, unknown>>(aiModule: T): T {\n const wrapped: Record<string, unknown> = { ...aiModule };\n\n for (const name of WRAPPED_FUNCTIONS) {\n const original = aiModule[name];\n if (typeof original !== 'function') continue;\n\n if (name === 'streamText' || name === 'streamObject') {\n wrapped[name] = createSyncWrapper(name, original as (opts: any) => unknown);\n } else {\n wrapped[name] = createAsyncWrapper(name, original as (opts: any) => Promise<unknown>);\n }\n }\n\n return wrapped as T;\n}\n\nfunction rootSpanKind(name: WrappedFunctionName): string {\n if (name === 'embed' || name === 'embedMany' || name === 'rerank') return 'CHAIN';\n return 'WORKFLOW';\n}\n\n\nfunction getParentContext() {\n const activeSpan = trace.getActiveSpan();\n if (activeSpan) {\n const instrScope = (activeSpan as any).instrumentationLibrary?.name ?? '';\n if (instrScope === 'next.js') {\n return ROOT_CONTEXT;\n }\n }\n return otelContext.active();\n}\n\nfunction createAsyncWrapper(\n name: WrappedFunctionName,\n original: (opts: any) => Promise<unknown>,\n): (opts: any) => Promise<unknown> {\n return async function wrappedAsyncFn(opts: any): Promise<unknown> {\n const tracer = trace.getTracer(TRACER_NAME);\n return tracer.startActiveSpan(\n `ai.${name}`,\n { attributes: { 'openinference.span.kind': rootSpanKind(name) } },\n getParentContext(),\n async (span) => {\n try {\n const isEmbedOrRerank = name === 'embed' || name === 'embedMany' || name === 'rerank';\n if (!isEmbedOrRerank) {\n setInputValue(span, opts);\n }\n if (name === 'rerank' && opts?.query) {\n span.setAttribute('ai.rerank.query', String(opts.query));\n }\n const merged = mergeTelemetry(opts);\n const result = await original(merged);\n if (!isEmbedOrRerank) {\n setOutputValue(span, result);\n }\n return result;\n } catch (err) {\n recordSpanError(span, err);\n throw err;\n } finally {\n span.end();\n }\n },\n );\n };\n}\n\nfunction createSyncWrapper(\n name: WrappedFunctionName,\n original: (opts: any) => unknown,\n): (opts: any) => unknown {\n return function wrappedSyncFn(opts: any): unknown {\n const tracer = trace.getTracer(TRACER_NAME);\n return tracer.startActiveSpan(`ai.${name}`, { attributes: { 'openinference.span.kind': rootSpanKind(name) } }, getParentContext(), (span) => {\n try {\n setInputValue(span, opts);\n const merged = mergeTelemetry(opts);\n const result = original(merged);\n return result;\n } catch (err) {\n recordSpanError(span, err);\n throw err;\n } finally {\n span.end();\n }\n });\n };\n}\n\nfunction mergeTelemetry(opts: any): any {\n const baseTelemetry: AITelemetryConfig = createAITelemetry({\n metadata: opts?.experimental_telemetry?.metadata,\n });\n return {\n ...opts,\n experimental_telemetry: {\n ...opts?.experimental_telemetry,\n ...baseTelemetry,\n },\n };\n}\n\n\nfunction recordSpanError(span: Span, err: unknown): void {\n if (err instanceof Error) {\n span.setStatus({ code: SpanStatusCode.ERROR, message: err.message });\n span.recordException(err);\n } else {\n span.setStatus({ code: SpanStatusCode.ERROR, message: String(err) });\n }\n}\n"],"mappings":"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","names":["stringified","otelContext"]}
1
+ {"version":3,"sources":["../src/ai-sdk.ts","../src/core/provider.ts"],"sourcesContent":["/**\n * Vercel AI SDK wrapper — inline implementation.\n *\n * Wraps `generateText`, `streamText`, `generateObject`, `streamObject` from\n * the `ai` package with OTel parent spans + forced telemetry. Static export,\n * no dynamic imports — bundler-friendly (works with Turbopack, webpack, esbuild).\n *\n * Usage:\n * import { wrapAISDK } from 'neatlogs';\n * import * as ai from 'ai';\n * const { streamText, generateText } = wrapAISDK(ai);\n */\n\nimport { SpanStatusCode, type Span, type Tracer } from '@opentelemetry/api';\nimport { getNeatlogsTracer, getNeatlogsParentContext, isolateTracer, withNeatlogsSpan } from './core/provider.js';\n\nconst TRACER_NAME = 'neatlogs.ai-sdk';\n\n// -- Telemetry config --------------------------------------------------------\n\nexport interface CreateAITelemetryOptions {\n metadata?: Record<string, unknown>;\n}\n\nexport interface AITelemetryConfig {\n isEnabled: true;\n recordInputs: true;\n recordOutputs: true;\n tracer: Tracer;\n metadata: Record<string, unknown>;\n}\n\nexport function createAITelemetry(\n opts: CreateAITelemetryOptions = {},\n): AITelemetryConfig {\n const userMeta = opts.metadata ?? {};\n return {\n isEnabled: true,\n recordInputs: true,\n recordOutputs: true,\n // Hand the AI SDK an isolation-aware tracer: it calls startActiveSpan()\n // internally, which would otherwise parent its native spans from the foreign\n // global context AND push them onto it (so a co-tenant's next span inherits\n // ours). The facade routes both through the private Neatlogs context.\n tracer: isolateTracer(getNeatlogsTracer(TRACER_NAME)),\n metadata: { ...userMeta, neatlogsWrapped: true },\n };\n}\n\n// -- Span attributes ---------------------------------------------------------\n\nfunction safeStringify(value: unknown): string {\n try {\n return JSON.stringify(value);\n } catch {\n return '';\n }\n}\n\nfunction setInputValue(span: Span, opts: Record<string, unknown>): void {\n // For generateText/streamText — only capture prompt or messages, not full model config\n if (opts && ('prompt' in opts || 'messages' in opts)) {\n const input = opts.messages ?? opts.prompt;\n const stringified = safeStringify(input);\n if (stringified) {\n span.setAttribute('input.value', stringified);\n }\n return;\n }\n const stringified = safeStringify(opts);\n if (stringified) {\n span.setAttribute('input.value', stringified);\n }\n}\n\nfunction setOutputValue(span: Span, result: unknown): void {\n if (result && typeof result === 'object') {\n const r = result as Record<string, unknown>;\n // GenerateTextResult / StreamTextResult — extract meaningful fields\n if ('text' in r && 'finishReason' in r) {\n const text = String(r.text ?? '');\n if (text) {\n span.setAttribute('output.value', text);\n }\n if (r.finishReason) {\n span.setAttribute('gen_ai.finish_reason', String(r.finishReason));\n }\n return;\n }\n // GenerateObjectResult — the structured object is the output, not `text`.\n // Without this the envelope (object+usage+response+…) gets stringified whole.\n if ('object' in r && 'finishReason' in r) {\n if (r.object !== undefined) {\n span.setAttribute('output.value', safeStringify(r.object));\n }\n if (r.finishReason) {\n span.setAttribute('gen_ai.finish_reason', String(r.finishReason));\n }\n return;\n }\n }\n const stringified = safeStringify(result);\n if (stringified) {\n span.setAttribute('output.value', stringified);\n }\n}\n\n// Extract output from a streamText/streamObject `onFinish` event. The event\n// extends StepResult, so `text` (streamText) / `object` (streamObject) sit at\n// the top level alongside `finishReason` — but the event also carries `steps`,\n// `usage`, etc., so we pull only the meaningful fields instead of stringifying\n// the whole envelope.\nfunction setStreamOutputValue(span: Span, event: unknown): void {\n if (!event || typeof event !== 'object') return;\n const e = event as Record<string, unknown>;\n if (typeof e.text === 'string' && e.text) {\n span.setAttribute('output.value', e.text);\n } else if ('object' in e && e.object !== undefined) {\n const stringified = safeStringify(e.object);\n if (stringified) span.setAttribute('output.value', stringified);\n }\n if (e.finishReason) {\n span.setAttribute('gen_ai.finish_reason', String(e.finishReason));\n }\n}\n\n// -- Wrapping ----------------------------------------------------------------\n\ntype WrappedFunctionName = 'generateText' | 'streamText' | 'generateObject' | 'streamObject' | 'embed' | 'embedMany' | 'rerank';\n\nconst WRAPPED_FUNCTIONS: readonly WrappedFunctionName[] = [\n 'generateText',\n 'streamText',\n 'generateObject',\n 'streamObject',\n 'embed',\n 'embedMany',\n 'rerank',\n] as const;\n\n/**\n * Wrap the `ai` module namespace so that every `generateText` / `streamText` /\n * `generateObject` / `streamObject` call:\n *\n * 1. Opens a parent OTel span on the active TracerProvider.\n * 2. Forces `experimental_telemetry: { isEnabled: true }`, merging user metadata.\n * 3. Records input/output on the parent span and propagates errors.\n *\n * Other exports (types, helpers) pass through unchanged.\n */\nexport function wrapAISDK<T extends Record<string, unknown>>(aiModule: T): T {\n const wrapped: Record<string, unknown> = { ...aiModule };\n\n for (const name of WRAPPED_FUNCTIONS) {\n const original = aiModule[name];\n if (typeof original !== 'function') continue;\n\n if (name === 'streamText' || name === 'streamObject') {\n wrapped[name] = createStreamWrapper(name, original as (opts: any) => unknown);\n } else {\n wrapped[name] = createAsyncWrapper(name, original as (opts: any) => Promise<unknown>);\n }\n }\n\n return wrapped as T;\n}\n\nfunction rootSpanKind(name: WrappedFunctionName): string {\n if (name === 'embed' || name === 'embedMany' || name === 'rerank') return 'CHAIN';\n return 'WORKFLOW';\n}\n\n\nfunction getParentContext() {\n // Our parent comes solely from the private span store; a\n // foreign provider's active span must never become our ancestor.\n return getNeatlogsParentContext();\n}\n\nfunction createAsyncWrapper(\n name: WrappedFunctionName,\n original: (opts: any) => Promise<unknown>,\n): (opts: any) => Promise<unknown> {\n return async function wrappedAsyncFn(opts: any): Promise<unknown> {\n const tracer = getNeatlogsTracer(TRACER_NAME);\n // startSpan (NOT startActiveSpan) + withNeatlogsSpan: startActiveSpan would\n // push our span onto the GLOBAL OTel context, so a foreign tracer's\n // startSpan() inside generateText() would read it as parent and inherit our\n // trace id. withNeatlogsSpan carries the parent in the private store in\n // the private context, leaving the global context untouched.\n const parentContext = getParentContext();\n const span = tracer.startSpan(\n `ai.${name}`,\n { attributes: { 'openinference.span.kind': rootSpanKind(name) } },\n parentContext,\n );\n return withNeatlogsSpan(\n span,\n async () => {\n try {\n const isEmbedOrRerank = name === 'embed' || name === 'embedMany' || name === 'rerank';\n if (!isEmbedOrRerank) {\n setInputValue(span, opts);\n }\n if (name === 'rerank' && opts?.query) {\n span.setAttribute('ai.rerank.query', String(opts.query));\n }\n const merged = mergeTelemetry(opts);\n const result = await original(merged);\n if (!isEmbedOrRerank) {\n setOutputValue(span, result);\n }\n return result;\n } catch (err) {\n recordSpanError(span, err);\n throw err;\n } finally {\n span.end();\n }\n },\n parentContext,\n );\n };\n}\n\n// streamText/streamObject return synchronously while the model keeps producing\n// tokens for seconds afterwards. Ending the span in a `finally` (as a plain sync\n// wrapper would) closes it in ~2ms with no output — the output only exists once\n// the stream finishes. Instead we keep the span open and end it from the AI SDK's\n// `onFinish` callback, where the final text/object is available. Any user-provided\n// `onFinish` is preserved and invoked first.\nfunction createStreamWrapper(\n name: WrappedFunctionName,\n original: (opts: any) => unknown,\n): (opts: any) => unknown {\n return function wrappedStreamFn(opts: any): unknown {\n const tracer = getNeatlogsTracer(TRACER_NAME);\n // startSpan + withNeatlogsSpan (see createAsyncWrapper) so streamText's\n // internals never see our span on the global OTel context. The span stays\n // open past the run scope and is ended from onFinish/onError.\n const parentContext = getParentContext();\n const span = tracer.startSpan(\n `ai.${name}`,\n { attributes: { 'openinference.span.kind': rootSpanKind(name) } },\n parentContext,\n );\n return withNeatlogsSpan(\n span,\n () => {\n let spanEnded = false;\n const endOnce = () => {\n if (spanEnded) return;\n spanEnded = true;\n span.end();\n };\n try {\n setInputValue(span, opts);\n const merged = mergeTelemetry(opts);\n const userOnFinish = opts?.onFinish;\n const userOnError = opts?.onError;\n const wrappedOpts = {\n ...merged,\n onFinish: async (event: any) => {\n try {\n setStreamOutputValue(span, event);\n } finally {\n endOnce();\n }\n if (typeof userOnFinish === 'function') {\n return userOnFinish(event);\n }\n },\n onError: (event: any) => {\n recordSpanError(span, (event && event.error) ?? event);\n endOnce();\n if (typeof userOnError === 'function') {\n return userOnError(event);\n }\n },\n };\n return original(wrappedOpts);\n } catch (err) {\n // Synchronous throw (e.g. bad arguments) — the stream never started.\n recordSpanError(span, err);\n endOnce();\n throw err;\n }\n },\n parentContext,\n );\n };\n}\n\nfunction mergeTelemetry(opts: any): any {\n const baseTelemetry: AITelemetryConfig = createAITelemetry({\n metadata: opts?.experimental_telemetry?.metadata,\n });\n return {\n ...opts,\n experimental_telemetry: {\n ...opts?.experimental_telemetry,\n ...baseTelemetry,\n },\n };\n}\n\n\nfunction recordSpanError(span: Span, err: unknown): void {\n if (err instanceof Error) {\n span.setStatus({ code: SpanStatusCode.ERROR, message: err.message });\n span.recordException(err);\n } else {\n span.setStatus({ code: SpanStatusCode.ERROR, message: String(err) });\n }\n}\n","/**\n * Neatlogs-owned tracing state.\n *\n * Spans are created by the private Neatlogs provider and their parent is carried\n * in a private context key. The process-global OpenTelemetry span is\n * deliberately left untouched so other observability SDKs cannot export or\n * become parents of Neatlogs spans.\n */\n\nimport { AsyncLocalStorage } from 'node:async_hooks';\nimport {\n ROOT_CONTEXT,\n INVALID_SPAN_CONTEXT,\n trace as otelTrace,\n type Context,\n type Span,\n type SpanOptions,\n type Tracer,\n type TracerProvider,\n} from '@opentelemetry/api';\n\n// Entry points are bundled independently (`neatlogs`, `neatlogs/openai`,\n// `neatlogs/ai`, `neatlogs/mastra`, … each in both CJS and ESM), so every piece\n// of shared tracing state — the private span store AND the resolved provider /\n// provider — must live on `globalThis` behind a `Symbol.for` key.\n// Otherwise `init()` (run from the `neatlogs` bundle) sets `_provider` in ITS\n// module copy while a wrapper imported from `neatlogs/openai` reads a different,\n// still-null copy and silently falls back to the foreign global provider.\nconst PRIVATE_SPAN_STORAGE_KEY = Symbol.for(\n 'neatlogs.private_span_async_local_storage',\n);\nconst PRIVATE_PROVIDER_STATE_KEY = Symbol.for(\n 'neatlogs.private_provider_state',\n);\ninterface PrivateProviderState {\n provider: TracerProvider | null;\n}\ntype NeatlogsGlobal = typeof globalThis & {\n [PRIVATE_SPAN_STORAGE_KEY]?: AsyncLocalStorage<Context>;\n [PRIVATE_PROVIDER_STATE_KEY]?: PrivateProviderState;\n};\nconst neatlogsGlobal = globalThis as NeatlogsGlobal;\n// Stores the full Neatlogs Context (parent span PLUS any threaded values such as\n// trace()'s prompt-template keys), not just the span. We never\n// activate the OTel global context, so this private store is the ONLY channel\n// through which those values propagate down to descendant spans.\nconst privateContextStorage =\n neatlogsGlobal[PRIVATE_SPAN_STORAGE_KEY] ??\n (neatlogsGlobal[PRIVATE_SPAN_STORAGE_KEY] = new AsyncLocalStorage<Context>());\nconst providerState: PrivateProviderState =\n neatlogsGlobal[PRIVATE_PROVIDER_STATE_KEY] ??\n (neatlogsGlobal[PRIVATE_PROVIDER_STATE_KEY] = {\n provider: null,\n });\n// Wrappers may be constructed or accidentally invoked before init(). They must\n// never fall back to a foreign process-global provider, so pre-init calls use a\n// local no-op tracer and safely emit no exported spans.\nconst preInitTracer: Tracer = {\n startSpan(): Span {\n return otelTrace.wrapSpanContext(INVALID_SPAN_CONTEXT);\n },\n startActiveSpan<F extends (span: Span) => unknown>(\n _name: string,\n arg2?: SpanOptions | Context | F,\n arg3?: Context | F,\n arg4?: F,\n ): ReturnType<F> {\n const fn =\n typeof arg2 === 'function'\n ? arg2\n : typeof arg3 === 'function'\n ? arg3\n : arg4;\n return fn!(otelTrace.wrapSpanContext(INVALID_SPAN_CONTEXT)) as ReturnType<F>;\n },\n};\n\n/** @internal Configure the provider used by Neatlogs-created spans. */\nexport function _setNeatlogsProvider(provider: TracerProvider | null): void {\n providerState.provider = provider;\n}\n\n/** Resolve a tracer from the private provider when one is configured. */\nexport function getNeatlogsTracer(name: string): Tracer {\n return providerState.provider?.getTracer(name) ?? preInitTracer;\n}\n\n/**\n * @internal The private Neatlogs provider, or null before\n * init(). Used by integrations that must repoint a self-instrumenting library's\n * captured provider onto ours.\n */\nexport function getNeatlogsProvider(): TracerProvider | null {\n return providerState.provider;\n}\n\n/**\n * Wrap a tracer so that spans it creates parent from — and, for\n * `startActiveSpan`, activate on — the PRIVATE Neatlogs context instead of the\n * global one.\n *\n * We hand this to libraries that create their own spans off a tracer we give\n * them (the Vercel AI SDK's `experimental_telemetry.tracer`, which calls\n * `tracer.startActiveSpan()` internally). Without the facade the AI SDK's native\n * spans would parent from `context.active()` — the foreign co-tenant's context —\n * and `startActiveSpan` would push them onto the GLOBAL context, so a foreign\n * tracer's next span reads our native span as its parent. Both directions leak.\n *\n */\nexport function isolateTracer(tracer: Tracer): Tracer {\n const facade: Tracer = {\n startSpan(name: string, options?: SpanOptions, context?: Context): Span {\n const parent = context ?? getNeatlogsActiveContext();\n return tracer.startSpan(name, options, parent);\n },\n startActiveSpan<F extends (span: Span) => unknown>(\n name: string,\n arg2?: SpanOptions | Context | F,\n arg3?: Context | F,\n arg4?: F,\n ): ReturnType<F> {\n // Normalize the 2/3/4-arg overloads of startActiveSpan.\n let options: SpanOptions | undefined;\n let context: Context | undefined;\n let fn: F;\n if (typeof arg2 === 'function') {\n fn = arg2 as F;\n } else if (typeof arg3 === 'function') {\n options = arg2 as SpanOptions;\n fn = arg3 as F;\n } else {\n options = arg2 as SpanOptions;\n context = arg3 as Context;\n fn = arg4 as F;\n }\n const parent = context ?? getNeatlogsActiveContext();\n const span = tracer.startSpan(name, options, parent);\n return withNeatlogsSpan(span, () => fn(span), parent) as ReturnType<F>;\n },\n };\n return facade;\n}\n\n/**\n * The base context to build new Neatlogs spans and values on. NEVER contains a\n * foreign provider's span: it reads our private store, or ROOT_CONTEXT when\n * nothing is active.\n */\nexport function getNeatlogsActiveContext(): Context {\n return privateContextStorage.getStore() ?? ROOT_CONTEXT;\n}\n\n/** Return only the active Neatlogs span (never a foreign provider's span). */\nexport function getActiveNeatlogsSpan(): Span | undefined {\n return otelTrace.getSpan(getNeatlogsActiveContext());\n}\n\n/**\n * Build a parent context that cannot contain a foreign provider's span.\n *\n * The active Neatlogs context already carries the parent span AND any values\n * the caller threaded in upstream (e.g. `trace()`'s prompt-template values), so\n * those values reach the span processor via `onStart(parentContext)`. An\n * explicit `baseContext` (a caller's own value-carrying context) is honored as-is.\n */\nexport function getNeatlogsParentContext(baseContext?: Context): Context {\n return baseContext ?? getNeatlogsActiveContext();\n}\n\n/**\n * A base context for callers that thread parent linkage themselves\n * (callback/event handlers that keep their own run-id → span map: LangChain,\n * OpenAI-Agents, Claude Agent SDK).\n *\n * This is the ACTIVE Neatlogs context — the private store's\n * context if a Neatlogs `trace()`/`span()` encloses this call, else\n * ROOT_CONTEXT. So a handler's own root/entry span nests under an enclosing\n * Neatlogs trace (preserving its session + end-user id) when one exists, and\n * auto-roots cleanly when one doesn't. A foreign provider's active span can\n * never leak in as an ancestor because the global context is never read.\n */\nexport function getNeatlogsBaseContext(baseContext?: Context): Context {\n return baseContext ?? getNeatlogsActiveContext();\n}\n\n/**\n * Build an execution context for a Neatlogs span while preserving the active\n * foreign context. The span rides our private context store.\n */\nexport function getNeatlogsExecutionContext(\n span: Span,\n baseContext: Context = ROOT_CONTEXT,\n): Context {\n return otelTrace.setSpan(baseContext, span);\n}\n\n/**\n * Run a callback with a Neatlogs span active under the appropriate policy.\n *\n * The stored context is `setSpan(base, span)` — carrying the span PLUS whatever\n * values `base` holds — so threaded values (prompt templates)\n * propagate to descendant spans through our private store instead of the global\n * OTel context we deliberately never touch.\n */\nexport function withNeatlogsSpan<T>(\n span: Span,\n fn: () => T,\n baseContext?: Context,\n): T {\n const base = baseContext ?? getNeatlogsActiveContext();\n const ctx = otelTrace.setSpan(base, span);\n return privateContextStorage.run(ctx, fn);\n}\n"],"mappings":"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","names":["import_api","otelTrace","otelTrace","stringified"]}
package/dist/ai-sdk.mjs CHANGED
@@ -1,5 +1,76 @@
1
1
  // src/ai-sdk.ts
2
- import { trace, context as otelContext, ROOT_CONTEXT, SpanStatusCode } from "@opentelemetry/api";
2
+ import { SpanStatusCode } from "@opentelemetry/api";
3
+
4
+ // src/core/provider.ts
5
+ import { AsyncLocalStorage } from "async_hooks";
6
+ import {
7
+ ROOT_CONTEXT,
8
+ INVALID_SPAN_CONTEXT,
9
+ trace as otelTrace
10
+ } from "@opentelemetry/api";
11
+ var PRIVATE_SPAN_STORAGE_KEY = /* @__PURE__ */ Symbol.for(
12
+ "neatlogs.private_span_async_local_storage"
13
+ );
14
+ var PRIVATE_PROVIDER_STATE_KEY = /* @__PURE__ */ Symbol.for(
15
+ "neatlogs.private_provider_state"
16
+ );
17
+ var neatlogsGlobal = globalThis;
18
+ var privateContextStorage = neatlogsGlobal[PRIVATE_SPAN_STORAGE_KEY] ?? (neatlogsGlobal[PRIVATE_SPAN_STORAGE_KEY] = new AsyncLocalStorage());
19
+ var providerState = neatlogsGlobal[PRIVATE_PROVIDER_STATE_KEY] ?? (neatlogsGlobal[PRIVATE_PROVIDER_STATE_KEY] = {
20
+ provider: null
21
+ });
22
+ var preInitTracer = {
23
+ startSpan() {
24
+ return otelTrace.wrapSpanContext(INVALID_SPAN_CONTEXT);
25
+ },
26
+ startActiveSpan(_name, arg2, arg3, arg4) {
27
+ const fn = typeof arg2 === "function" ? arg2 : typeof arg3 === "function" ? arg3 : arg4;
28
+ return fn(otelTrace.wrapSpanContext(INVALID_SPAN_CONTEXT));
29
+ }
30
+ };
31
+ function getNeatlogsTracer(name) {
32
+ return providerState.provider?.getTracer(name) ?? preInitTracer;
33
+ }
34
+ function isolateTracer(tracer) {
35
+ const facade = {
36
+ startSpan(name, options, context) {
37
+ const parent = context ?? getNeatlogsActiveContext();
38
+ return tracer.startSpan(name, options, parent);
39
+ },
40
+ startActiveSpan(name, arg2, arg3, arg4) {
41
+ let options;
42
+ let context;
43
+ let fn;
44
+ if (typeof arg2 === "function") {
45
+ fn = arg2;
46
+ } else if (typeof arg3 === "function") {
47
+ options = arg2;
48
+ fn = arg3;
49
+ } else {
50
+ options = arg2;
51
+ context = arg3;
52
+ fn = arg4;
53
+ }
54
+ const parent = context ?? getNeatlogsActiveContext();
55
+ const span = tracer.startSpan(name, options, parent);
56
+ return withNeatlogsSpan(span, () => fn(span), parent);
57
+ }
58
+ };
59
+ return facade;
60
+ }
61
+ function getNeatlogsActiveContext() {
62
+ return privateContextStorage.getStore() ?? ROOT_CONTEXT;
63
+ }
64
+ function getNeatlogsParentContext(baseContext) {
65
+ return baseContext ?? getNeatlogsActiveContext();
66
+ }
67
+ function withNeatlogsSpan(span, fn, baseContext) {
68
+ const base = baseContext ?? getNeatlogsActiveContext();
69
+ const ctx = otelTrace.setSpan(base, span);
70
+ return privateContextStorage.run(ctx, fn);
71
+ }
72
+
73
+ // src/ai-sdk.ts
3
74
  var TRACER_NAME = "neatlogs.ai-sdk";
4
75
  function createAITelemetry(opts = {}) {
5
76
  const userMeta = opts.metadata ?? {};
@@ -7,7 +78,11 @@ function createAITelemetry(opts = {}) {
7
78
  isEnabled: true,
8
79
  recordInputs: true,
9
80
  recordOutputs: true,
10
- tracer: trace.getTracer(TRACER_NAME),
81
+ // Hand the AI SDK an isolation-aware tracer: it calls startActiveSpan()
82
+ // internally, which would otherwise parent its native spans from the foreign
83
+ // global context AND push them onto it (so a co-tenant's next span inherits
84
+ // ours). The facade routes both through the private Neatlogs context.
85
+ tracer: isolateTracer(getNeatlogsTracer(TRACER_NAME)),
11
86
  metadata: { ...userMeta, neatlogsWrapped: true }
12
87
  };
13
88
  }
@@ -45,12 +120,34 @@ function setOutputValue(span, result) {
45
120
  }
46
121
  return;
47
122
  }
123
+ if ("object" in r && "finishReason" in r) {
124
+ if (r.object !== void 0) {
125
+ span.setAttribute("output.value", safeStringify(r.object));
126
+ }
127
+ if (r.finishReason) {
128
+ span.setAttribute("gen_ai.finish_reason", String(r.finishReason));
129
+ }
130
+ return;
131
+ }
48
132
  }
49
133
  const stringified = safeStringify(result);
50
134
  if (stringified) {
51
135
  span.setAttribute("output.value", stringified);
52
136
  }
53
137
  }
138
+ function setStreamOutputValue(span, event) {
139
+ if (!event || typeof event !== "object") return;
140
+ const e = event;
141
+ if (typeof e.text === "string" && e.text) {
142
+ span.setAttribute("output.value", e.text);
143
+ } else if ("object" in e && e.object !== void 0) {
144
+ const stringified = safeStringify(e.object);
145
+ if (stringified) span.setAttribute("output.value", stringified);
146
+ }
147
+ if (e.finishReason) {
148
+ span.setAttribute("gen_ai.finish_reason", String(e.finishReason));
149
+ }
150
+ }
54
151
  var WRAPPED_FUNCTIONS = [
55
152
  "generateText",
56
153
  "streamText",
@@ -66,7 +163,7 @@ function wrapAISDK(aiModule) {
66
163
  const original = aiModule[name];
67
164
  if (typeof original !== "function") continue;
68
165
  if (name === "streamText" || name === "streamObject") {
69
- wrapped[name] = createSyncWrapper(name, original);
166
+ wrapped[name] = createStreamWrapper(name, original);
70
167
  } else {
71
168
  wrapped[name] = createAsyncWrapper(name, original);
72
169
  }
@@ -78,23 +175,20 @@ function rootSpanKind(name) {
78
175
  return "WORKFLOW";
79
176
  }
80
177
  function getParentContext() {
81
- const activeSpan = trace.getActiveSpan();
82
- if (activeSpan) {
83
- const instrScope = activeSpan.instrumentationLibrary?.name ?? "";
84
- if (instrScope === "next.js") {
85
- return ROOT_CONTEXT;
86
- }
87
- }
88
- return otelContext.active();
178
+ return getNeatlogsParentContext();
89
179
  }
90
180
  function createAsyncWrapper(name, original) {
91
181
  return async function wrappedAsyncFn(opts) {
92
- const tracer = trace.getTracer(TRACER_NAME);
93
- return tracer.startActiveSpan(
182
+ const tracer = getNeatlogsTracer(TRACER_NAME);
183
+ const parentContext = getParentContext();
184
+ const span = tracer.startSpan(
94
185
  `ai.${name}`,
95
186
  { attributes: { "openinference.span.kind": rootSpanKind(name) } },
96
- getParentContext(),
97
- async (span) => {
187
+ parentContext
188
+ );
189
+ return withNeatlogsSpan(
190
+ span,
191
+ async () => {
98
192
  try {
99
193
  const isEmbedOrRerank = name === "embed" || name === "embedMany" || name === "rerank";
100
194
  if (!isEmbedOrRerank) {
@@ -115,26 +209,63 @@ function createAsyncWrapper(name, original) {
115
209
  } finally {
116
210
  span.end();
117
211
  }
118
- }
212
+ },
213
+ parentContext
119
214
  );
120
215
  };
121
216
  }
122
- function createSyncWrapper(name, original) {
123
- return function wrappedSyncFn(opts) {
124
- const tracer = trace.getTracer(TRACER_NAME);
125
- return tracer.startActiveSpan(`ai.${name}`, { attributes: { "openinference.span.kind": rootSpanKind(name) } }, getParentContext(), (span) => {
126
- try {
127
- setInputValue(span, opts);
128
- const merged = mergeTelemetry(opts);
129
- const result = original(merged);
130
- return result;
131
- } catch (err) {
132
- recordSpanError(span, err);
133
- throw err;
134
- } finally {
135
- span.end();
136
- }
137
- });
217
+ function createStreamWrapper(name, original) {
218
+ return function wrappedStreamFn(opts) {
219
+ const tracer = getNeatlogsTracer(TRACER_NAME);
220
+ const parentContext = getParentContext();
221
+ const span = tracer.startSpan(
222
+ `ai.${name}`,
223
+ { attributes: { "openinference.span.kind": rootSpanKind(name) } },
224
+ parentContext
225
+ );
226
+ return withNeatlogsSpan(
227
+ span,
228
+ () => {
229
+ let spanEnded = false;
230
+ const endOnce = () => {
231
+ if (spanEnded) return;
232
+ spanEnded = true;
233
+ span.end();
234
+ };
235
+ try {
236
+ setInputValue(span, opts);
237
+ const merged = mergeTelemetry(opts);
238
+ const userOnFinish = opts?.onFinish;
239
+ const userOnError = opts?.onError;
240
+ const wrappedOpts = {
241
+ ...merged,
242
+ onFinish: async (event) => {
243
+ try {
244
+ setStreamOutputValue(span, event);
245
+ } finally {
246
+ endOnce();
247
+ }
248
+ if (typeof userOnFinish === "function") {
249
+ return userOnFinish(event);
250
+ }
251
+ },
252
+ onError: (event) => {
253
+ recordSpanError(span, (event && event.error) ?? event);
254
+ endOnce();
255
+ if (typeof userOnError === "function") {
256
+ return userOnError(event);
257
+ }
258
+ }
259
+ };
260
+ return original(wrappedOpts);
261
+ } catch (err) {
262
+ recordSpanError(span, err);
263
+ endOnce();
264
+ throw err;
265
+ }
266
+ },
267
+ parentContext
268
+ );
138
269
  };
139
270
  }
140
271
  function mergeTelemetry(opts) {