@telemetry-dev/tanstack-ai 0.1.0 → 0.1.1

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.
package/dist/index.d.mts CHANGED
@@ -17,7 +17,7 @@ interface TelemetryDevOptions {
17
17
  /** Serverless extender (e.g. Cloudflare `ctx.waitUntil`). When provided, `onFinish` does not await the POST. */
18
18
  waitUntil?: (p: Promise<unknown>) => void;
19
19
  /** Receives any error raised while emitting telemetry; the integration never throws into the SDK. */
20
- onError?: (error: unknown) => void;
20
+ onError?: (cause: unknown) => void;
21
21
  }
22
22
  //#endregion
23
23
  //#region src/otel.d.ts
package/dist/index.mjs CHANGED
@@ -6,7 +6,7 @@ import { BasicTracerProvider } from "@opentelemetry/sdk-trace-base";
6
6
  //#region src/config.ts
7
7
  const DEFAULT_BASE_URL = "https://ingest.telemetry.dev";
8
8
  function resolveConfig(options = {}) {
9
- const env = typeof process !== "undefined" && process.env ? process.env : {};
9
+ const env = globalThis.process?.env ?? {};
10
10
  const baseUrl = (options.baseUrl ?? env.TELEMETRY_DEV_BASE_URL ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
11
11
  return {
12
12
  apiKey: options.apiKey ?? env.TELEMETRY_DEV_API_KEY,
@@ -86,6 +86,7 @@ const postOtlp = async ({ fetchImpl, url, headers, body }) => {
86
86
  await delay(RETRY_DELAYS_MS[attempt]);
87
87
  }
88
88
  };
89
+ const isReadableSpan = (span) => "resource" in span && "instrumentationScope" in span && "events" in span;
89
90
  const cache = /* @__PURE__ */ new Map();
90
91
  const buildCore = (config) => {
91
92
  const { apiKey, baseUrl, environment, serviceName } = config;
@@ -95,7 +96,8 @@ const buildCore = (config) => {
95
96
  });
96
97
  const headers = {
97
98
  "content-type": "application/x-protobuf",
98
- authorization: `Bearer ${apiKey}`
99
+ authorization: `Bearer ${apiKey}`,
100
+ "x-telemetry-dev-sdk": "@telemetry-dev/tanstack-ai"
99
101
  };
100
102
  const tracer = new BasicTracerProvider({ resource }).getTracer(SCOPE_NAME, SCOPE_VERSION);
101
103
  const sendSpans = async (spans, transport) => {
@@ -137,11 +139,11 @@ const buildCore = (config) => {
137
139
  return;
138
140
  }
139
141
  resultCallback({ code: 0 });
140
- }).catch((error) => {
141
- onError?.(error);
142
+ }).catch((cause) => {
143
+ onError?.(cause);
142
144
  resultCallback({
143
145
  code: 1,
144
- error: error instanceof Error ? error : void 0
146
+ error: cause instanceof Error ? cause : void 0
145
147
  });
146
148
  });
147
149
  },
@@ -199,14 +201,14 @@ const createEmitter = (config, overrides) => {
199
201
  metrics = void 0;
200
202
  const p = (async () => {
201
203
  try {
202
- await sendSpans(spans);
204
+ await sendSpans(spans.filter(isReadableSpan));
203
205
  } catch (e) {
204
- onError?.(e);
206
+ onError?.(e instanceof Error ? e : new Error(String(e)));
205
207
  }
206
208
  try {
207
209
  if (pipeline) await pipeline.shutdown();
208
210
  } catch (e) {
209
- onError?.(e);
211
+ onError?.(e instanceof Error ? e : new Error(String(e)));
210
212
  }
211
213
  })();
212
214
  if (config.waitUntil) {
@@ -224,6 +226,10 @@ const createEmitter = (config, overrides) => {
224
226
  };
225
227
  //#endregion
226
228
  //#region src/middleware.ts
229
+ const isString = (value) => typeof value === "string";
230
+ const isNumber = (value) => typeof value === "number";
231
+ const isBigInt = (value) => typeof value === "bigint";
232
+ const isObject = (value) => value !== null && typeof value === "object";
227
233
  function omitUndefined(attributes) {
228
234
  const out = {};
229
235
  for (const key of Object.keys(attributes)) {
@@ -233,13 +239,13 @@ function omitUndefined(attributes) {
233
239
  return out;
234
240
  }
235
241
  function readId(value) {
236
- if (typeof value === "string") return value.length > 0 ? value : null;
237
- if (typeof value === "number" || typeof value === "bigint") return value.toString();
242
+ if (isString(value)) return value.length > 0 ? value : null;
243
+ if (isNumber(value) || isBigInt(value)) return value.toString();
238
244
  return null;
239
245
  }
240
246
  function jsonAttr(value) {
241
247
  if (value === void 0) return void 0;
242
- if (typeof value === "string") return value;
248
+ if (isString(value)) return value;
243
249
  try {
244
250
  return JSON.stringify(value);
245
251
  } catch {
@@ -247,22 +253,22 @@ function jsonAttr(value) {
247
253
  }
248
254
  }
249
255
  function firstNumber(...candidates) {
250
- for (const candidate of candidates) if (typeof candidate === "number" && Number.isFinite(candidate)) return candidate;
256
+ for (const candidate of candidates) if (isNumber(candidate) && Number.isFinite(candidate)) return candidate;
251
257
  }
252
258
  function errorTypeName(err) {
253
259
  if (err instanceof Error) return err.name || "Error";
254
- if (err && typeof err === "object" && "name" in err) {
260
+ if (err && isObject(err) && "name" in err) {
255
261
  const n = err.name;
256
- if (typeof n === "string" && n.length > 0) return n;
262
+ if (isString(n) && n.length > 0) return n;
257
263
  }
258
264
  return "Error";
259
265
  }
260
266
  function errorMessage(err) {
261
267
  if (err instanceof Error) return err.message;
262
- if (typeof err === "string") return err;
263
- if (err && typeof err === "object" && "message" in err) {
268
+ if (isString(err)) return err;
269
+ if (err && isObject(err) && "message" in err) {
264
270
  const m = err.message;
265
- if (typeof m === "string") return m;
271
+ if (isString(m)) return m;
266
272
  }
267
273
  return String(err);
268
274
  }
@@ -278,7 +284,7 @@ const MAX_TOKENS_KEYS = [
278
284
  ];
279
285
  function samplingAttributes(modelOptions) {
280
286
  const sampling = modelOptions ?? {};
281
- const nested = sampling["options"] && typeof sampling["options"] === "object" ? sampling["options"] : void 0;
287
+ const nested = sampling["options"] && isObject(sampling["options"]) ? sampling["options"] : void 0;
282
288
  return omitUndefined({
283
289
  "gen_ai.request.temperature": firstNumber(sampling["temperature"], nested?.["temperature"]),
284
290
  "gen_ai.request.top_p": firstNumber(sampling["top_p"], sampling["topP"], nested?.["top_p"]),
@@ -337,7 +343,7 @@ function telemetryDev(options, overrides) {
337
343
  ...state.rootSampling
338
344
  }));
339
345
  if (state.restMetadata) for (const [key, value] of Object.entries(state.restMetadata)) {
340
- const attr = typeof value === "string" ? value : jsonAttr(value);
346
+ const attr = isString(value) ? value : jsonAttr(value);
341
347
  if (attr !== void 0) state.rootSpan.setAttribute(`td.metadata.${key}`, attr);
342
348
  }
343
349
  };
@@ -398,7 +404,7 @@ function telemetryDev(options, overrides) {
398
404
  onStart(ctx) {
399
405
  try {
400
406
  const rawMetadata = ctx.options?.["metadata"];
401
- const metadata = rawMetadata && typeof rawMetadata === "object" ? rawMetadata : void 0;
407
+ const metadata = rawMetadata && isObject(rawMetadata) ? rawMetadata : void 0;
402
408
  const userId = readId(metadata?.["userId"]);
403
409
  const sessionId = readId(metadata?.["sessionId"]) ?? ctx.threadId;
404
410
  let restMetadata;
@@ -444,7 +450,7 @@ function telemetryDev(options, overrides) {
444
450
  const inputMessages = [];
445
451
  for (const prompt of chatConfig.systemPrompts) inputMessages.push({
446
452
  role: "system",
447
- content: typeof prompt === "string" ? prompt : prompt.content
453
+ content: isString(prompt) ? prompt : prompt.content
448
454
  });
449
455
  for (const message of chatConfig.messages) inputMessages.push({
450
456
  role: message.role,
@@ -493,7 +499,7 @@ function telemetryDev(options, overrides) {
493
499
  if (chunk.type === "CUSTOM") {
494
500
  if (iteration.structured && chunk.name === "structured-output.complete") {
495
501
  const raw = chunk.value?.raw;
496
- if (typeof raw === "string") iteration.outputText = raw;
502
+ if (isString(raw)) iteration.outputText = raw;
497
503
  }
498
504
  return;
499
505
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@telemetry-dev/tanstack-ai",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "TanStack AI telemetry integration for telemetry.dev: chat() middleware emitting OpenTelemetry GenAI spans.",
5
5
  "keywords": [
6
6
  "genai",
package/src/config.ts CHANGED
@@ -12,7 +12,7 @@ export interface TelemetryDevOptions {
12
12
  /** Serverless extender (e.g. Cloudflare `ctx.waitUntil`). When provided, `onFinish` does not await the POST. */
13
13
  waitUntil?: (p: Promise<unknown>) => void;
14
14
  /** Receives any error raised while emitting telemetry; the integration never throws into the SDK. */
15
- onError?: (error: unknown) => void;
15
+ onError?: (cause: unknown) => void;
16
16
  }
17
17
 
18
18
  export interface ResolvedConfig {
@@ -22,16 +22,13 @@ export interface ResolvedConfig {
22
22
  serviceName: string;
23
23
  fetchImpl: typeof fetch;
24
24
  waitUntil?: (p: Promise<unknown>) => void;
25
- onError?: (error: unknown) => void;
25
+ onError?: (cause: unknown) => void;
26
26
  }
27
27
 
28
28
  const DEFAULT_BASE_URL = "https://ingest.telemetry.dev";
29
29
 
30
30
  export function resolveConfig(options: TelemetryDevOptions = {}): ResolvedConfig {
31
- const env =
32
- typeof process !== "undefined" && process.env
33
- ? process.env
34
- : ({} as Record<string, string | undefined>);
31
+ const env = globalThis.process?.env ?? {};
35
32
  const baseUrl = (options.baseUrl ?? env.TELEMETRY_DEV_BASE_URL ?? DEFAULT_BASE_URL).replace(
36
33
  /\/+$/,
37
34
  "",
package/src/middleware.ts CHANGED
@@ -24,6 +24,22 @@ import type {
24
24
  import { resolveConfig, type TelemetryDevOptions } from "./config.ts";
25
25
  import { createEmitter, type EmitterOverrides } from "./otel.ts";
26
26
 
27
+ type JsonValue =
28
+ | string
29
+ | number
30
+ | bigint
31
+ | boolean
32
+ | null
33
+ | undefined
34
+ | JsonValue[]
35
+ | { [key: string]: JsonValue };
36
+
37
+ const isString = (value: unknown): value is string => typeof value === "string";
38
+ const isNumber = (value: unknown): value is number => typeof value === "number";
39
+ const isBigInt = (value: unknown): value is bigint => typeof value === "bigint";
40
+ const isObject = <T>(value: T): value is T & { [key: string]: JsonValue } =>
41
+ value !== null && typeof value === "object";
42
+
27
43
  function omitUndefined(attributes: Attributes): Attributes {
28
44
  const out: Attributes = {};
29
45
  for (const key of Object.keys(attributes)) {
@@ -35,11 +51,11 @@ function omitUndefined(attributes: Attributes): Attributes {
35
51
  return out;
36
52
  }
37
53
 
38
- function readId(value: unknown): string | null {
39
- if (typeof value === "string") {
54
+ function readId<T>(value: T): string | null {
55
+ if (isString(value)) {
40
56
  return value.length > 0 ? value : null;
41
57
  }
42
- if (typeof value === "number" || typeof value === "bigint") {
58
+ if (isNumber(value) || isBigInt(value)) {
43
59
  return value.toString();
44
60
  }
45
61
  return null;
@@ -47,9 +63,9 @@ function readId(value: unknown): string | null {
47
63
 
48
64
  // Stringify structured content (messages / tool args) for the gen_ai.* string attributes the
49
65
  // ingest parses back into JSON. Returns undefined so omitUndefined drops absent content.
50
- function jsonAttr(value: unknown): string | undefined {
66
+ function jsonAttr<T>(value: T): string | undefined {
51
67
  if (value === undefined) return undefined;
52
- if (typeof value === "string") return value;
68
+ if (isString(value)) return value;
53
69
  try {
54
70
  return JSON.stringify(value);
55
71
  } catch {
@@ -57,30 +73,30 @@ function jsonAttr(value: unknown): string | undefined {
57
73
  }
58
74
  }
59
75
 
60
- function firstNumber(...candidates: unknown[]): number | undefined {
76
+ function firstNumber<T>(...candidates: T[]): number | undefined {
61
77
  for (const candidate of candidates) {
62
- if (typeof candidate === "number" && Number.isFinite(candidate)) {
78
+ if (isNumber(candidate) && Number.isFinite(candidate)) {
63
79
  return candidate;
64
80
  }
65
81
  }
66
82
  return undefined;
67
83
  }
68
84
 
69
- function errorTypeName(err: unknown): string {
85
+ function errorTypeName<T>(err: T): string {
70
86
  if (err instanceof Error) return err.name || "Error";
71
- if (err && typeof err === "object" && "name" in err) {
87
+ if (err && isObject(err) && "name" in err) {
72
88
  const n = (err as { name?: unknown }).name;
73
- if (typeof n === "string" && n.length > 0) return n;
89
+ if (isString(n) && n.length > 0) return n;
74
90
  }
75
91
  return "Error";
76
92
  }
77
93
 
78
- function errorMessage(err: unknown): string {
94
+ function errorMessage<T>(err: T): string {
79
95
  if (err instanceof Error) return err.message;
80
- if (typeof err === "string") return err;
81
- if (err && typeof err === "object" && "message" in err) {
96
+ if (isString(err)) return err;
97
+ if (err && isObject(err) && "message" in err) {
82
98
  const m = (err as { message?: unknown }).message;
83
- if (typeof m === "string") return m;
99
+ if (isString(m)) return m;
84
100
  }
85
101
  return String(err);
86
102
  }
@@ -101,11 +117,11 @@ const MAX_TOKENS_KEYS = [
101
117
 
102
118
  // Sampling options live in opaque provider-native `modelOptions`; pick the first numeric value
103
119
  // among the known spellings (including Ollama's nested `options`) for the gen_ai.request.* attrs.
104
- function samplingAttributes(modelOptions: Record<string, unknown> | undefined): Attributes {
120
+ function samplingAttributes(modelOptions: Record<string, JsonValue> | undefined): Attributes {
105
121
  const sampling = modelOptions ?? {};
106
122
  const nested =
107
- sampling["options"] && typeof sampling["options"] === "object"
108
- ? (sampling["options"] as Record<string, unknown>)
123
+ sampling["options"] && isObject(sampling["options"])
124
+ ? (sampling["options"] as Record<string, JsonValue>)
109
125
  : undefined;
110
126
  return omitUndefined({
111
127
  "gen_ai.request.temperature": firstNumber(sampling["temperature"], nested?.["temperature"]),
@@ -136,7 +152,7 @@ interface RunState {
136
152
  responseModel: string | null;
137
153
  userId: string | null;
138
154
  sessionId: string;
139
- restMetadata: Record<string, unknown> | undefined;
155
+ restMetadata: Record<string, JsonValue> | undefined;
140
156
  rootInput: string | undefined;
141
157
  rootSampling: Attributes;
142
158
  rootCaptured: boolean;
@@ -229,7 +245,7 @@ export function telemetryDev(
229
245
  );
230
246
  if (state.restMetadata) {
231
247
  for (const [key, value] of Object.entries(state.restMetadata)) {
232
- const attr = typeof value === "string" ? value : jsonAttr(value);
248
+ const attr = isString(value) ? value : jsonAttr(value);
233
249
  if (attr !== undefined) {
234
250
  state.rootSpan.setAttribute(`td.metadata.${key}`, attr);
235
251
  }
@@ -297,14 +313,14 @@ export function telemetryDev(
297
313
  try {
298
314
  const rawMetadata = ctx.options?.["metadata"];
299
315
  const metadata =
300
- rawMetadata && typeof rawMetadata === "object"
301
- ? (rawMetadata as Record<string, unknown>)
316
+ rawMetadata && isObject(rawMetadata)
317
+ ? (rawMetadata as Record<string, JsonValue>)
302
318
  : undefined;
303
319
  const userId = readId(metadata?.["userId"]);
304
320
  const sessionId = readId(metadata?.["sessionId"]) ?? ctx.threadId;
305
- let restMetadata: Record<string, unknown> | undefined;
321
+ let restMetadata: Record<string, JsonValue> | undefined;
306
322
  if (metadata) {
307
- const rest: Record<string, unknown> = {};
323
+ const rest: Record<string, JsonValue> = {};
308
324
  for (const [key, value] of Object.entries(metadata)) {
309
325
  if (key !== "userId" && key !== "sessionId") {
310
326
  rest[key] = value;
@@ -360,14 +376,16 @@ export function telemetryDev(
360
376
  for (const prompt of chatConfig.systemPrompts) {
361
377
  inputMessages.push({
362
378
  role: "system",
363
- content: typeof prompt === "string" ? prompt : prompt.content,
379
+ content: isString(prompt) ? prompt : prompt.content,
364
380
  });
365
381
  }
366
382
  for (const message of chatConfig.messages) {
367
383
  inputMessages.push({ role: message.role, content: message.content });
368
384
  }
369
385
  const inputJson = jsonAttr(inputMessages);
370
- const sampling = samplingAttributes(chatConfig.modelOptions ?? ctx.modelOptions);
386
+ const sampling = samplingAttributes(
387
+ (chatConfig.modelOptions ?? ctx.modelOptions) as Record<string, JsonValue> | undefined,
388
+ );
371
389
  if (!state.rootCaptured) {
372
390
  state.rootCaptured = true;
373
391
  state.rootInput = inputJson;
@@ -418,7 +436,7 @@ export function telemetryDev(
418
436
  // still holds the agent loop's text, so this is the structured span's only output.
419
437
  if (iteration.structured && chunk.name === "structured-output.complete") {
420
438
  const raw = (chunk.value as { raw?: unknown } | null | undefined)?.raw;
421
- if (typeof raw === "string") iteration.outputText = raw;
439
+ if (isString(raw)) iteration.outputText = raw;
422
440
  }
423
441
  return undefined;
424
442
  }
package/src/otel.ts CHANGED
@@ -44,7 +44,7 @@ export interface EmitterOverrides {
44
44
  recordTokens?: (tokenType: "input" | "output", count: number, attributes: Attributes) => void;
45
45
  }
46
46
 
47
- type Transport = { fetchImpl: typeof fetch; onError?: (error: unknown) => void };
47
+ type Transport = { fetchImpl: typeof fetch; onError?: (cause: unknown) => void };
48
48
 
49
49
  const RETRY_DELAYS_MS = [100, 500] as const;
50
50
 
@@ -97,6 +97,9 @@ interface EmitterCore {
97
97
  buildMetrics: (transport: Transport) => MetricsPipeline;
98
98
  }
99
99
 
100
+ const isReadableSpan = (span: Span): span is Span & ReadableSpan =>
101
+ "resource" in span && "instrumentationScope" in span && "events" in span;
102
+
100
103
  const cache = new Map<string, EmitterCore>();
101
104
 
102
105
  const buildCore = (config: ResolvedConfig): EmitterCore => {
@@ -108,6 +111,7 @@ const buildCore = (config: ResolvedConfig): EmitterCore => {
108
111
  const headers = {
109
112
  "content-type": "application/x-protobuf",
110
113
  authorization: `Bearer ${apiKey}`,
114
+ "x-telemetry-dev-sdk": "@telemetry-dev/tanstack-ai",
111
115
  };
112
116
 
113
117
  const provider = new BasicTracerProvider({ resource });
@@ -148,9 +152,9 @@ const buildCore = (config: ResolvedConfig): EmitterCore => {
148
152
  }
149
153
  resultCallback({ code: 0 });
150
154
  })
151
- .catch((error: unknown) => {
152
- onError?.(error);
153
- resultCallback({ code: 1, error: error instanceof Error ? error : undefined });
155
+ .catch((cause: unknown) => {
156
+ onError?.(cause);
157
+ resultCallback({ code: 1, error: cause instanceof Error ? cause : undefined });
154
158
  });
155
159
  },
156
160
  selectAggregationTemporality: () => AggregationTemporality.DELTA,
@@ -226,14 +230,15 @@ export const createEmitter = (config: ResolvedConfig, overrides?: EmitterOverrid
226
230
  metrics = undefined;
227
231
  const p = (async () => {
228
232
  try {
229
- await sendSpans(spans as unknown as ReadableSpan[]);
233
+ const readableSpans = spans.filter(isReadableSpan);
234
+ await sendSpans(readableSpans);
230
235
  } catch (e) {
231
- onError?.(e);
236
+ onError?.(e instanceof Error ? e : new Error(String(e)));
232
237
  }
233
238
  try {
234
239
  if (pipeline) await pipeline.shutdown();
235
240
  } catch (e) {
236
- onError?.(e);
241
+ onError?.(e instanceof Error ? e : new Error(String(e)));
237
242
  }
238
243
  })();
239
244
  if (config.waitUntil) {