@introspection-sdk/introspection-node 0.4.0

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 (89) hide show
  1. package/.turbo/turbo-build.log +4 -0
  2. package/.turbo/turbo-check-types.log +5 -0
  3. package/.turbo/turbo-lint.log +5 -0
  4. package/dist/ai-sdk-integration.d.ts +123 -0
  5. package/dist/ai-sdk-integration.d.ts.map +1 -0
  6. package/dist/ai-sdk-integration.js +475 -0
  7. package/dist/anthropic.d.ts +54 -0
  8. package/dist/anthropic.d.ts.map +1 -0
  9. package/dist/anthropic.js +375 -0
  10. package/dist/claude-hooks.d.ts +328 -0
  11. package/dist/claude-hooks.d.ts.map +1 -0
  12. package/dist/claude-hooks.js +571 -0
  13. package/dist/claude-wrapper.d.ts +93 -0
  14. package/dist/claude-wrapper.d.ts.map +1 -0
  15. package/dist/claude-wrapper.js +154 -0
  16. package/dist/client.d.ts +157 -0
  17. package/dist/client.d.ts.map +1 -0
  18. package/dist/client.js +394 -0
  19. package/dist/converters/ai-sdk.d.ts +65 -0
  20. package/dist/converters/ai-sdk.d.ts.map +1 -0
  21. package/dist/converters/ai-sdk.js +238 -0
  22. package/dist/converters/claude.d.ts +146 -0
  23. package/dist/converters/claude.d.ts.map +1 -0
  24. package/dist/converters/claude.js +202 -0
  25. package/dist/converters/index.d.ts +9 -0
  26. package/dist/converters/index.d.ts.map +1 -0
  27. package/dist/converters/index.js +8 -0
  28. package/dist/converters/openai.d.ts +51 -0
  29. package/dist/converters/openai.d.ts.map +1 -0
  30. package/dist/converters/openai.js +268 -0
  31. package/dist/converters/openclaw.d.ts +16 -0
  32. package/dist/converters/openclaw.d.ts.map +1 -0
  33. package/dist/converters/openclaw.js +49 -0
  34. package/dist/converters/openinference.d.ts +107 -0
  35. package/dist/converters/openinference.d.ts.map +1 -0
  36. package/dist/converters/openinference.js +552 -0
  37. package/dist/converters/vercel.d.ts +21 -0
  38. package/dist/converters/vercel.d.ts.map +1 -0
  39. package/dist/converters/vercel.js +208 -0
  40. package/dist/index.d.ts +38 -0
  41. package/dist/index.d.ts.map +1 -0
  42. package/dist/index.js +39 -0
  43. package/dist/langchain-handler.d.ts +101 -0
  44. package/dist/langchain-handler.d.ts.map +1 -0
  45. package/dist/langchain-handler.js +489 -0
  46. package/dist/mastra-exporter.d.ts +85 -0
  47. package/dist/mastra-exporter.d.ts.map +1 -0
  48. package/dist/mastra-exporter.js +471 -0
  49. package/dist/span-processor.d.ts +62 -0
  50. package/dist/span-processor.d.ts.map +1 -0
  51. package/dist/span-processor.js +310 -0
  52. package/dist/tracing-processor.d.ts +136 -0
  53. package/dist/tracing-processor.d.ts.map +1 -0
  54. package/dist/tracing-processor.js +368 -0
  55. package/dist/types/genai.d.ts +165 -0
  56. package/dist/types/genai.d.ts.map +1 -0
  57. package/dist/types/genai.js +82 -0
  58. package/dist/types.d.ts +7 -0
  59. package/dist/types.d.ts.map +1 -0
  60. package/dist/types.js +1 -0
  61. package/dist/utils.d.ts +19 -0
  62. package/dist/utils.d.ts.map +1 -0
  63. package/dist/utils.js +57 -0
  64. package/dist/version.d.ts +3 -0
  65. package/dist/version.d.ts.map +1 -0
  66. package/dist/version.js +2 -0
  67. package/eslint.config.mjs +4 -0
  68. package/package.json +78 -0
  69. package/src/ai-sdk-integration.ts +689 -0
  70. package/src/anthropic.ts +465 -0
  71. package/src/claude-hooks.ts +1010 -0
  72. package/src/claude-wrapper.ts +240 -0
  73. package/src/client.ts +513 -0
  74. package/src/converters/ai-sdk.ts +298 -0
  75. package/src/converters/claude.ts +334 -0
  76. package/src/converters/index.ts +44 -0
  77. package/src/converters/openai.ts +302 -0
  78. package/src/converters/openinference.ts +673 -0
  79. package/src/converters/vercel.ts +264 -0
  80. package/src/index.ts +121 -0
  81. package/src/langchain-handler.ts +722 -0
  82. package/src/mastra-exporter.ts +650 -0
  83. package/src/span-processor.ts +418 -0
  84. package/src/tracing-processor.ts +548 -0
  85. package/src/types/genai.ts +264 -0
  86. package/src/types.ts +13 -0
  87. package/src/utils.ts +69 -0
  88. package/src/version.ts +2 -0
  89. package/tsconfig.json +8 -0
@@ -0,0 +1,418 @@
1
+ import {
2
+ Context,
3
+ Attributes,
4
+ propagation,
5
+ context as otelContext,
6
+ } from "@opentelemetry/api";
7
+ import {
8
+ SpanProcessor,
9
+ ReadableSpan,
10
+ Span,
11
+ BatchSpanProcessor,
12
+ SpanExporter,
13
+ } from "@opentelemetry/sdk-trace-base";
14
+ import { Resource, resourceFromAttributes } from "@opentelemetry/resources";
15
+ import { ATTR_SERVICE_NAME } from "@opentelemetry/semantic-conventions";
16
+ import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-proto";
17
+ import { ExportResult, ExportResultCode } from "@opentelemetry/core";
18
+ import { randomUUID } from "crypto";
19
+ import type { AdvancedOptions } from "./types.js";
20
+ import { logger } from "./utils.js";
21
+ import {
22
+ isOpenInferenceSpan,
23
+ replaceOpenInferenceWithGenAI,
24
+ isVercelAISpan,
25
+ convertVercelAIToGenAI,
26
+ } from "./converters/index.js";
27
+
28
+ /**
29
+ * Extended span type that includes both v1 and v2 instrumentation properties.
30
+ * OpenTelemetry SDK v2 uses instrumentationScope, while v1 uses instrumentationLibrary.
31
+ */
32
+ type CompatibleSpan = ReadableSpan & {
33
+ instrumentationLibrary?: unknown;
34
+ instrumentationScope?: unknown;
35
+ };
36
+
37
+ /**
38
+ * A {@link ReadableSpan} wrapper that substitutes the original attributes with
39
+ * a converted set while delegating every other property to the original span.
40
+ *
41
+ * Used internally by {@link IntrospectionSpanProcessor} to convert
42
+ * OpenInference attributes to Gen AI semantic conventions on-the-fly.
43
+ */
44
+ class ConvertedReadableSpan implements ReadableSpan {
45
+ private _original: ReadableSpan;
46
+ private _convertedAttributes: Attributes;
47
+ private _resource?: Resource;
48
+
49
+ constructor(
50
+ original: ReadableSpan,
51
+ convertedAttributes: Attributes,
52
+ resource?: Resource,
53
+ ) {
54
+ this._original = original;
55
+ this._convertedAttributes = convertedAttributes;
56
+ this._resource = resource;
57
+ }
58
+
59
+ get name() {
60
+ return this._original.name;
61
+ }
62
+ get kind() {
63
+ return this._original.kind;
64
+ }
65
+ spanContext() {
66
+ return this._original.spanContext();
67
+ }
68
+ get startTime() {
69
+ return this._original.startTime;
70
+ }
71
+ get endTime() {
72
+ return this._original.endTime;
73
+ }
74
+ get status() {
75
+ return this._original.status;
76
+ }
77
+ get links() {
78
+ return this._original.links;
79
+ }
80
+ get events() {
81
+ return this._original.events;
82
+ }
83
+ get duration() {
84
+ return this._original.duration;
85
+ }
86
+ get ended() {
87
+ return this._original.ended;
88
+ }
89
+ get resource() {
90
+ return this._resource ?? this._original.resource;
91
+ }
92
+ get instrumentationScope() {
93
+ return this._original.instrumentationScope;
94
+ }
95
+ get droppedAttributesCount() {
96
+ return this._original.droppedAttributesCount;
97
+ }
98
+ get droppedEventsCount() {
99
+ return this._original.droppedEventsCount;
100
+ }
101
+ get droppedLinksCount() {
102
+ return this._original.droppedLinksCount;
103
+ }
104
+
105
+ get attributes() {
106
+ return this._convertedAttributes;
107
+ }
108
+ }
109
+
110
+ export interface IntrospectionSpanProcessorOptions {
111
+ /** Authentication token (env: INTROSPECTION_TOKEN) */
112
+ token?: string;
113
+ /**
114
+ * Service name for telemetry (env: INTROSPECTION_SERVICE_NAME).
115
+ * Note: For spans, the service name is typically set on the TracerProvider's resource.
116
+ * This option is provided for consistency but the TracerProvider's resource takes precedence.
117
+ */
118
+ serviceName?: string;
119
+ /** Advanced options for configuration and testing */
120
+ advanced?: AdvancedOptions;
121
+ }
122
+
123
+ /**
124
+ * OTel {@link SpanProcessor} that forwards traces to the Introspection backend
125
+ * via OTLP, automatically converting any OpenInference spans to Gen AI
126
+ * semantic convention attributes.
127
+ *
128
+ * @example
129
+ * ```ts
130
+ * import { IntrospectionSpanProcessor } from "@introspection-sdk/introspection-node";
131
+ *
132
+ * const processor = new IntrospectionSpanProcessor({ token: "sk-intro-…" });
133
+ * tracerProvider.addSpanProcessor(processor);
134
+ * ```
135
+ */
136
+ export class IntrospectionSpanProcessor implements SpanProcessor {
137
+ private _spanProcessor: SpanProcessor;
138
+ private _serviceName?: string;
139
+ private _conversationIds: Map<string, string> = new Map();
140
+
141
+ constructor(options: IntrospectionSpanProcessorOptions = {}) {
142
+ const token = options.token || process.env.INTROSPECTION_TOKEN;
143
+ if (!token) {
144
+ throw new Error("INTROSPECTION_TOKEN is required");
145
+ }
146
+
147
+ const serviceName =
148
+ options.serviceName ||
149
+ process.env.INTROSPECTION_SERVICE_NAME ||
150
+ undefined; // Falls back to TracerProvider's resource
151
+
152
+ this._serviceName = serviceName;
153
+
154
+ const advanced = options.advanced || {};
155
+ const baseUrl =
156
+ advanced.baseUrl ||
157
+ process.env.INTROSPECTION_BASE_URL ||
158
+ "https://otel.introspection.dev";
159
+
160
+ const headers: Record<string, string> = {
161
+ Authorization: `Bearer ${token}`,
162
+ ...(advanced.additionalHeaders || {}),
163
+ };
164
+
165
+ // Construct endpoint URL
166
+ let endpoint: string;
167
+ if (baseUrl.endsWith("/v1/traces")) {
168
+ endpoint = baseUrl;
169
+ } else {
170
+ endpoint = `${baseUrl.replace(/\/$/, "")}/v1/traces`;
171
+ }
172
+
173
+ logger.info(
174
+ `IntrospectionSpanProcessor initialized: endpoint=${endpoint}${serviceName ? `, serviceName=${serviceName}` : ""}`,
175
+ );
176
+
177
+ // Use custom spanExporter if provided (for testing), otherwise create OTLP exporter
178
+ const baseExporter: SpanExporter =
179
+ advanced.spanExporter ||
180
+ new OTLPTraceExporter({
181
+ url: endpoint,
182
+ headers,
183
+ });
184
+
185
+ // Wrap exporter to add v1/v2 compatibility and basic validation
186
+ const exporter: SpanExporter = {
187
+ export: (
188
+ spans: ReadableSpan[],
189
+ resultCallback: (result: ExportResult) => void,
190
+ ) => {
191
+ // Filter and fix spans: ensure they have required properties and add v1 compatibility
192
+ const validSpans = spans
193
+ .filter((span) => {
194
+ // Basic validation: span must have name and resource
195
+ if (!span?.name || !span.resource) {
196
+ return false;
197
+ }
198
+ // Must have either instrumentationLibrary or instrumentationScope
199
+ const compatSpan = span as CompatibleSpan;
200
+ return !!(
201
+ compatSpan.instrumentationLibrary ||
202
+ compatSpan.instrumentationScope
203
+ );
204
+ })
205
+ .map((span) => span as CompatibleSpan)
206
+ .map((span) => {
207
+ // Compatibility: add instrumentationLibrary for spans that only have instrumentationScope (v2)
208
+ const compatSpan = span as CompatibleSpan;
209
+ if (
210
+ compatSpan.instrumentationScope &&
211
+ !compatSpan.instrumentationLibrary
212
+ ) {
213
+ compatSpan.instrumentationLibrary =
214
+ compatSpan.instrumentationScope;
215
+ }
216
+ return span;
217
+ });
218
+
219
+ if (validSpans.length === 0) {
220
+ resultCallback({ code: ExportResultCode.SUCCESS });
221
+ return;
222
+ }
223
+
224
+ // Export with error handling
225
+ try {
226
+ baseExporter.export(validSpans, (result) => {
227
+ if (result.code === ExportResultCode.SUCCESS) {
228
+ logger.debug(
229
+ `Exported ${validSpans.length} span(s) to ${endpoint}`,
230
+ );
231
+ } else {
232
+ logger.error(
233
+ `Failed to export ${validSpans.length} span(s): ${result.error?.message || "Unknown error"}`,
234
+ );
235
+ }
236
+ resultCallback(result);
237
+ });
238
+ } catch (error) {
239
+ logger.error(
240
+ `Export error: ${error instanceof Error ? error.message : String(error)}`,
241
+ );
242
+ resultCallback({
243
+ code: ExportResultCode.FAILED,
244
+ error: error instanceof Error ? error : new Error(String(error)),
245
+ });
246
+ }
247
+ },
248
+ shutdown: () => baseExporter.shutdown(),
249
+ forceFlush: () => {
250
+ if (baseExporter.forceFlush) {
251
+ return baseExporter.forceFlush();
252
+ }
253
+ return Promise.resolve();
254
+ },
255
+ };
256
+
257
+ // Use BatchSpanProcessor like logfire does
258
+ this._spanProcessor = new BatchSpanProcessor(exporter);
259
+ }
260
+
261
+ /**
262
+ * Called when a new span is started; delegates to the inner batch processor.
263
+ *
264
+ * @param span - The span that was just started.
265
+ * @param parentContext - The parent {@link Context}.
266
+ */
267
+ onStart(span: Span, parentContext: Context): void {
268
+ this._spanProcessor.onStart(span, parentContext);
269
+ }
270
+
271
+ /**
272
+ * Called when a span ends. If the span originates from an OpenInference
273
+ * instrumentor, its attributes are converted to `gen_ai.*` semconv keys
274
+ * before being forwarded.
275
+ *
276
+ * @param span - The completed {@link ReadableSpan}.
277
+ */
278
+ onEnd(span: ReadableSpan): void {
279
+ const compatSpan = span as CompatibleSpan;
280
+ const scopeName =
281
+ (compatSpan.instrumentationScope as { name?: string })?.name ||
282
+ (compatSpan.instrumentationLibrary as { name?: string })?.name;
283
+
284
+ const isOI =
285
+ isOpenInferenceSpan(scopeName) ||
286
+ typeof span.attributes["openinference.span.kind"] === "string";
287
+ const isVercel = isVercelAISpan(span.attributes);
288
+
289
+ // Skip Vercel AI SDK wrapper spans (ai.streamText, ai.generateText).
290
+ // They duplicate the child doStream/doGenerate output without proper
291
+ // input message windows, causing empty user messages in conversation view.
292
+ // This applies regardless of whether OI attributes are also present.
293
+ const aiOperationId = span.attributes["ai.operationId"];
294
+ if (
295
+ typeof aiOperationId === "string" &&
296
+ !String(aiOperationId).includes(".do")
297
+ ) {
298
+ return;
299
+ }
300
+
301
+ // Skip spans that have no LLM-relevant data — they are infrastructure spans
302
+ // (e.g. Next.js route resolution, HTTP spans) that should not be exported.
303
+ const hasGenAi =
304
+ isOI ||
305
+ isVercel ||
306
+ span.attributes["gen_ai.system"] != null ||
307
+ span.attributes["gen_ai.operation.name"] != null ||
308
+ span.attributes["gen_ai.request.model"] != null ||
309
+ span.attributes["gen_ai.input.messages"] != null ||
310
+ span.attributes["gen_ai.output.messages"] != null;
311
+
312
+ if (!hasGenAi) return;
313
+
314
+ let attrs: Record<string, unknown>;
315
+ if (isOI) {
316
+ attrs = {
317
+ ...(replaceOpenInferenceWithGenAI(span.attributes) as Record<
318
+ string,
319
+ unknown
320
+ >),
321
+ };
322
+ // When both OI and Vercel attrs present, merge Vercel-specific
323
+ // enrichments (conversation ID from metadata, etc.)
324
+ if (isVercel) {
325
+ const vercelAttrs = convertVercelAIToGenAI(span.attributes);
326
+ for (const [key, value] of Object.entries(vercelAttrs)) {
327
+ // Prefer Vercel output messages when they contain reasoning
328
+ if (
329
+ key === "gen_ai.output.messages" &&
330
+ typeof value === "string" &&
331
+ value.includes('"thinking"')
332
+ ) {
333
+ attrs[key] = value;
334
+ } else if (attrs[key] === undefined) {
335
+ attrs[key] = value;
336
+ }
337
+ }
338
+ }
339
+ logger.debug(`Converting OpenInference span: ${span.name}`);
340
+ } else if (isVercel) {
341
+ attrs = {
342
+ ...span.attributes,
343
+ ...convertVercelAIToGenAI(span.attributes),
344
+ };
345
+ logger.debug(`Converting Vercel AI span: ${span.name}`);
346
+ } else {
347
+ attrs = { ...span.attributes };
348
+ }
349
+
350
+ // Read baggage from active context
351
+ const baggage = propagation.getBaggage(otelContext.active());
352
+
353
+ // Conversation ID: baggage > existing attr > auto-generate per trace
354
+ const baggageConvId = baggage?.getEntry("gen_ai.conversation.id")?.value;
355
+ const existingConvId = attrs["gen_ai.conversation.id"] as
356
+ | string
357
+ | undefined;
358
+ if (baggageConvId) {
359
+ attrs["gen_ai.conversation.id"] = baggageConvId;
360
+ } else if (!existingConvId) {
361
+ const traceId = span.spanContext().traceId;
362
+ if (!this._conversationIds.has(traceId)) {
363
+ this._conversationIds.set(
364
+ traceId,
365
+ `intro_conv_${randomUUID().replace(/-/g, "")}`,
366
+ );
367
+ }
368
+ attrs["gen_ai.conversation.id"] = this._conversationIds.get(traceId)!;
369
+ }
370
+
371
+ // Default gen_ai.operation.name to "chat" for spans with messages but no operation name
372
+ if (
373
+ !attrs["gen_ai.operation.name"] &&
374
+ (attrs["gen_ai.input.messages"] || attrs["gen_ai.output.messages"])
375
+ ) {
376
+ attrs["gen_ai.operation.name"] = "chat";
377
+ }
378
+
379
+ // Agent name from baggage
380
+ const baggageAgentName = baggage?.getEntry("gen_ai.agent.name")?.value;
381
+ if (baggageAgentName && !attrs["gen_ai.agent.name"]) {
382
+ attrs["gen_ai.agent.name"] = baggageAgentName;
383
+ }
384
+
385
+ // Override resource with service name if provided
386
+ let resource: Resource | undefined;
387
+ if (this._serviceName) {
388
+ resource = span.resource.merge(
389
+ resourceFromAttributes({ [ATTR_SERVICE_NAME]: this._serviceName }),
390
+ );
391
+ }
392
+
393
+ const processedSpan = new ConvertedReadableSpan(
394
+ span,
395
+ attrs as Attributes,
396
+ resource,
397
+ );
398
+ this._spanProcessor.onEnd(processedSpan);
399
+ }
400
+
401
+ /**
402
+ * Shut down the inner batch processor and its exporter.
403
+ *
404
+ * @returns A promise that resolves once all pending spans are flushed.
405
+ */
406
+ async shutdown(): Promise<void> {
407
+ await this._spanProcessor.shutdown();
408
+ }
409
+
410
+ /**
411
+ * Force-flush any buffered spans to the Introspection backend.
412
+ *
413
+ * @returns A promise that resolves once the flush completes.
414
+ */
415
+ async forceFlush(): Promise<void> {
416
+ await this._spanProcessor.forceFlush();
417
+ }
418
+ }