@posthog/ai 8.13.7 → 8.14.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.
@@ -3,7 +3,7 @@ let _google_adk = require("@google/adk");
3
3
  let uuid = require("uuid");
4
4
  let _posthog_core = require("@posthog/core");
5
5
  //#region package.json
6
- var version = "8.13.7";
6
+ var version = "8.14.0";
7
7
  //#endregion
8
8
  //#region src/captureAiEvent.ts
9
9
  /** @internal */
@@ -2,7 +2,7 @@ import { BasePlugin } from "@google/adk";
2
2
  import { v4 } from "uuid";
3
3
  import { toJsonSafeValue, uuidv7 } from "@posthog/core";
4
4
  //#region package.json
5
- var version = "8.13.7";
5
+ var version = "8.14.0";
6
6
  //#endregion
7
7
  //#region src/captureAiEvent.ts
8
8
  /** @internal */
@@ -367,7 +367,7 @@ function addDefaults(params) {
367
367
  }
368
368
  //#endregion
369
369
  //#region package.json
370
- var version = "8.13.7";
370
+ var version = "8.14.0";
371
371
  //#endregion
372
372
  //#region src/serializeError.ts
373
373
  const DEFAULT_MAX_DEPTH = 3;
@@ -340,7 +340,7 @@ function addDefaults(params) {
340
340
  }
341
341
  //#endregion
342
342
  //#region package.json
343
- var version = "8.13.7";
343
+ var version = "8.14.0";
344
344
  //#endregion
345
345
  //#region src/serializeError.ts
346
346
  const DEFAULT_MAX_DEPTH = 3;
@@ -3,7 +3,7 @@ let _anthropic_ai_claude_agent_sdk = require("@anthropic-ai/claude-agent-sdk");
3
3
  let uuid = require("uuid");
4
4
  let _posthog_core = require("@posthog/core");
5
5
  //#region package.json
6
- var version = "8.13.7";
6
+ var version = "8.14.0";
7
7
  //#endregion
8
8
  //#region src/captureAiEvent.ts
9
9
  /** @internal */
@@ -2,7 +2,7 @@ import { SYSTEM_PROMPT_DYNAMIC_BOUNDARY, query as query$1 } from "@anthropic-ai/
2
2
  import { v4 } from "uuid";
3
3
  import { toJsonSafeValue, uuidv7 } from "@posthog/core";
4
4
  //#region package.json
5
- var version = "8.13.7";
5
+ var version = "8.14.0";
6
6
  //#endregion
7
7
  //#region src/captureAiEvent.ts
8
8
  /** @internal */
@@ -439,7 +439,7 @@ function addDefaults(params) {
439
439
  }
440
440
  //#endregion
441
441
  //#region package.json
442
- var version = "8.13.7";
442
+ var version = "8.14.0";
443
443
  //#endregion
444
444
  //#region src/serializeError.ts
445
445
  const DEFAULT_MAX_DEPTH = 3;
@@ -435,7 +435,7 @@ function addDefaults(params) {
435
435
  }
436
436
  //#endregion
437
437
  //#region package.json
438
- var version = "8.13.7";
438
+ var version = "8.14.0";
439
439
  //#endregion
440
440
  //#region src/serializeError.ts
441
441
  const DEFAULT_MAX_DEPTH = 3;
package/dist/index.cjs CHANGED
@@ -396,7 +396,7 @@ function sanitizeValues(obj) {
396
396
  }
397
397
  //#endregion
398
398
  //#region package.json
399
- var version = "8.13.7";
399
+ var version = "8.14.0";
400
400
  //#endregion
401
401
  //#region src/serializeError.ts
402
402
  const DEFAULT_MAX_DEPTH = 3;
package/dist/index.mjs CHANGED
@@ -395,7 +395,7 @@ function sanitizeValues(obj) {
395
395
  }
396
396
  //#endregion
397
397
  //#region package.json
398
- var version = "8.13.7";
398
+ var version = "8.14.0";
399
399
  //#endregion
400
400
  //#region src/serializeError.ts
401
401
  const DEFAULT_MAX_DEPTH = 3;
@@ -261,7 +261,7 @@ function sanitizeValues(obj) {
261
261
  }
262
262
  //#endregion
263
263
  //#region package.json
264
- var version = "8.13.7";
264
+ var version = "8.14.0";
265
265
  //#endregion
266
266
  //#region src/serializeError.ts
267
267
  const DEFAULT_MAX_DEPTH = 3;
@@ -260,7 +260,7 @@ function sanitizeValues(obj) {
260
260
  }
261
261
  //#endregion
262
262
  //#region package.json
263
- var version = "8.13.7";
263
+ var version = "8.14.0";
264
264
  //#endregion
265
265
  //#region src/serializeError.ts
266
266
  const DEFAULT_MAX_DEPTH = 3;
@@ -266,7 +266,7 @@ function sanitizeValues(obj) {
266
266
  }
267
267
  //#endregion
268
268
  //#region package.json
269
- var version = "8.13.7";
269
+ var version = "8.14.0";
270
270
  //#endregion
271
271
  //#region src/serializeError.ts
272
272
  const DEFAULT_MAX_DEPTH = 3;
@@ -265,7 +265,7 @@ function sanitizeValues(obj) {
265
265
  }
266
266
  //#endregion
267
267
  //#region package.json
268
- var version = "8.13.7";
268
+ var version = "8.14.0";
269
269
  //#endregion
270
270
  //#region src/serializeError.ts
271
271
  const DEFAULT_MAX_DEPTH = 3;
@@ -499,7 +499,7 @@ function formatOpenAIResponsesInput(input, instructions) {
499
499
  }
500
500
  //#endregion
501
501
  //#region package.json
502
- var version = "8.13.7";
502
+ var version = "8.14.0";
503
503
  //#endregion
504
504
  //#region src/serializeError.ts
505
505
  const DEFAULT_MAX_DEPTH = 3;
@@ -495,7 +495,7 @@ function formatOpenAIResponsesInput(input, instructions) {
495
495
  }
496
496
  //#endregion
497
497
  //#region package.json
498
- var version = "8.13.7";
498
+ var version = "8.14.0";
499
499
  //#endregion
500
500
  //#region src/serializeError.ts
501
501
  const DEFAULT_MAX_DEPTH = 3;
@@ -245,7 +245,7 @@ const truncate = (input, client, maxBytes = MAX_OUTPUT_SIZE) => {
245
245
  };
246
246
  //#endregion
247
247
  //#region package.json
248
- var version = "8.13.7";
248
+ var version = "8.14.0";
249
249
  //#endregion
250
250
  //#region src/gatewayWarning.ts
251
251
  const POSTHOG_AI_GATEWAY_HOSTS = [
@@ -244,7 +244,7 @@ const truncate = (input, client, maxBytes = MAX_OUTPUT_SIZE) => {
244
244
  };
245
245
  //#endregion
246
246
  //#region package.json
247
- var version = "8.13.7";
247
+ var version = "8.14.0";
248
248
  //#endregion
249
249
  //#region src/gatewayWarning.ts
250
250
  const POSTHOG_AI_GATEWAY_HOSTS = [
@@ -175,25 +175,47 @@ var BinaryContentRedactor = class {
175
175
  }
176
176
  };
177
177
  //#endregion
178
- //#region src/otel/redact.ts
179
- const redactor = new BinaryContentRedactor();
180
- function redactSpan(span) {
181
- const attributes = span.attributes ? redactAttributes(span.attributes) : span.attributes;
182
- const events = span.events ? redactEvents(span.events) : span.events;
183
- if (attributes === span.attributes && events === span.events) return span;
178
+ //#region src/otel/spans.ts
179
+ const AI_SPAN_PREFIXES = [
180
+ "gen_ai.",
181
+ "llm.",
182
+ "ai.",
183
+ "traceloop."
184
+ ];
185
+ /**
186
+ * Returns `true` when the span is AI-related — its name or any attribute
187
+ * key starts with `gen_ai.`, `llm.`, `ai.`, or `traceloop.`.
188
+ */
189
+ function isAISpan(span) {
190
+ if (AI_SPAN_PREFIXES.some((prefix) => span.name.startsWith(prefix))) return true;
191
+ const attributes = span.attributes;
192
+ if (attributes) return Object.keys(attributes).some((key) => AI_SPAN_PREFIXES.some((prefix) => key.startsWith(prefix)));
193
+ return false;
194
+ }
195
+ /** Copies rather than mutates: the span is shared across every registered processor/exporter. */
196
+ function withAttributes(span, attributes, events) {
184
197
  return Object.create(span, {
185
198
  attributes: {
186
199
  value: attributes,
187
200
  enumerable: true,
188
201
  configurable: true
189
202
  },
190
- events: {
203
+ ...events ? { events: {
191
204
  value: events,
192
205
  enumerable: true,
193
206
  configurable: true
194
- }
207
+ } } : {}
195
208
  });
196
209
  }
210
+ //#endregion
211
+ //#region src/otel/redact.ts
212
+ const redactor = new BinaryContentRedactor();
213
+ function redactSpan(span) {
214
+ const attributes = span.attributes ? redactAttributes(span.attributes) : span.attributes;
215
+ const events = span.events ? redactEvents(span.events) : span.events;
216
+ if (attributes === span.attributes && events === span.events) return span;
217
+ return withAttributes(span, attributes, events);
218
+ }
197
219
  function redactAttributes(attributes) {
198
220
  let changed = false;
199
221
  const out = {};
@@ -256,24 +278,6 @@ function redactJson(value) {
256
278
  return redactedStr === JSON.stringify(parsed) ? value : redactedStr;
257
279
  }
258
280
  //#endregion
259
- //#region src/otel/spans.ts
260
- const AI_SPAN_PREFIXES = [
261
- "gen_ai.",
262
- "llm.",
263
- "ai.",
264
- "traceloop."
265
- ];
266
- /**
267
- * Returns `true` when the span is AI-related — its name or any attribute
268
- * key starts with `gen_ai.`, `llm.`, `ai.`, or `traceloop.`.
269
- */
270
- function isAISpan(span) {
271
- if (AI_SPAN_PREFIXES.some((prefix) => span.name.startsWith(prefix))) return true;
272
- const attributes = span.attributes;
273
- if (attributes) return Object.keys(attributes).some((key) => AI_SPAN_PREFIXES.some((prefix) => key.startsWith(prefix)));
274
- return false;
275
- }
276
- //#endregion
277
281
  //#region src/gatewayWarning.ts
278
282
  const POSTHOG_AI_GATEWAY_HOSTS = [
279
283
  "gateway.posthog.com",
@@ -338,6 +342,9 @@ function normalizeHost$1(value) {
338
342
  * `projectToken` is required; a blank token disables the exporter as a defensive no-op.
339
343
  * You can also optionally override the `host` URL.
340
344
  *
345
+ * Exporters only see finished spans, so they can't add per-request PostHog context;
346
+ * use {@link PostHogSpanProcessor} with `client` for that.
347
+ *
341
348
  * @example
342
349
  * ```ts
343
350
  * import { PostHogTraceExporter } from '@posthog/ai/otel'
@@ -376,6 +383,76 @@ var PostHogTraceExporter = class extends _opentelemetry_exporter_trace_otlp_http
376
383
  }
377
384
  };
378
385
  //#endregion
386
+ //#region src/otel/requestContext.ts
387
+ const DISTINCT_ID_ATTRIBUTE = "posthog.distinct_id";
388
+ const SESSION_ID_ATTRIBUTE = "$session_id";
389
+ const IDENTITY_ATTRIBUTES = [
390
+ DISTINCT_ID_ATTRIBUTE,
391
+ "user.id",
392
+ "ai.telemetry.metadata.posthog_distinct_id",
393
+ "ai.settings.context.posthog_distinct_id"
394
+ ];
395
+ function isPrimitive(value) {
396
+ return typeof value === "string" || typeof value === "boolean" || typeof value === "number" && Number.isFinite(value);
397
+ }
398
+ function isHomogeneousPrimitiveArray(value) {
399
+ return Array.isArray(value) && value.every((item) => isPrimitive(item) && typeof item === typeof value[0]);
400
+ }
401
+ function rejectNonFinite(_key, value) {
402
+ if (typeof value === "number" && !Number.isFinite(value)) throw new TypeError("non-finite number");
403
+ return value;
404
+ }
405
+ function toAttributeValue(value) {
406
+ if (value === void 0 || value === null || value === "") return;
407
+ if (isPrimitive(value) || isHomogeneousPrimitiveArray(value)) return value;
408
+ try {
409
+ const json = JSON.stringify(value, rejectNonFinite);
410
+ if (json === void 0 || json === "null") return;
411
+ return json.startsWith("\"") ? String(JSON.parse(json)) || void 0 : json;
412
+ } catch {
413
+ return;
414
+ }
415
+ }
416
+ function hasValues(context) {
417
+ return Boolean(context.distinctId || context.sessionId || Object.keys(context.properties ?? {}).length > 0);
418
+ }
419
+ function captureContext(client) {
420
+ try {
421
+ if (typeof client.getContext !== "function") return;
422
+ const context = client.getContext();
423
+ return context && hasValues(context) ? context : void 0;
424
+ } catch {
425
+ return;
426
+ }
427
+ }
428
+ function toAttributes(context, spanAttributes) {
429
+ const hasIdentity = IDENTITY_ATTRIBUTES.some((key) => spanAttributes[key] !== void 0);
430
+ const values = {
431
+ ...context.distinctId && !hasIdentity ? { [DISTINCT_ID_ATTRIBUTE]: context.distinctId } : {},
432
+ ...context.sessionId ? { [SESSION_ID_ATTRIBUTE]: context.sessionId } : {},
433
+ ...context.properties
434
+ };
435
+ const attributes = {};
436
+ for (const [key, value] of Object.entries(values)) {
437
+ if (hasIdentity && key === DISTINCT_ID_ATTRIBUTE) continue;
438
+ const attribute = toAttributeValue(value);
439
+ if (attribute !== void 0) attributes[key] = attribute;
440
+ }
441
+ return attributes;
442
+ }
443
+ function withRequestContext(span, context) {
444
+ try {
445
+ const attributes = toAttributes(context, span.attributes);
446
+ if (Object.keys(attributes).length === 0) return span;
447
+ return withAttributes(span, {
448
+ ...attributes,
449
+ ...span.attributes
450
+ });
451
+ } catch {
452
+ return span;
453
+ }
454
+ }
455
+ //#endregion
379
456
  //#region src/otel/processor.ts
380
457
  const DEFAULT_OTEL_HOST = "https://us.i.posthog.com";
381
458
  function normalizeToken(value) {
@@ -424,15 +501,24 @@ var NoopSpanProcessor = class {
424
501
  * // In request-scoped runtimes, wait for queued exports before returning.
425
502
  * await processor.forceFlush()
426
503
  * ```
504
+ *
505
+ * @example
506
+ * ```ts
507
+ * // Per request: pass your posthog-node client, then wrap the work in a context.
508
+ * const processor = new PostHogSpanProcessor({ projectToken: 'phc_...', client: posthog })
509
+ * await posthog.withContext({ distinctId: user.id, properties: { plan: 'pro' } }, () => handleRequest())
510
+ * ```
427
511
  */
428
512
  var PostHogSpanProcessor = class {
429
513
  constructor(options) {
514
+ this.capturedContexts = /* @__PURE__ */ new WeakMap();
430
515
  const token = normalizeToken(options.projectToken);
431
516
  if (!token) {
432
517
  console.warn("[PostHogSpanProcessor] projectToken is missing or blank; the processor will be disabled.");
433
518
  this.inner = new NoopSpanProcessor();
434
519
  return;
435
520
  }
521
+ this.client = options.client;
436
522
  if (options._spanProcessor) this.inner = options._spanProcessor;
437
523
  else {
438
524
  const host = new URL(normalizeHost(options.host)).origin;
@@ -444,12 +530,16 @@ var PostHogSpanProcessor = class {
444
530
  }
445
531
  }
446
532
  onStart(span, parentContext) {
533
+ const captured = this.client && captureContext(this.client);
534
+ if (captured) this.capturedContexts.set(span, captured);
447
535
  this.inner.onStart(span, parentContext);
448
536
  }
449
537
  onEnd(span) {
538
+ const captured = this.capturedContexts.get(span);
539
+ this.capturedContexts.delete(span);
450
540
  if (isAISpan(span)) {
451
541
  warnIfPostHogAiGatewayOtelAttributes(span.attributes);
452
- this.inner.onEnd(redactSpan(span));
542
+ this.inner.onEnd(redactSpan(captured ? withRequestContext(span, captured) : span));
453
543
  }
454
544
  }
455
545
  shutdown() {
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs","names":["DEFAULT_OTEL_HOST","normalizeToken","normalizeHost","OTLPTraceExporter","OTLPTraceExporter","BatchSpanProcessor"],"sources":["../../src/sanitization/base64_recognizer.ts","../../src/sanitization/media_type_context.ts","../../src/sanitization/binary_content_redactor.ts","../../src/otel/redact.ts","../../src/otel/spans.ts","../../src/gatewayWarning.ts","../../src/otel/exporter.ts","../../src/otel/processor.ts"],"sourcesContent":["const DATA_URL_PREFIX_RE = /^data:([^;,\\s]+)(?:;[^;,\\s]+)*;base64,/i\nconst BASE64_ALPHABET_RE = /^[A-Za-z0-9+/_=-]+$/\n\nexport type Base64Recognition = { kind: 'data-url'; mediaType: string } | { kind: 'raw' } | { kind: 'none' }\n\nexport class Base64Recognizer {\n recognize(value: string, minLength: number): Base64Recognition {\n const dataUrl = DATA_URL_PREFIX_RE.exec(value)\n if (dataUrl) return { kind: 'data-url', mediaType: dataUrl[1] }\n\n if (value.length < minLength) return { kind: 'none' }\n\n const confidencePrefix = value.slice(0, minLength)\n if (BASE64_ALPHABET_RE.test(confidencePrefix)) {\n return { kind: 'raw' }\n } else {\n return { kind: 'none' }\n }\n }\n}\n","const MIME_HINT_KEYS = ['mediaType', 'media_type', 'mimeType', 'mime_type'] as const\n\nconst STRONG_CONTEXT_KEYS = new Set([\n 'data',\n 'file_data',\n 'fileData',\n 'image_url',\n 'imageUrl',\n 'video_url',\n 'videoUrl',\n 'audio',\n 'audio_data',\n 'audioData',\n 'inline_data',\n 'inlineData',\n 'source',\n 'result',\n])\n\nconst STRONG_CONTEXT_TYPES = new Set([\n 'image',\n 'image_url',\n 'input_image',\n 'audio',\n 'input_audio',\n 'video',\n 'video_url',\n 'file',\n 'input_file',\n 'document',\n 'media',\n 'file-data',\n])\n\nconst FILE_FAMILY_TYPES = new Set(['file', 'input_file', 'document', 'media', 'file-data'])\n\nconst KNOWN_AUDIO_FORMATS = new Set(['wav', 'mp3', 'ogg', 'flac', 'm4a', 'aac', 'webm'])\n\nexport class MediaTypeContext {\n static readonly EMPTY = new MediaTypeContext(undefined, undefined)\n\n constructor(\n private readonly parent: Record<string, unknown> | undefined,\n private readonly key: string | undefined,\n private readonly explicitMediaType?: string\n ) {}\n\n inferMediaType(): string | undefined {\n return (\n this.inferFromSiblingMime() ?? this.inferFromSiblingFormat() ?? this.inferFromParentType() ?? this.inferFromKey()\n )\n }\n\n inferFromSiblingMime(): string | undefined {\n if (this.explicitMediaType) return this.explicitMediaType\n if (!this.parent) return undefined\n for (const hint of MIME_HINT_KEYS) {\n const v = this.parent[hint]\n if (typeof v === 'string') return v\n }\n return undefined\n }\n\n inferFromSiblingFormat(): string | undefined {\n if (!this.parent) return undefined\n const fmt = this.parent.format\n if (typeof fmt === 'string' && KNOWN_AUDIO_FORMATS.has(fmt.toLowerCase())) {\n return `audio/${fmt.toLowerCase()}`\n }\n return undefined\n }\n\n inferFromParentType(): string | undefined {\n if (!this.parent) return undefined\n const t = this.parent.type\n if (typeof t !== 'string') return undefined\n if (t === 'image' || t === 'image_url' || t === 'input_image') return 'image'\n if (t === 'audio' || t === 'input_audio') return 'audio'\n if (t === 'video' || t === 'video_url') return 'video'\n if (FILE_FAMILY_TYPES.has(t)) return 'application/octet-stream'\n return undefined\n }\n\n inferFromKey(): string | undefined {\n if (!this.key) return undefined\n const key = this.key.toLowerCase()\n if (key.includes('audio')) return 'audio'\n if (key.includes('video')) return 'video'\n if (key.includes('image')) return 'image'\n if (key.includes('file') || key.includes('document')) return 'application/octet-stream'\n return undefined\n }\n\n hasExplicitBinaryMediaType(): boolean {\n if (!this.explicitMediaType && (!this.parent || !this.key || !STRONG_CONTEXT_KEYS.has(this.key))) return false\n const mediaType = this.inferFromSiblingMime()\n return mediaType !== undefined && !mediaType.toLowerCase().startsWith('text/')\n }\n\n signalsBinary(): boolean {\n if (this.explicitMediaType) return true\n if (this.parent) {\n for (const hint of MIME_HINT_KEYS) {\n if (typeof this.parent[hint] === 'string') return true\n }\n const fmt = this.parent.format\n if (typeof fmt === 'string' && KNOWN_AUDIO_FORMATS.has(fmt.toLowerCase())) return true\n const t = this.parent.type\n if (typeof t === 'string' && STRONG_CONTEXT_TYPES.has(t)) return true\n }\n if (this.key && STRONG_CONTEXT_KEYS.has(this.key)) return true\n return false\n }\n}\n","import { Base64Recognizer } from './base64_recognizer'\nimport { MediaTypeContext } from './media_type_context'\n\nconst STRONG_CONTEXT_MIN_LENGTH = 64\nconst WEAK_CONTEXT_MIN_LENGTH = 1024\n\nexport class BinaryContentRedactor {\n private visited: WeakSet<object> = new WeakSet()\n\n constructor(private readonly recognizer: Base64Recognizer = new Base64Recognizer()) {}\n\n redact<T>(value: T, mediaType?: string): T\n redact(value: unknown, mediaType?: string): unknown {\n this.visited = new WeakSet()\n return this.walk(value, mediaType ? new MediaTypeContext(undefined, undefined, mediaType) : MediaTypeContext.EMPTY)\n }\n\n private walk(value: unknown, ctx: MediaTypeContext): unknown {\n if (value === null || value === undefined) return value\n if (typeof value === 'string') return this.redactString(value, ctx)\n if (typeof value !== 'object') return value\n\n // Buffer extends Uint8Array, so this branch catches both.\n if (typeof Uint8Array !== 'undefined' && value instanceof Uint8Array) {\n return this.placeholderFor(ctx.inferMediaType())\n }\n\n if (this.visited.has(value)) return null\n this.visited.add(value)\n\n if (Array.isArray(value)) {\n return value.map((item) => this.walk(item, ctx))\n }\n\n const obj = value as Record<string, unknown>\n const out: Record<string, unknown> = {}\n for (const k of Object.keys(obj)) {\n out[k] = this.walk(obj[k], new MediaTypeContext(obj, k))\n }\n return out\n }\n\n private redactString(value: string, ctx: MediaTypeContext): string {\n const hasExplicitBinaryMediaType = ctx.hasExplicitBinaryMediaType()\n const recognitionValue = hasExplicitBinaryMediaType ? value.replace(/[\\r\\n]/g, '') : value\n const minLength = hasExplicitBinaryMediaType\n ? Math.min(recognitionValue.length, STRONG_CONTEXT_MIN_LENGTH)\n : ctx.signalsBinary()\n ? STRONG_CONTEXT_MIN_LENGTH\n : WEAK_CONTEXT_MIN_LENGTH\n const recognition = this.recognizer.recognize(recognitionValue, minLength)\n switch (recognition.kind) {\n case 'data-url':\n return this.placeholderFor(recognition.mediaType)\n case 'raw':\n return this.placeholderFor(ctx.inferMediaType())\n case 'none':\n return value\n }\n }\n\n private placeholderFor(mediaType?: string): string {\n if (!mediaType) return '[base64 redacted]'\n if (mediaType === 'application/octet-stream') return '[base64 file redacted]'\n return `[base64 ${mediaType} redacted]`\n }\n}\n","import type { Attributes, AttributeValue } from '@opentelemetry/api'\nimport type { ReadableSpan, TimedEvent } from '@opentelemetry/sdk-trace-base'\n\nimport { BinaryContentRedactor } from '../sanitization/binary_content_redactor'\n\n// Deliberately always redacts here — the OTLP export path has no per-client passthrough gate.\nconst redactor = new BinaryContentRedactor()\n\nexport function redactSpan(span: ReadableSpan): ReadableSpan {\n const attributes = span.attributes ? redactAttributes(span.attributes) : span.attributes\n const events = span.events ? redactEvents(span.events) : span.events\n if (attributes === span.attributes && events === span.events) {\n return span\n }\n // Copy rather than mutate: the span is shared across every registered processor/exporter.\n return Object.create(span, {\n attributes: { value: attributes, enumerable: true, configurable: true },\n events: { value: events, enumerable: true, configurable: true },\n })\n}\n\nfunction redactAttributes(attributes: Attributes): Attributes {\n let changed = false\n const out: Attributes = {}\n for (const key of Object.keys(attributes)) {\n const value = attributes[key]\n const redacted = value === undefined ? value : redactAttributeValue(value)\n if (redacted !== value) {\n changed = true\n }\n out[key] = redacted\n }\n return changed ? out : attributes\n}\n\nfunction redactEvents(events: TimedEvent[]): TimedEvent[] {\n let changed = false\n const out = events.map((event) => {\n if (!event.attributes) {\n return event\n }\n const attributes = redactAttributes(event.attributes)\n if (attributes === event.attributes) {\n return event\n }\n changed = true\n return { ...event, attributes }\n })\n return changed ? out : events\n}\n\nfunction redactAttributeValue(value: AttributeValue): AttributeValue {\n if (typeof value === 'string') {\n return redactString(value)\n }\n if (isStringArray(value)) {\n let changed = false\n const out = value.map((item) => {\n if (typeof item !== 'string') {\n return item\n }\n const redacted = redactString(item)\n if (redacted !== item) {\n changed = true\n }\n return redacted\n })\n return changed ? out : value\n }\n return value\n}\n\nfunction isStringArray(value: AttributeValue): value is Array<string | null | undefined> {\n return Array.isArray(value) && value.every((item) => typeof item !== 'number' && typeof item !== 'boolean')\n}\n\nfunction redactString(value: string): string {\n const trimmed = value.trimStart()\n if (trimmed.startsWith('{') || trimmed.startsWith('[')) {\n const redacted = redactJson(value)\n if (redacted !== undefined) {\n return redacted\n }\n }\n return redactor.redact(value)\n}\n\nfunction redactJson(value: string): string | undefined {\n let parsed: unknown\n try {\n parsed = JSON.parse(value)\n } catch {\n return undefined\n }\n if (parsed === null || typeof parsed !== 'object') {\n return undefined\n }\n const redactedStr = JSON.stringify(redactor.redact(parsed))\n // Preserve the original string (and its formatting) when nothing was redacted.\n return redactedStr === JSON.stringify(parsed) ? value : redactedStr\n}\n","import type { ReadableSpan } from '@opentelemetry/sdk-trace-base'\n\nconst AI_SPAN_PREFIXES = ['gen_ai.', 'llm.', 'ai.', 'traceloop.'] as const\n\n/**\n * Returns `true` when the span is AI-related — its name or any attribute\n * key starts with `gen_ai.`, `llm.`, `ai.`, or `traceloop.`.\n */\nexport function isAISpan(span: ReadableSpan): boolean {\n if (AI_SPAN_PREFIXES.some((prefix) => span.name.startsWith(prefix))) {\n return true\n }\n const attributes = span.attributes\n if (attributes) {\n return Object.keys(attributes).some((key) => AI_SPAN_PREFIXES.some((prefix) => key.startsWith(prefix)))\n }\n return false\n}\n","// Warn when a wrapper's base_url points at the PostHog AI Gateway: the gateway\n// emits its own $ai_generation, so each call would be captured (and, for billable\n// products, billed) twice. We only warn — the wrapper's event carries data the\n// gateway never sees (groups, custom properties, trace hierarchy).\n\n// Keep in sync with the gateway's deployed hosts (see services/llm-gateway in the\n// main repo). gateway.us.posthog.com is live today; the rest are listed ahead of\n// any traffic moving to them.\nexport const POSTHOG_AI_GATEWAY_HOSTS: readonly string[] = [\n 'gateway.posthog.com',\n 'gateway.us.posthog.com',\n 'gateway.eu.posthog.com',\n 'ai-gateway.us.posthog.com',\n 'ai-gateway.eu.posthog.com',\n]\n\n// Swap for the dedicated AI Gateway page once it ships.\nconst GATEWAY_DOCS_URL = 'https://posthog.com/docs/ai-observability'\n\nconst extractHost = (baseURL: string): string | undefined => {\n try {\n // Tolerate bare hosts that omit a scheme, e.g. \"gateway.us.posthog.com/v1\".\n const hasScheme = /^[a-z][a-z0-9+.-]*:\\/\\//i.test(baseURL)\n return new URL(hasScheme ? baseURL : `https://${baseURL}`).hostname.toLowerCase()\n } catch {\n return undefined\n }\n}\n\nexport const isPostHogAiGatewayUrl = (baseURL: string | undefined | null): boolean => {\n if (!baseURL) {\n return false\n }\n const host = extractHost(baseURL)\n return host !== undefined && POSTHOG_AI_GATEWAY_HOSTS.includes(host)\n}\n\n// Warns on every gateway call by design: the misconfiguration is impossible to\n// miss that way, and a doubled bill is worse than noisy logs.\nexport const warnIfPostHogAiGateway = (baseURL: string | undefined | null): void => {\n if (!isPostHogAiGatewayUrl(baseURL)) {\n return\n }\n console.warn(\n '[PostHog] The PostHog AI wrapper is pointed at the PostHog AI Gateway. ' +\n 'Both capture $ai_generation, so every call is double-counted and double-billed. ' +\n `Use one or the other — see ${GATEWAY_DOCS_URL}.`\n )\n}\n\n// OTel spans don't pass through captureAiGeneration, so detect the gateway from\n// the span's host/URL attributes instead. These follow the GenAI / HTTP semantic\n// conventions: `server.address` is a bare host, `url.full` a full URL, both of\n// which isPostHogAiGatewayUrl accepts.\nconst OTEL_GATEWAY_URL_ATTRIBUTES = ['server.address', 'url.full'] as const\n\nexport const warnIfPostHogAiGatewayOtelAttributes = (attributes: Record<string, unknown> | undefined): void => {\n if (!attributes) {\n return\n }\n for (const key of OTEL_GATEWAY_URL_ATTRIBUTES) {\n const value = attributes[key]\n if (typeof value === 'string' && isPostHogAiGatewayUrl(value)) {\n warnIfPostHogAiGateway(value)\n return\n }\n }\n}\n","import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http'\nimport type { ReadableSpan } from '@opentelemetry/sdk-trace-base'\n\nimport { redactSpan } from './redact'\nimport { isAISpan } from './spans'\nimport { warnIfPostHogAiGatewayOtelAttributes } from '../gatewayWarning'\n\nconst DEFAULT_OTEL_HOST = 'https://us.i.posthog.com'\n// OpenTelemetry's ExportResultCode.SUCCESS is 0. Keep this local so loading the\n// published subpath does not require the exporter package's transitive dependencies.\nconst EXPORT_SUCCESS = 0\n\nfunction normalizeToken(value?: unknown): string {\n return typeof value === 'string' ? value.trim() : ''\n}\n\nfunction normalizeHost(value?: unknown): string {\n const normalizedValue = typeof value === 'string' ? value.trim() : ''\n return normalizedValue || DEFAULT_OTEL_HOST\n}\n\n/**\n * Options for the PostHogTraceExporter. `projectToken` is required; a blank token disables the\n * exporter as a defensive no-op. You can also optionally override the `host` URL. `host` defaults to `https://us.i.posthog.com`.\n *\n * @example\n * ```ts\n * import { PostHogTraceExporter } from '@posthog/ai/otel'\n *\n * new PostHogTraceExporter({ projectToken: 'phc_...' })\n * ```\n *\n * @example\n * ```ts\n * import { PostHogTraceExporter } from '@posthog/ai/otel'\n *\n * new PostHogTraceExporter({ projectToken: 'phc_...', host: 'https://eu.i.posthog.com' })\n * ```\n */\nexport type PostHogTraceExporterOptions = { projectToken: string; host?: string }\n\n/**\n * An OpenTelemetry `TraceExporter` that sends AI traces to PostHog's OTLP\n * ingestion endpoint. PostHog converts `gen_ai.*` spans into\n * `$ai_generation` events server-side.\n *\n * Only AI-related spans (those whose name or attribute keys start with\n * `gen_ai.`, `llm.`, `ai.`, or `traceloop.`) are exported; all other\n * spans are silently dropped.\n *\n * Use this when the API you're integrating with only accepts a\n * `TraceExporter` (e.g. Vercel's `registerOTel`) or when you need to\n * plug PostHog into an existing processor chain. Otherwise prefer\n * {@link PostHogSpanProcessor}, which is self-contained.\n *\n * `projectToken` is required; a blank token disables the exporter as a defensive no-op.\n * You can also optionally override the `host` URL.\n *\n * @example\n * ```ts\n * import { PostHogTraceExporter } from '@posthog/ai/otel'\n * import { registerOTel } from '@vercel/otel'\n *\n * registerOTel({\n * serviceName: 'my-app',\n * traceExporter: new PostHogTraceExporter({ projectToken: 'phc_...' }),\n * })\n * ```\n */\nexport class PostHogTraceExporter extends OTLPTraceExporter {\n private readonly disabled: boolean\n\n constructor(options: PostHogTraceExporterOptions) {\n const token = normalizeToken(options.projectToken)\n const disabled = !token\n const host = token ? new URL(normalizeHost(options.host)).origin : DEFAULT_OTEL_HOST\n super({\n url: `${host}/i/v0/ai/otel`,\n headers: token\n ? {\n Authorization: `Bearer ${token}`,\n }\n : {},\n })\n\n this.disabled = disabled\n if (this.disabled) {\n console.warn('[PostHogTraceExporter] projectToken is missing or blank; the exporter will be disabled.')\n }\n }\n\n override export(spans: ReadableSpan[], resultCallback: (result: { code: number; error?: Error }) => void): void {\n if (this.disabled) {\n // Intentionally report success: missing or blank tokens disable exporting as a compatibility no-op.\n // Reporting failure would make OpenTelemetry treat every span as an export error.\n resultCallback({ code: EXPORT_SUCCESS })\n return\n }\n\n const aiSpans = spans.filter(isAISpan)\n if (aiSpans.length === 0) {\n resultCallback({ code: EXPORT_SUCCESS })\n return\n }\n for (const span of aiSpans) {\n warnIfPostHogAiGatewayOtelAttributes(span.attributes)\n }\n super.export(aiSpans.map(redactSpan), resultCallback)\n }\n}\n","import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http'\nimport type { Context } from '@opentelemetry/api'\nimport { BatchSpanProcessor, type SpanProcessor, type ReadableSpan, type Span } from '@opentelemetry/sdk-trace-base'\n\nimport { redactSpan } from './redact'\nimport { isAISpan } from './spans'\nimport { warnIfPostHogAiGatewayOtelAttributes } from '../gatewayWarning'\n\nconst DEFAULT_OTEL_HOST = 'https://us.i.posthog.com'\n\nfunction normalizeToken(value?: unknown): string {\n return typeof value === 'string' ? value.trim() : ''\n}\n\nfunction normalizeHost(value?: unknown): string {\n const normalizedValue = typeof value === 'string' ? value.trim() : ''\n return normalizedValue || DEFAULT_OTEL_HOST\n}\n\nexport interface PostHogSpanProcessorOptions {\n /**\n * Your PostHog project token (the `phc_...` key). Required; a blank token disables the\n * processor as a defensive no-op.\n */\n projectToken: string\n\n /**\n * PostHog host URL. Defaults to `https://us.i.posthog.com`.\n */\n host?: string\n\n /**\n * @internal Injected processor for testing — bypasses exporter creation.\n */\n _spanProcessor?: SpanProcessor\n}\n\nclass NoopSpanProcessor implements SpanProcessor {\n onStart(_span: Span, _parentContext: Context): void {\n return\n }\n onEnd(_span: ReadableSpan): void {\n return\n }\n shutdown(): Promise<void> {\n return Promise.resolve()\n }\n forceFlush(): Promise<void> {\n return Promise.resolve()\n }\n}\n\n/**\n * An OpenTelemetry `SpanProcessor` that sends AI traces to PostHog.\n *\n * `projectToken` is required; a blank token disables the processor as a defensive no-op.\n *\n * Internally batches spans and exports them to PostHog's OTLP ingestion\n * endpoint. Only AI-related spans (those whose name or attribute keys\n * start with `gen_ai.`, `llm.`, `ai.`, or `traceloop.`) are exported;\n * all other spans are silently dropped.\n *\n * Request-scoped and serverless runtimes should keep a reference to this\n * processor and await {@link PostHogSpanProcessor.forceFlush} before the request\n * lifecycle ends. This waits for queued exports without sending one request per span.\n *\n * This is the recommended integration point when your setup accepts a\n * `SpanProcessor`. If you need a `TraceExporter` instead (e.g. for\n * Vercel's `registerOTel`), use {@link PostHogTraceExporter}.\n *\n * @example\n * ```ts\n * import { PostHogSpanProcessor } from '@posthog/ai/otel'\n * import { NodeSDK } from '@opentelemetry/sdk-node'\n *\n * const processor = new PostHogSpanProcessor({ projectToken: 'phc_...' })\n * const sdk = new NodeSDK({ spanProcessors: [processor] })\n * sdk.start()\n *\n * // In request-scoped runtimes, wait for queued exports before returning.\n * await processor.forceFlush()\n * ```\n */\nexport class PostHogSpanProcessor implements SpanProcessor {\n private readonly inner: SpanProcessor\n\n constructor(options: PostHogSpanProcessorOptions) {\n const token = normalizeToken(options.projectToken)\n if (!token) {\n console.warn('[PostHogSpanProcessor] projectToken is missing or blank; the processor will be disabled.')\n this.inner = new NoopSpanProcessor()\n return\n }\n\n if (options._spanProcessor) {\n this.inner = options._spanProcessor\n } else {\n const host = new URL(normalizeHost(options.host)).origin\n const exporter = new OTLPTraceExporter({\n url: `${host}/i/v0/ai/otel`,\n headers: {\n Authorization: `Bearer ${token}`,\n },\n })\n this.inner = new BatchSpanProcessor(exporter)\n }\n }\n\n onStart(span: Span, parentContext: Context): void {\n // Forwarded unconditionally — filtering happens in onEnd. We can't filter\n // here because the span hasn't finished yet and may not have AI attributes\n // set. BatchSpanProcessor.onStart is a no-op so this is safe.\n this.inner.onStart(span, parentContext)\n }\n\n onEnd(span: ReadableSpan): void {\n if (isAISpan(span)) {\n warnIfPostHogAiGatewayOtelAttributes(span.attributes)\n this.inner.onEnd(redactSpan(span))\n }\n }\n\n shutdown(): Promise<void> {\n return this.inner.shutdown()\n }\n\n forceFlush(): Promise<void> {\n return this.inner.forceFlush()\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.cjs","names":["DEFAULT_OTEL_HOST","normalizeToken","normalizeHost","OTLPTraceExporter","OTLPTraceExporter","BatchSpanProcessor"],"sources":["../../src/sanitization/base64_recognizer.ts","../../src/sanitization/media_type_context.ts","../../src/sanitization/binary_content_redactor.ts","../../src/otel/spans.ts","../../src/otel/redact.ts","../../src/gatewayWarning.ts","../../src/otel/exporter.ts","../../src/otel/requestContext.ts","../../src/otel/processor.ts"],"sourcesContent":["const DATA_URL_PREFIX_RE = /^data:([^;,\\s]+)(?:;[^;,\\s]+)*;base64,/i\nconst BASE64_ALPHABET_RE = /^[A-Za-z0-9+/_=-]+$/\n\nexport type Base64Recognition = { kind: 'data-url'; mediaType: string } | { kind: 'raw' } | { kind: 'none' }\n\nexport class Base64Recognizer {\n recognize(value: string, minLength: number): Base64Recognition {\n const dataUrl = DATA_URL_PREFIX_RE.exec(value)\n if (dataUrl) return { kind: 'data-url', mediaType: dataUrl[1] }\n\n if (value.length < minLength) return { kind: 'none' }\n\n const confidencePrefix = value.slice(0, minLength)\n if (BASE64_ALPHABET_RE.test(confidencePrefix)) {\n return { kind: 'raw' }\n } else {\n return { kind: 'none' }\n }\n }\n}\n","const MIME_HINT_KEYS = ['mediaType', 'media_type', 'mimeType', 'mime_type'] as const\n\nconst STRONG_CONTEXT_KEYS = new Set([\n 'data',\n 'file_data',\n 'fileData',\n 'image_url',\n 'imageUrl',\n 'video_url',\n 'videoUrl',\n 'audio',\n 'audio_data',\n 'audioData',\n 'inline_data',\n 'inlineData',\n 'source',\n 'result',\n])\n\nconst STRONG_CONTEXT_TYPES = new Set([\n 'image',\n 'image_url',\n 'input_image',\n 'audio',\n 'input_audio',\n 'video',\n 'video_url',\n 'file',\n 'input_file',\n 'document',\n 'media',\n 'file-data',\n])\n\nconst FILE_FAMILY_TYPES = new Set(['file', 'input_file', 'document', 'media', 'file-data'])\n\nconst KNOWN_AUDIO_FORMATS = new Set(['wav', 'mp3', 'ogg', 'flac', 'm4a', 'aac', 'webm'])\n\nexport class MediaTypeContext {\n static readonly EMPTY = new MediaTypeContext(undefined, undefined)\n\n constructor(\n private readonly parent: Record<string, unknown> | undefined,\n private readonly key: string | undefined,\n private readonly explicitMediaType?: string\n ) {}\n\n inferMediaType(): string | undefined {\n return (\n this.inferFromSiblingMime() ?? this.inferFromSiblingFormat() ?? this.inferFromParentType() ?? this.inferFromKey()\n )\n }\n\n inferFromSiblingMime(): string | undefined {\n if (this.explicitMediaType) return this.explicitMediaType\n if (!this.parent) return undefined\n for (const hint of MIME_HINT_KEYS) {\n const v = this.parent[hint]\n if (typeof v === 'string') return v\n }\n return undefined\n }\n\n inferFromSiblingFormat(): string | undefined {\n if (!this.parent) return undefined\n const fmt = this.parent.format\n if (typeof fmt === 'string' && KNOWN_AUDIO_FORMATS.has(fmt.toLowerCase())) {\n return `audio/${fmt.toLowerCase()}`\n }\n return undefined\n }\n\n inferFromParentType(): string | undefined {\n if (!this.parent) return undefined\n const t = this.parent.type\n if (typeof t !== 'string') return undefined\n if (t === 'image' || t === 'image_url' || t === 'input_image') return 'image'\n if (t === 'audio' || t === 'input_audio') return 'audio'\n if (t === 'video' || t === 'video_url') return 'video'\n if (FILE_FAMILY_TYPES.has(t)) return 'application/octet-stream'\n return undefined\n }\n\n inferFromKey(): string | undefined {\n if (!this.key) return undefined\n const key = this.key.toLowerCase()\n if (key.includes('audio')) return 'audio'\n if (key.includes('video')) return 'video'\n if (key.includes('image')) return 'image'\n if (key.includes('file') || key.includes('document')) return 'application/octet-stream'\n return undefined\n }\n\n hasExplicitBinaryMediaType(): boolean {\n if (!this.explicitMediaType && (!this.parent || !this.key || !STRONG_CONTEXT_KEYS.has(this.key))) return false\n const mediaType = this.inferFromSiblingMime()\n return mediaType !== undefined && !mediaType.toLowerCase().startsWith('text/')\n }\n\n signalsBinary(): boolean {\n if (this.explicitMediaType) return true\n if (this.parent) {\n for (const hint of MIME_HINT_KEYS) {\n if (typeof this.parent[hint] === 'string') return true\n }\n const fmt = this.parent.format\n if (typeof fmt === 'string' && KNOWN_AUDIO_FORMATS.has(fmt.toLowerCase())) return true\n const t = this.parent.type\n if (typeof t === 'string' && STRONG_CONTEXT_TYPES.has(t)) return true\n }\n if (this.key && STRONG_CONTEXT_KEYS.has(this.key)) return true\n return false\n }\n}\n","import { Base64Recognizer } from './base64_recognizer'\nimport { MediaTypeContext } from './media_type_context'\n\nconst STRONG_CONTEXT_MIN_LENGTH = 64\nconst WEAK_CONTEXT_MIN_LENGTH = 1024\n\nexport class BinaryContentRedactor {\n private visited: WeakSet<object> = new WeakSet()\n\n constructor(private readonly recognizer: Base64Recognizer = new Base64Recognizer()) {}\n\n redact<T>(value: T, mediaType?: string): T\n redact(value: unknown, mediaType?: string): unknown {\n this.visited = new WeakSet()\n return this.walk(value, mediaType ? new MediaTypeContext(undefined, undefined, mediaType) : MediaTypeContext.EMPTY)\n }\n\n private walk(value: unknown, ctx: MediaTypeContext): unknown {\n if (value === null || value === undefined) return value\n if (typeof value === 'string') return this.redactString(value, ctx)\n if (typeof value !== 'object') return value\n\n // Buffer extends Uint8Array, so this branch catches both.\n if (typeof Uint8Array !== 'undefined' && value instanceof Uint8Array) {\n return this.placeholderFor(ctx.inferMediaType())\n }\n\n if (this.visited.has(value)) return null\n this.visited.add(value)\n\n if (Array.isArray(value)) {\n return value.map((item) => this.walk(item, ctx))\n }\n\n const obj = value as Record<string, unknown>\n const out: Record<string, unknown> = {}\n for (const k of Object.keys(obj)) {\n out[k] = this.walk(obj[k], new MediaTypeContext(obj, k))\n }\n return out\n }\n\n private redactString(value: string, ctx: MediaTypeContext): string {\n const hasExplicitBinaryMediaType = ctx.hasExplicitBinaryMediaType()\n const recognitionValue = hasExplicitBinaryMediaType ? value.replace(/[\\r\\n]/g, '') : value\n const minLength = hasExplicitBinaryMediaType\n ? Math.min(recognitionValue.length, STRONG_CONTEXT_MIN_LENGTH)\n : ctx.signalsBinary()\n ? STRONG_CONTEXT_MIN_LENGTH\n : WEAK_CONTEXT_MIN_LENGTH\n const recognition = this.recognizer.recognize(recognitionValue, minLength)\n switch (recognition.kind) {\n case 'data-url':\n return this.placeholderFor(recognition.mediaType)\n case 'raw':\n return this.placeholderFor(ctx.inferMediaType())\n case 'none':\n return value\n }\n }\n\n private placeholderFor(mediaType?: string): string {\n if (!mediaType) return '[base64 redacted]'\n if (mediaType === 'application/octet-stream') return '[base64 file redacted]'\n return `[base64 ${mediaType} redacted]`\n }\n}\n","import type { Attributes } from '@opentelemetry/api'\nimport type { ReadableSpan, TimedEvent } from '@opentelemetry/sdk-trace-base'\n\nconst AI_SPAN_PREFIXES = ['gen_ai.', 'llm.', 'ai.', 'traceloop.'] as const\n\n/**\n * Returns `true` when the span is AI-related — its name or any attribute\n * key starts with `gen_ai.`, `llm.`, `ai.`, or `traceloop.`.\n */\nexport function isAISpan(span: ReadableSpan): boolean {\n if (AI_SPAN_PREFIXES.some((prefix) => span.name.startsWith(prefix))) {\n return true\n }\n const attributes = span.attributes\n if (attributes) {\n return Object.keys(attributes).some((key) => AI_SPAN_PREFIXES.some((prefix) => key.startsWith(prefix)))\n }\n return false\n}\n\n/** Copies rather than mutates: the span is shared across every registered processor/exporter. */\nexport function withAttributes(span: ReadableSpan, attributes: Attributes, events?: TimedEvent[]): ReadableSpan {\n return Object.create(span, {\n attributes: { value: attributes, enumerable: true, configurable: true },\n ...(events ? { events: { value: events, enumerable: true, configurable: true } } : {}),\n })\n}\n","import type { Attributes, AttributeValue } from '@opentelemetry/api'\nimport type { ReadableSpan, TimedEvent } from '@opentelemetry/sdk-trace-base'\n\nimport { BinaryContentRedactor } from '../sanitization/binary_content_redactor'\nimport { withAttributes } from './spans'\n\n// Deliberately always redacts here — the OTLP export path has no per-client passthrough gate.\nconst redactor = new BinaryContentRedactor()\n\nexport function redactSpan(span: ReadableSpan): ReadableSpan {\n const attributes = span.attributes ? redactAttributes(span.attributes) : span.attributes\n const events = span.events ? redactEvents(span.events) : span.events\n if (attributes === span.attributes && events === span.events) {\n return span\n }\n return withAttributes(span, attributes, events)\n}\n\nfunction redactAttributes(attributes: Attributes): Attributes {\n let changed = false\n const out: Attributes = {}\n for (const key of Object.keys(attributes)) {\n const value = attributes[key]\n const redacted = value === undefined ? value : redactAttributeValue(value)\n if (redacted !== value) {\n changed = true\n }\n out[key] = redacted\n }\n return changed ? out : attributes\n}\n\nfunction redactEvents(events: TimedEvent[]): TimedEvent[] {\n let changed = false\n const out = events.map((event) => {\n if (!event.attributes) {\n return event\n }\n const attributes = redactAttributes(event.attributes)\n if (attributes === event.attributes) {\n return event\n }\n changed = true\n return { ...event, attributes }\n })\n return changed ? out : events\n}\n\nfunction redactAttributeValue(value: AttributeValue): AttributeValue {\n if (typeof value === 'string') {\n return redactString(value)\n }\n if (isStringArray(value)) {\n let changed = false\n const out = value.map((item) => {\n if (typeof item !== 'string') {\n return item\n }\n const redacted = redactString(item)\n if (redacted !== item) {\n changed = true\n }\n return redacted\n })\n return changed ? out : value\n }\n return value\n}\n\nfunction isStringArray(value: AttributeValue): value is Array<string | null | undefined> {\n return Array.isArray(value) && value.every((item) => typeof item !== 'number' && typeof item !== 'boolean')\n}\n\nfunction redactString(value: string): string {\n const trimmed = value.trimStart()\n if (trimmed.startsWith('{') || trimmed.startsWith('[')) {\n const redacted = redactJson(value)\n if (redacted !== undefined) {\n return redacted\n }\n }\n return redactor.redact(value)\n}\n\nfunction redactJson(value: string): string | undefined {\n let parsed: unknown\n try {\n parsed = JSON.parse(value)\n } catch {\n return undefined\n }\n if (parsed === null || typeof parsed !== 'object') {\n return undefined\n }\n const redactedStr = JSON.stringify(redactor.redact(parsed))\n // Preserve the original string (and its formatting) when nothing was redacted.\n return redactedStr === JSON.stringify(parsed) ? value : redactedStr\n}\n","// Warn when a wrapper's base_url points at the PostHog AI Gateway: the gateway\n// emits its own $ai_generation, so each call would be captured (and, for billable\n// products, billed) twice. We only warn — the wrapper's event carries data the\n// gateway never sees (groups, custom properties, trace hierarchy).\n\n// Keep in sync with the gateway's deployed hosts (see services/llm-gateway in the\n// main repo). gateway.us.posthog.com is live today; the rest are listed ahead of\n// any traffic moving to them.\nexport const POSTHOG_AI_GATEWAY_HOSTS: readonly string[] = [\n 'gateway.posthog.com',\n 'gateway.us.posthog.com',\n 'gateway.eu.posthog.com',\n 'ai-gateway.us.posthog.com',\n 'ai-gateway.eu.posthog.com',\n]\n\n// Swap for the dedicated AI Gateway page once it ships.\nconst GATEWAY_DOCS_URL = 'https://posthog.com/docs/ai-observability'\n\nconst extractHost = (baseURL: string): string | undefined => {\n try {\n // Tolerate bare hosts that omit a scheme, e.g. \"gateway.us.posthog.com/v1\".\n const hasScheme = /^[a-z][a-z0-9+.-]*:\\/\\//i.test(baseURL)\n return new URL(hasScheme ? baseURL : `https://${baseURL}`).hostname.toLowerCase()\n } catch {\n return undefined\n }\n}\n\nexport const isPostHogAiGatewayUrl = (baseURL: string | undefined | null): boolean => {\n if (!baseURL) {\n return false\n }\n const host = extractHost(baseURL)\n return host !== undefined && POSTHOG_AI_GATEWAY_HOSTS.includes(host)\n}\n\n// Warns on every gateway call by design: the misconfiguration is impossible to\n// miss that way, and a doubled bill is worse than noisy logs.\nexport const warnIfPostHogAiGateway = (baseURL: string | undefined | null): void => {\n if (!isPostHogAiGatewayUrl(baseURL)) {\n return\n }\n console.warn(\n '[PostHog] The PostHog AI wrapper is pointed at the PostHog AI Gateway. ' +\n 'Both capture $ai_generation, so every call is double-counted and double-billed. ' +\n `Use one or the other — see ${GATEWAY_DOCS_URL}.`\n )\n}\n\n// OTel spans don't pass through captureAiGeneration, so detect the gateway from\n// the span's host/URL attributes instead. These follow the GenAI / HTTP semantic\n// conventions: `server.address` is a bare host, `url.full` a full URL, both of\n// which isPostHogAiGatewayUrl accepts.\nconst OTEL_GATEWAY_URL_ATTRIBUTES = ['server.address', 'url.full'] as const\n\nexport const warnIfPostHogAiGatewayOtelAttributes = (attributes: Record<string, unknown> | undefined): void => {\n if (!attributes) {\n return\n }\n for (const key of OTEL_GATEWAY_URL_ATTRIBUTES) {\n const value = attributes[key]\n if (typeof value === 'string' && isPostHogAiGatewayUrl(value)) {\n warnIfPostHogAiGateway(value)\n return\n }\n }\n}\n","import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http'\nimport type { ReadableSpan } from '@opentelemetry/sdk-trace-base'\n\nimport { redactSpan } from './redact'\nimport { isAISpan } from './spans'\nimport { warnIfPostHogAiGatewayOtelAttributes } from '../gatewayWarning'\n\nconst DEFAULT_OTEL_HOST = 'https://us.i.posthog.com'\n// OpenTelemetry's ExportResultCode.SUCCESS is 0. Keep this local so loading the\n// published subpath does not require the exporter package's transitive dependencies.\nconst EXPORT_SUCCESS = 0\n\nfunction normalizeToken(value?: unknown): string {\n return typeof value === 'string' ? value.trim() : ''\n}\n\nfunction normalizeHost(value?: unknown): string {\n const normalizedValue = typeof value === 'string' ? value.trim() : ''\n return normalizedValue || DEFAULT_OTEL_HOST\n}\n\n/**\n * Options for the PostHogTraceExporter. `projectToken` is required; a blank token disables the\n * exporter as a defensive no-op. You can also optionally override the `host` URL. `host` defaults to `https://us.i.posthog.com`.\n *\n * @example\n * ```ts\n * import { PostHogTraceExporter } from '@posthog/ai/otel'\n *\n * new PostHogTraceExporter({ projectToken: 'phc_...' })\n * ```\n *\n * @example\n * ```ts\n * import { PostHogTraceExporter } from '@posthog/ai/otel'\n *\n * new PostHogTraceExporter({ projectToken: 'phc_...', host: 'https://eu.i.posthog.com' })\n * ```\n */\nexport type PostHogTraceExporterOptions = { projectToken: string; host?: string }\n\n/**\n * An OpenTelemetry `TraceExporter` that sends AI traces to PostHog's OTLP\n * ingestion endpoint. PostHog converts `gen_ai.*` spans into\n * `$ai_generation` events server-side.\n *\n * Only AI-related spans (those whose name or attribute keys start with\n * `gen_ai.`, `llm.`, `ai.`, or `traceloop.`) are exported; all other\n * spans are silently dropped.\n *\n * Use this when the API you're integrating with only accepts a\n * `TraceExporter` (e.g. Vercel's `registerOTel`) or when you need to\n * plug PostHog into an existing processor chain. Otherwise prefer\n * {@link PostHogSpanProcessor}, which is self-contained.\n *\n * `projectToken` is required; a blank token disables the exporter as a defensive no-op.\n * You can also optionally override the `host` URL.\n *\n * Exporters only see finished spans, so they can't add per-request PostHog context;\n * use {@link PostHogSpanProcessor} with `client` for that.\n *\n * @example\n * ```ts\n * import { PostHogTraceExporter } from '@posthog/ai/otel'\n * import { registerOTel } from '@vercel/otel'\n *\n * registerOTel({\n * serviceName: 'my-app',\n * traceExporter: new PostHogTraceExporter({ projectToken: 'phc_...' }),\n * })\n * ```\n */\nexport class PostHogTraceExporter extends OTLPTraceExporter {\n private readonly disabled: boolean\n\n constructor(options: PostHogTraceExporterOptions) {\n const token = normalizeToken(options.projectToken)\n const disabled = !token\n const host = token ? new URL(normalizeHost(options.host)).origin : DEFAULT_OTEL_HOST\n super({\n url: `${host}/i/v0/ai/otel`,\n headers: token\n ? {\n Authorization: `Bearer ${token}`,\n }\n : {},\n })\n\n this.disabled = disabled\n if (this.disabled) {\n console.warn('[PostHogTraceExporter] projectToken is missing or blank; the exporter will be disabled.')\n }\n }\n\n override export(spans: ReadableSpan[], resultCallback: (result: { code: number; error?: Error }) => void): void {\n if (this.disabled) {\n // Intentionally report success: missing or blank tokens disable exporting as a compatibility no-op.\n // Reporting failure would make OpenTelemetry treat every span as an export error.\n resultCallback({ code: EXPORT_SUCCESS })\n return\n }\n\n const aiSpans = spans.filter(isAISpan)\n if (aiSpans.length === 0) {\n resultCallback({ code: EXPORT_SUCCESS })\n return\n }\n for (const span of aiSpans) {\n warnIfPostHogAiGatewayOtelAttributes(span.attributes)\n }\n super.export(aiSpans.map(redactSpan), resultCallback)\n }\n}\n","import type { AttributeValue, Attributes } from '@opentelemetry/api'\nimport type { ReadableSpan } from '@opentelemetry/sdk-trace-base'\n\nimport { withAttributes } from './spans'\n\nexport type CapturedContext = {\n distinctId?: string\n sessionId?: string\n properties?: Record<string, unknown>\n}\n\n// Structural so declarations type-check against posthog-node versions that have no getContext.\nexport type PostHogContextSource = {\n getContext?(): CapturedContext | undefined\n}\n\nconst DISTINCT_ID_ATTRIBUTE = 'posthog.distinct_id'\nconst SESSION_ID_ATTRIBUTE = '$session_id'\n\n// Ingestion prefers posthog.distinct_id, so adding it would override the user a span already names.\nconst IDENTITY_ATTRIBUTES = [\n DISTINCT_ID_ATTRIBUTE,\n 'user.id',\n 'ai.telemetry.metadata.posthog_distinct_id',\n 'ai.settings.context.posthog_distinct_id',\n]\n\ntype Primitive = string | number | boolean\n\nfunction isPrimitive(value: unknown): value is Primitive {\n return (\n typeof value === 'string' || typeof value === 'boolean' || (typeof value === 'number' && Number.isFinite(value))\n )\n}\n\nfunction isHomogeneousPrimitiveArray(value: unknown): value is string[] | number[] | boolean[] {\n return Array.isArray(value) && value.every((item) => isPrimitive(item) && typeof item === typeof value[0])\n}\n\nfunction rejectNonFinite(_key: string, value: unknown): unknown {\n if (typeof value === 'number' && !Number.isFinite(value)) {\n throw new TypeError('non-finite number')\n }\n return value\n}\n\nfunction toAttributeValue(value: unknown): AttributeValue | undefined {\n if (value === undefined || value === null || value === '') {\n return undefined\n }\n if (isPrimitive(value) || isHomogeneousPrimitiveArray(value)) {\n return value\n }\n try {\n const json = JSON.stringify(value, rejectNonFinite)\n if (json === undefined || json === 'null') {\n return undefined\n }\n return json.startsWith('\"') ? String(JSON.parse(json)) || undefined : json\n } catch {\n return undefined\n }\n}\n\nfunction hasValues(context: CapturedContext): boolean {\n return Boolean(context.distinctId || context.sessionId || Object.keys(context.properties ?? {}).length > 0)\n}\n\nexport function captureContext(client: PostHogContextSource): CapturedContext | undefined {\n try {\n // posthog-node versions before getContext existed are still within the peer range.\n if (typeof client.getContext !== 'function') {\n return undefined\n }\n const context = client.getContext()\n return context && hasValues(context) ? context : undefined\n } catch {\n // Capturing is best effort and must never break the instrumented call.\n return undefined\n }\n}\n\nfunction toAttributes(context: CapturedContext, spanAttributes: Attributes): Attributes {\n const hasIdentity = IDENTITY_ATTRIBUTES.some((key) => spanAttributes[key] !== undefined)\n const values: Record<string, unknown> = {\n ...(context.distinctId && !hasIdentity ? { [DISTINCT_ID_ATTRIBUTE]: context.distinctId } : {}),\n ...(context.sessionId ? { [SESSION_ID_ATTRIBUTE]: context.sessionId } : {}),\n ...context.properties,\n }\n const attributes: Attributes = {}\n for (const [key, value] of Object.entries(values)) {\n if (hasIdentity && key === DISTINCT_ID_ATTRIBUTE) {\n continue\n }\n const attribute = toAttributeValue(value)\n if (attribute !== undefined) {\n attributes[key] = attribute\n }\n }\n return attributes\n}\n\nexport function withRequestContext(span: ReadableSpan, context: CapturedContext): ReadableSpan {\n try {\n const attributes = toAttributes(context, span.attributes)\n if (Object.keys(attributes).length === 0) {\n return span\n }\n return withAttributes(span, { ...attributes, ...span.attributes })\n } catch {\n // Conversion runs inside span.end() and must never break the instrumented call.\n return span\n }\n}\n","import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http'\nimport type { Context } from '@opentelemetry/api'\nimport { BatchSpanProcessor, type SpanProcessor, type ReadableSpan, type Span } from '@opentelemetry/sdk-trace-base'\n\nimport { redactSpan } from './redact'\nimport { captureContext, withRequestContext, type CapturedContext, type PostHogContextSource } from './requestContext'\nimport { isAISpan } from './spans'\nimport { warnIfPostHogAiGatewayOtelAttributes } from '../gatewayWarning'\n\nconst DEFAULT_OTEL_HOST = 'https://us.i.posthog.com'\n\nfunction normalizeToken(value?: unknown): string {\n return typeof value === 'string' ? value.trim() : ''\n}\n\nfunction normalizeHost(value?: unknown): string {\n const normalizedValue = typeof value === 'string' ? value.trim() : ''\n return normalizedValue || DEFAULT_OTEL_HOST\n}\n\nexport interface PostHogSpanProcessorOptions {\n /**\n * Your PostHog project token (the `phc_...` key). Required; a blank token disables the\n * processor as a defensive no-op.\n */\n projectToken: string\n\n /**\n * PostHog host URL. Defaults to `https://us.i.posthog.com`.\n */\n host?: string\n\n /**\n * Your posthog-node client. When set, each AI span this processor exports gets the distinct ID,\n * session ID (as `$session_id`) and properties from the context entered with `client.withContext()` or\n * `client.enterContext()`. Other processors on the provider don't see them.\n * Attributes already on the span win over properties, which win over the context's IDs.\n * The context is captured when a span starts, but its properties are read when the span ends,\n * so mutating a context object while a span is open shows up in the export.\n * Must be the same posthog-node instance you call `withContext` or `enterContext` on.\n */\n client?: PostHogContextSource\n\n /**\n * @internal Injected processor for testing — bypasses exporter creation.\n */\n _spanProcessor?: SpanProcessor\n}\n\nclass NoopSpanProcessor implements SpanProcessor {\n onStart(_span: Span, _parentContext: Context): void {\n return\n }\n onEnd(_span: ReadableSpan): void {\n return\n }\n shutdown(): Promise<void> {\n return Promise.resolve()\n }\n forceFlush(): Promise<void> {\n return Promise.resolve()\n }\n}\n\n/**\n * An OpenTelemetry `SpanProcessor` that sends AI traces to PostHog.\n *\n * `projectToken` is required; a blank token disables the processor as a defensive no-op.\n *\n * Internally batches spans and exports them to PostHog's OTLP ingestion\n * endpoint. Only AI-related spans (those whose name or attribute keys\n * start with `gen_ai.`, `llm.`, `ai.`, or `traceloop.`) are exported;\n * all other spans are silently dropped.\n *\n * Request-scoped and serverless runtimes should keep a reference to this\n * processor and await {@link PostHogSpanProcessor.forceFlush} before the request\n * lifecycle ends. This waits for queued exports without sending one request per span.\n *\n * This is the recommended integration point when your setup accepts a\n * `SpanProcessor`. If you need a `TraceExporter` instead (e.g. for\n * Vercel's `registerOTel`), use {@link PostHogTraceExporter}.\n *\n * @example\n * ```ts\n * import { PostHogSpanProcessor } from '@posthog/ai/otel'\n * import { NodeSDK } from '@opentelemetry/sdk-node'\n *\n * const processor = new PostHogSpanProcessor({ projectToken: 'phc_...' })\n * const sdk = new NodeSDK({ spanProcessors: [processor] })\n * sdk.start()\n *\n * // In request-scoped runtimes, wait for queued exports before returning.\n * await processor.forceFlush()\n * ```\n *\n * @example\n * ```ts\n * // Per request: pass your posthog-node client, then wrap the work in a context.\n * const processor = new PostHogSpanProcessor({ projectToken: 'phc_...', client: posthog })\n * await posthog.withContext({ distinctId: user.id, properties: { plan: 'pro' } }, () => handleRequest())\n * ```\n */\nexport class PostHogSpanProcessor implements SpanProcessor {\n private readonly inner: SpanProcessor\n private readonly client?: PostHogContextSource\n private readonly capturedContexts = new WeakMap<ReadableSpan, CapturedContext>()\n\n constructor(options: PostHogSpanProcessorOptions) {\n const token = normalizeToken(options.projectToken)\n if (!token) {\n console.warn('[PostHogSpanProcessor] projectToken is missing or blank; the processor will be disabled.')\n this.inner = new NoopSpanProcessor()\n return\n }\n this.client = options.client\n\n if (options._spanProcessor) {\n this.inner = options._spanProcessor\n } else {\n const host = new URL(normalizeHost(options.host)).origin\n const exporter = new OTLPTraceExporter({\n url: `${host}/i/v0/ai/otel`,\n headers: {\n Authorization: `Bearer ${token}`,\n },\n })\n this.inner = new BatchSpanProcessor(exporter)\n }\n }\n\n onStart(span: Span, parentContext: Context): void {\n const captured = this.client && captureContext(this.client)\n if (captured) {\n this.capturedContexts.set(span, captured)\n }\n // Forwarded unconditionally — filtering happens in onEnd. We can't filter\n // here because the span hasn't finished yet and may not have AI attributes\n // set. BatchSpanProcessor.onStart is a no-op so this is safe.\n this.inner.onStart(span, parentContext)\n }\n\n onEnd(span: ReadableSpan): void {\n const captured = this.capturedContexts.get(span)\n this.capturedContexts.delete(span)\n if (isAISpan(span)) {\n warnIfPostHogAiGatewayOtelAttributes(span.attributes)\n this.inner.onEnd(redactSpan(captured ? withRequestContext(span, captured) : span))\n }\n }\n\n shutdown(): Promise<void> {\n return this.inner.shutdown()\n }\n\n forceFlush(): Promise<void> {\n return this.inner.forceFlush()\n }\n}\n"],"mappings":"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"}
@@ -41,6 +41,9 @@ type PostHogTraceExporterOptions = {
41
41
  * `projectToken` is required; a blank token disables the exporter as a defensive no-op.
42
42
  * You can also optionally override the `host` URL.
43
43
  *
44
+ * Exporters only see finished spans, so they can't add per-request PostHog context;
45
+ * use {@link PostHogSpanProcessor} with `client` for that.
46
+ *
44
47
  * @example
45
48
  * ```ts
46
49
  * import { PostHogTraceExporter } from '@posthog/ai/otel'
@@ -61,6 +64,16 @@ declare class PostHogTraceExporter extends OTLPTraceExporter {
61
64
  }) => void): void;
62
65
  }
63
66
  //#endregion
67
+ //#region src/otel/requestContext.d.ts
68
+ type CapturedContext = {
69
+ distinctId?: string;
70
+ sessionId?: string;
71
+ properties?: Record<string, unknown>;
72
+ };
73
+ type PostHogContextSource = {
74
+ getContext?(): CapturedContext | undefined;
75
+ };
76
+ //#endregion
64
77
  //#region src/otel/processor.d.ts
65
78
  interface PostHogSpanProcessorOptions {
66
79
  /**
@@ -72,6 +85,16 @@ interface PostHogSpanProcessorOptions {
72
85
  * PostHog host URL. Defaults to `https://us.i.posthog.com`.
73
86
  */
74
87
  host?: string;
88
+ /**
89
+ * Your posthog-node client. When set, each AI span this processor exports gets the distinct ID,
90
+ * session ID (as `$session_id`) and properties from the context entered with `client.withContext()` or
91
+ * `client.enterContext()`. Other processors on the provider don't see them.
92
+ * Attributes already on the span win over properties, which win over the context's IDs.
93
+ * The context is captured when a span starts, but its properties are read when the span ends,
94
+ * so mutating a context object while a span is open shows up in the export.
95
+ * Must be the same posthog-node instance you call `withContext` or `enterContext` on.
96
+ */
97
+ client?: PostHogContextSource;
75
98
  /**
76
99
  * @internal Injected processor for testing — bypasses exporter creation.
77
100
  */
@@ -107,9 +130,18 @@ interface PostHogSpanProcessorOptions {
107
130
  * // In request-scoped runtimes, wait for queued exports before returning.
108
131
  * await processor.forceFlush()
109
132
  * ```
133
+ *
134
+ * @example
135
+ * ```ts
136
+ * // Per request: pass your posthog-node client, then wrap the work in a context.
137
+ * const processor = new PostHogSpanProcessor({ projectToken: 'phc_...', client: posthog })
138
+ * await posthog.withContext({ distinctId: user.id, properties: { plan: 'pro' } }, () => handleRequest())
139
+ * ```
110
140
  */
111
141
  declare class PostHogSpanProcessor implements SpanProcessor {
112
142
  private readonly inner;
143
+ private readonly client?;
144
+ private readonly capturedContexts;
113
145
  constructor(options: PostHogSpanProcessorOptions);
114
146
  onStart(span: Span, parentContext: Context): void;
115
147
  onEnd(span: ReadableSpan): void;
@@ -117,5 +149,5 @@ declare class PostHogSpanProcessor implements SpanProcessor {
117
149
  forceFlush(): Promise<void>;
118
150
  }
119
151
  //#endregion
120
- export { PostHogSpanProcessor, type PostHogSpanProcessorOptions, PostHogTraceExporter, type PostHogTraceExporterOptions };
152
+ export { type PostHogContextSource, PostHogSpanProcessor, type PostHogSpanProcessorOptions, PostHogTraceExporter, type PostHogTraceExporterOptions };
121
153
  //# sourceMappingURL=index.d.ts.map
@@ -174,25 +174,47 @@ var BinaryContentRedactor = class {
174
174
  }
175
175
  };
176
176
  //#endregion
177
- //#region src/otel/redact.ts
178
- const redactor = new BinaryContentRedactor();
179
- function redactSpan(span) {
180
- const attributes = span.attributes ? redactAttributes(span.attributes) : span.attributes;
181
- const events = span.events ? redactEvents(span.events) : span.events;
182
- if (attributes === span.attributes && events === span.events) return span;
177
+ //#region src/otel/spans.ts
178
+ const AI_SPAN_PREFIXES = [
179
+ "gen_ai.",
180
+ "llm.",
181
+ "ai.",
182
+ "traceloop."
183
+ ];
184
+ /**
185
+ * Returns `true` when the span is AI-related — its name or any attribute
186
+ * key starts with `gen_ai.`, `llm.`, `ai.`, or `traceloop.`.
187
+ */
188
+ function isAISpan(span) {
189
+ if (AI_SPAN_PREFIXES.some((prefix) => span.name.startsWith(prefix))) return true;
190
+ const attributes = span.attributes;
191
+ if (attributes) return Object.keys(attributes).some((key) => AI_SPAN_PREFIXES.some((prefix) => key.startsWith(prefix)));
192
+ return false;
193
+ }
194
+ /** Copies rather than mutates: the span is shared across every registered processor/exporter. */
195
+ function withAttributes(span, attributes, events) {
183
196
  return Object.create(span, {
184
197
  attributes: {
185
198
  value: attributes,
186
199
  enumerable: true,
187
200
  configurable: true
188
201
  },
189
- events: {
202
+ ...events ? { events: {
190
203
  value: events,
191
204
  enumerable: true,
192
205
  configurable: true
193
- }
206
+ } } : {}
194
207
  });
195
208
  }
209
+ //#endregion
210
+ //#region src/otel/redact.ts
211
+ const redactor = new BinaryContentRedactor();
212
+ function redactSpan(span) {
213
+ const attributes = span.attributes ? redactAttributes(span.attributes) : span.attributes;
214
+ const events = span.events ? redactEvents(span.events) : span.events;
215
+ if (attributes === span.attributes && events === span.events) return span;
216
+ return withAttributes(span, attributes, events);
217
+ }
196
218
  function redactAttributes(attributes) {
197
219
  let changed = false;
198
220
  const out = {};
@@ -255,24 +277,6 @@ function redactJson(value) {
255
277
  return redactedStr === JSON.stringify(parsed) ? value : redactedStr;
256
278
  }
257
279
  //#endregion
258
- //#region src/otel/spans.ts
259
- const AI_SPAN_PREFIXES = [
260
- "gen_ai.",
261
- "llm.",
262
- "ai.",
263
- "traceloop."
264
- ];
265
- /**
266
- * Returns `true` when the span is AI-related — its name or any attribute
267
- * key starts with `gen_ai.`, `llm.`, `ai.`, or `traceloop.`.
268
- */
269
- function isAISpan(span) {
270
- if (AI_SPAN_PREFIXES.some((prefix) => span.name.startsWith(prefix))) return true;
271
- const attributes = span.attributes;
272
- if (attributes) return Object.keys(attributes).some((key) => AI_SPAN_PREFIXES.some((prefix) => key.startsWith(prefix)));
273
- return false;
274
- }
275
- //#endregion
276
280
  //#region src/gatewayWarning.ts
277
281
  const POSTHOG_AI_GATEWAY_HOSTS = [
278
282
  "gateway.posthog.com",
@@ -337,6 +341,9 @@ function normalizeHost$1(value) {
337
341
  * `projectToken` is required; a blank token disables the exporter as a defensive no-op.
338
342
  * You can also optionally override the `host` URL.
339
343
  *
344
+ * Exporters only see finished spans, so they can't add per-request PostHog context;
345
+ * use {@link PostHogSpanProcessor} with `client` for that.
346
+ *
340
347
  * @example
341
348
  * ```ts
342
349
  * import { PostHogTraceExporter } from '@posthog/ai/otel'
@@ -375,6 +382,76 @@ var PostHogTraceExporter = class extends OTLPTraceExporter {
375
382
  }
376
383
  };
377
384
  //#endregion
385
+ //#region src/otel/requestContext.ts
386
+ const DISTINCT_ID_ATTRIBUTE = "posthog.distinct_id";
387
+ const SESSION_ID_ATTRIBUTE = "$session_id";
388
+ const IDENTITY_ATTRIBUTES = [
389
+ DISTINCT_ID_ATTRIBUTE,
390
+ "user.id",
391
+ "ai.telemetry.metadata.posthog_distinct_id",
392
+ "ai.settings.context.posthog_distinct_id"
393
+ ];
394
+ function isPrimitive(value) {
395
+ return typeof value === "string" || typeof value === "boolean" || typeof value === "number" && Number.isFinite(value);
396
+ }
397
+ function isHomogeneousPrimitiveArray(value) {
398
+ return Array.isArray(value) && value.every((item) => isPrimitive(item) && typeof item === typeof value[0]);
399
+ }
400
+ function rejectNonFinite(_key, value) {
401
+ if (typeof value === "number" && !Number.isFinite(value)) throw new TypeError("non-finite number");
402
+ return value;
403
+ }
404
+ function toAttributeValue(value) {
405
+ if (value === void 0 || value === null || value === "") return;
406
+ if (isPrimitive(value) || isHomogeneousPrimitiveArray(value)) return value;
407
+ try {
408
+ const json = JSON.stringify(value, rejectNonFinite);
409
+ if (json === void 0 || json === "null") return;
410
+ return json.startsWith("\"") ? String(JSON.parse(json)) || void 0 : json;
411
+ } catch {
412
+ return;
413
+ }
414
+ }
415
+ function hasValues(context) {
416
+ return Boolean(context.distinctId || context.sessionId || Object.keys(context.properties ?? {}).length > 0);
417
+ }
418
+ function captureContext(client) {
419
+ try {
420
+ if (typeof client.getContext !== "function") return;
421
+ const context = client.getContext();
422
+ return context && hasValues(context) ? context : void 0;
423
+ } catch {
424
+ return;
425
+ }
426
+ }
427
+ function toAttributes(context, spanAttributes) {
428
+ const hasIdentity = IDENTITY_ATTRIBUTES.some((key) => spanAttributes[key] !== void 0);
429
+ const values = {
430
+ ...context.distinctId && !hasIdentity ? { [DISTINCT_ID_ATTRIBUTE]: context.distinctId } : {},
431
+ ...context.sessionId ? { [SESSION_ID_ATTRIBUTE]: context.sessionId } : {},
432
+ ...context.properties
433
+ };
434
+ const attributes = {};
435
+ for (const [key, value] of Object.entries(values)) {
436
+ if (hasIdentity && key === DISTINCT_ID_ATTRIBUTE) continue;
437
+ const attribute = toAttributeValue(value);
438
+ if (attribute !== void 0) attributes[key] = attribute;
439
+ }
440
+ return attributes;
441
+ }
442
+ function withRequestContext(span, context) {
443
+ try {
444
+ const attributes = toAttributes(context, span.attributes);
445
+ if (Object.keys(attributes).length === 0) return span;
446
+ return withAttributes(span, {
447
+ ...attributes,
448
+ ...span.attributes
449
+ });
450
+ } catch {
451
+ return span;
452
+ }
453
+ }
454
+ //#endregion
378
455
  //#region src/otel/processor.ts
379
456
  const DEFAULT_OTEL_HOST = "https://us.i.posthog.com";
380
457
  function normalizeToken(value) {
@@ -423,15 +500,24 @@ var NoopSpanProcessor = class {
423
500
  * // In request-scoped runtimes, wait for queued exports before returning.
424
501
  * await processor.forceFlush()
425
502
  * ```
503
+ *
504
+ * @example
505
+ * ```ts
506
+ * // Per request: pass your posthog-node client, then wrap the work in a context.
507
+ * const processor = new PostHogSpanProcessor({ projectToken: 'phc_...', client: posthog })
508
+ * await posthog.withContext({ distinctId: user.id, properties: { plan: 'pro' } }, () => handleRequest())
509
+ * ```
426
510
  */
427
511
  var PostHogSpanProcessor = class {
428
512
  constructor(options) {
513
+ this.capturedContexts = /* @__PURE__ */ new WeakMap();
429
514
  const token = normalizeToken(options.projectToken);
430
515
  if (!token) {
431
516
  console.warn("[PostHogSpanProcessor] projectToken is missing or blank; the processor will be disabled.");
432
517
  this.inner = new NoopSpanProcessor();
433
518
  return;
434
519
  }
520
+ this.client = options.client;
435
521
  if (options._spanProcessor) this.inner = options._spanProcessor;
436
522
  else {
437
523
  const host = new URL(normalizeHost(options.host)).origin;
@@ -443,12 +529,16 @@ var PostHogSpanProcessor = class {
443
529
  }
444
530
  }
445
531
  onStart(span, parentContext) {
532
+ const captured = this.client && captureContext(this.client);
533
+ if (captured) this.capturedContexts.set(span, captured);
446
534
  this.inner.onStart(span, parentContext);
447
535
  }
448
536
  onEnd(span) {
537
+ const captured = this.capturedContexts.get(span);
538
+ this.capturedContexts.delete(span);
449
539
  if (isAISpan(span)) {
450
540
  warnIfPostHogAiGatewayOtelAttributes(span.attributes);
451
- this.inner.onEnd(redactSpan(span));
541
+ this.inner.onEnd(redactSpan(captured ? withRequestContext(span, captured) : span));
452
542
  }
453
543
  }
454
544
  shutdown() {
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":["DEFAULT_OTEL_HOST","normalizeToken","normalizeHost"],"sources":["../../src/sanitization/base64_recognizer.ts","../../src/sanitization/media_type_context.ts","../../src/sanitization/binary_content_redactor.ts","../../src/otel/redact.ts","../../src/otel/spans.ts","../../src/gatewayWarning.ts","../../src/otel/exporter.ts","../../src/otel/processor.ts"],"sourcesContent":["const DATA_URL_PREFIX_RE = /^data:([^;,\\s]+)(?:;[^;,\\s]+)*;base64,/i\nconst BASE64_ALPHABET_RE = /^[A-Za-z0-9+/_=-]+$/\n\nexport type Base64Recognition = { kind: 'data-url'; mediaType: string } | { kind: 'raw' } | { kind: 'none' }\n\nexport class Base64Recognizer {\n recognize(value: string, minLength: number): Base64Recognition {\n const dataUrl = DATA_URL_PREFIX_RE.exec(value)\n if (dataUrl) return { kind: 'data-url', mediaType: dataUrl[1] }\n\n if (value.length < minLength) return { kind: 'none' }\n\n const confidencePrefix = value.slice(0, minLength)\n if (BASE64_ALPHABET_RE.test(confidencePrefix)) {\n return { kind: 'raw' }\n } else {\n return { kind: 'none' }\n }\n }\n}\n","const MIME_HINT_KEYS = ['mediaType', 'media_type', 'mimeType', 'mime_type'] as const\n\nconst STRONG_CONTEXT_KEYS = new Set([\n 'data',\n 'file_data',\n 'fileData',\n 'image_url',\n 'imageUrl',\n 'video_url',\n 'videoUrl',\n 'audio',\n 'audio_data',\n 'audioData',\n 'inline_data',\n 'inlineData',\n 'source',\n 'result',\n])\n\nconst STRONG_CONTEXT_TYPES = new Set([\n 'image',\n 'image_url',\n 'input_image',\n 'audio',\n 'input_audio',\n 'video',\n 'video_url',\n 'file',\n 'input_file',\n 'document',\n 'media',\n 'file-data',\n])\n\nconst FILE_FAMILY_TYPES = new Set(['file', 'input_file', 'document', 'media', 'file-data'])\n\nconst KNOWN_AUDIO_FORMATS = new Set(['wav', 'mp3', 'ogg', 'flac', 'm4a', 'aac', 'webm'])\n\nexport class MediaTypeContext {\n static readonly EMPTY = new MediaTypeContext(undefined, undefined)\n\n constructor(\n private readonly parent: Record<string, unknown> | undefined,\n private readonly key: string | undefined,\n private readonly explicitMediaType?: string\n ) {}\n\n inferMediaType(): string | undefined {\n return (\n this.inferFromSiblingMime() ?? this.inferFromSiblingFormat() ?? this.inferFromParentType() ?? this.inferFromKey()\n )\n }\n\n inferFromSiblingMime(): string | undefined {\n if (this.explicitMediaType) return this.explicitMediaType\n if (!this.parent) return undefined\n for (const hint of MIME_HINT_KEYS) {\n const v = this.parent[hint]\n if (typeof v === 'string') return v\n }\n return undefined\n }\n\n inferFromSiblingFormat(): string | undefined {\n if (!this.parent) return undefined\n const fmt = this.parent.format\n if (typeof fmt === 'string' && KNOWN_AUDIO_FORMATS.has(fmt.toLowerCase())) {\n return `audio/${fmt.toLowerCase()}`\n }\n return undefined\n }\n\n inferFromParentType(): string | undefined {\n if (!this.parent) return undefined\n const t = this.parent.type\n if (typeof t !== 'string') return undefined\n if (t === 'image' || t === 'image_url' || t === 'input_image') return 'image'\n if (t === 'audio' || t === 'input_audio') return 'audio'\n if (t === 'video' || t === 'video_url') return 'video'\n if (FILE_FAMILY_TYPES.has(t)) return 'application/octet-stream'\n return undefined\n }\n\n inferFromKey(): string | undefined {\n if (!this.key) return undefined\n const key = this.key.toLowerCase()\n if (key.includes('audio')) return 'audio'\n if (key.includes('video')) return 'video'\n if (key.includes('image')) return 'image'\n if (key.includes('file') || key.includes('document')) return 'application/octet-stream'\n return undefined\n }\n\n hasExplicitBinaryMediaType(): boolean {\n if (!this.explicitMediaType && (!this.parent || !this.key || !STRONG_CONTEXT_KEYS.has(this.key))) return false\n const mediaType = this.inferFromSiblingMime()\n return mediaType !== undefined && !mediaType.toLowerCase().startsWith('text/')\n }\n\n signalsBinary(): boolean {\n if (this.explicitMediaType) return true\n if (this.parent) {\n for (const hint of MIME_HINT_KEYS) {\n if (typeof this.parent[hint] === 'string') return true\n }\n const fmt = this.parent.format\n if (typeof fmt === 'string' && KNOWN_AUDIO_FORMATS.has(fmt.toLowerCase())) return true\n const t = this.parent.type\n if (typeof t === 'string' && STRONG_CONTEXT_TYPES.has(t)) return true\n }\n if (this.key && STRONG_CONTEXT_KEYS.has(this.key)) return true\n return false\n }\n}\n","import { Base64Recognizer } from './base64_recognizer'\nimport { MediaTypeContext } from './media_type_context'\n\nconst STRONG_CONTEXT_MIN_LENGTH = 64\nconst WEAK_CONTEXT_MIN_LENGTH = 1024\n\nexport class BinaryContentRedactor {\n private visited: WeakSet<object> = new WeakSet()\n\n constructor(private readonly recognizer: Base64Recognizer = new Base64Recognizer()) {}\n\n redact<T>(value: T, mediaType?: string): T\n redact(value: unknown, mediaType?: string): unknown {\n this.visited = new WeakSet()\n return this.walk(value, mediaType ? new MediaTypeContext(undefined, undefined, mediaType) : MediaTypeContext.EMPTY)\n }\n\n private walk(value: unknown, ctx: MediaTypeContext): unknown {\n if (value === null || value === undefined) return value\n if (typeof value === 'string') return this.redactString(value, ctx)\n if (typeof value !== 'object') return value\n\n // Buffer extends Uint8Array, so this branch catches both.\n if (typeof Uint8Array !== 'undefined' && value instanceof Uint8Array) {\n return this.placeholderFor(ctx.inferMediaType())\n }\n\n if (this.visited.has(value)) return null\n this.visited.add(value)\n\n if (Array.isArray(value)) {\n return value.map((item) => this.walk(item, ctx))\n }\n\n const obj = value as Record<string, unknown>\n const out: Record<string, unknown> = {}\n for (const k of Object.keys(obj)) {\n out[k] = this.walk(obj[k], new MediaTypeContext(obj, k))\n }\n return out\n }\n\n private redactString(value: string, ctx: MediaTypeContext): string {\n const hasExplicitBinaryMediaType = ctx.hasExplicitBinaryMediaType()\n const recognitionValue = hasExplicitBinaryMediaType ? value.replace(/[\\r\\n]/g, '') : value\n const minLength = hasExplicitBinaryMediaType\n ? Math.min(recognitionValue.length, STRONG_CONTEXT_MIN_LENGTH)\n : ctx.signalsBinary()\n ? STRONG_CONTEXT_MIN_LENGTH\n : WEAK_CONTEXT_MIN_LENGTH\n const recognition = this.recognizer.recognize(recognitionValue, minLength)\n switch (recognition.kind) {\n case 'data-url':\n return this.placeholderFor(recognition.mediaType)\n case 'raw':\n return this.placeholderFor(ctx.inferMediaType())\n case 'none':\n return value\n }\n }\n\n private placeholderFor(mediaType?: string): string {\n if (!mediaType) return '[base64 redacted]'\n if (mediaType === 'application/octet-stream') return '[base64 file redacted]'\n return `[base64 ${mediaType} redacted]`\n }\n}\n","import type { Attributes, AttributeValue } from '@opentelemetry/api'\nimport type { ReadableSpan, TimedEvent } from '@opentelemetry/sdk-trace-base'\n\nimport { BinaryContentRedactor } from '../sanitization/binary_content_redactor'\n\n// Deliberately always redacts here — the OTLP export path has no per-client passthrough gate.\nconst redactor = new BinaryContentRedactor()\n\nexport function redactSpan(span: ReadableSpan): ReadableSpan {\n const attributes = span.attributes ? redactAttributes(span.attributes) : span.attributes\n const events = span.events ? redactEvents(span.events) : span.events\n if (attributes === span.attributes && events === span.events) {\n return span\n }\n // Copy rather than mutate: the span is shared across every registered processor/exporter.\n return Object.create(span, {\n attributes: { value: attributes, enumerable: true, configurable: true },\n events: { value: events, enumerable: true, configurable: true },\n })\n}\n\nfunction redactAttributes(attributes: Attributes): Attributes {\n let changed = false\n const out: Attributes = {}\n for (const key of Object.keys(attributes)) {\n const value = attributes[key]\n const redacted = value === undefined ? value : redactAttributeValue(value)\n if (redacted !== value) {\n changed = true\n }\n out[key] = redacted\n }\n return changed ? out : attributes\n}\n\nfunction redactEvents(events: TimedEvent[]): TimedEvent[] {\n let changed = false\n const out = events.map((event) => {\n if (!event.attributes) {\n return event\n }\n const attributes = redactAttributes(event.attributes)\n if (attributes === event.attributes) {\n return event\n }\n changed = true\n return { ...event, attributes }\n })\n return changed ? out : events\n}\n\nfunction redactAttributeValue(value: AttributeValue): AttributeValue {\n if (typeof value === 'string') {\n return redactString(value)\n }\n if (isStringArray(value)) {\n let changed = false\n const out = value.map((item) => {\n if (typeof item !== 'string') {\n return item\n }\n const redacted = redactString(item)\n if (redacted !== item) {\n changed = true\n }\n return redacted\n })\n return changed ? out : value\n }\n return value\n}\n\nfunction isStringArray(value: AttributeValue): value is Array<string | null | undefined> {\n return Array.isArray(value) && value.every((item) => typeof item !== 'number' && typeof item !== 'boolean')\n}\n\nfunction redactString(value: string): string {\n const trimmed = value.trimStart()\n if (trimmed.startsWith('{') || trimmed.startsWith('[')) {\n const redacted = redactJson(value)\n if (redacted !== undefined) {\n return redacted\n }\n }\n return redactor.redact(value)\n}\n\nfunction redactJson(value: string): string | undefined {\n let parsed: unknown\n try {\n parsed = JSON.parse(value)\n } catch {\n return undefined\n }\n if (parsed === null || typeof parsed !== 'object') {\n return undefined\n }\n const redactedStr = JSON.stringify(redactor.redact(parsed))\n // Preserve the original string (and its formatting) when nothing was redacted.\n return redactedStr === JSON.stringify(parsed) ? value : redactedStr\n}\n","import type { ReadableSpan } from '@opentelemetry/sdk-trace-base'\n\nconst AI_SPAN_PREFIXES = ['gen_ai.', 'llm.', 'ai.', 'traceloop.'] as const\n\n/**\n * Returns `true` when the span is AI-related — its name or any attribute\n * key starts with `gen_ai.`, `llm.`, `ai.`, or `traceloop.`.\n */\nexport function isAISpan(span: ReadableSpan): boolean {\n if (AI_SPAN_PREFIXES.some((prefix) => span.name.startsWith(prefix))) {\n return true\n }\n const attributes = span.attributes\n if (attributes) {\n return Object.keys(attributes).some((key) => AI_SPAN_PREFIXES.some((prefix) => key.startsWith(prefix)))\n }\n return false\n}\n","// Warn when a wrapper's base_url points at the PostHog AI Gateway: the gateway\n// emits its own $ai_generation, so each call would be captured (and, for billable\n// products, billed) twice. We only warn — the wrapper's event carries data the\n// gateway never sees (groups, custom properties, trace hierarchy).\n\n// Keep in sync with the gateway's deployed hosts (see services/llm-gateway in the\n// main repo). gateway.us.posthog.com is live today; the rest are listed ahead of\n// any traffic moving to them.\nexport const POSTHOG_AI_GATEWAY_HOSTS: readonly string[] = [\n 'gateway.posthog.com',\n 'gateway.us.posthog.com',\n 'gateway.eu.posthog.com',\n 'ai-gateway.us.posthog.com',\n 'ai-gateway.eu.posthog.com',\n]\n\n// Swap for the dedicated AI Gateway page once it ships.\nconst GATEWAY_DOCS_URL = 'https://posthog.com/docs/ai-observability'\n\nconst extractHost = (baseURL: string): string | undefined => {\n try {\n // Tolerate bare hosts that omit a scheme, e.g. \"gateway.us.posthog.com/v1\".\n const hasScheme = /^[a-z][a-z0-9+.-]*:\\/\\//i.test(baseURL)\n return new URL(hasScheme ? baseURL : `https://${baseURL}`).hostname.toLowerCase()\n } catch {\n return undefined\n }\n}\n\nexport const isPostHogAiGatewayUrl = (baseURL: string | undefined | null): boolean => {\n if (!baseURL) {\n return false\n }\n const host = extractHost(baseURL)\n return host !== undefined && POSTHOG_AI_GATEWAY_HOSTS.includes(host)\n}\n\n// Warns on every gateway call by design: the misconfiguration is impossible to\n// miss that way, and a doubled bill is worse than noisy logs.\nexport const warnIfPostHogAiGateway = (baseURL: string | undefined | null): void => {\n if (!isPostHogAiGatewayUrl(baseURL)) {\n return\n }\n console.warn(\n '[PostHog] The PostHog AI wrapper is pointed at the PostHog AI Gateway. ' +\n 'Both capture $ai_generation, so every call is double-counted and double-billed. ' +\n `Use one or the other — see ${GATEWAY_DOCS_URL}.`\n )\n}\n\n// OTel spans don't pass through captureAiGeneration, so detect the gateway from\n// the span's host/URL attributes instead. These follow the GenAI / HTTP semantic\n// conventions: `server.address` is a bare host, `url.full` a full URL, both of\n// which isPostHogAiGatewayUrl accepts.\nconst OTEL_GATEWAY_URL_ATTRIBUTES = ['server.address', 'url.full'] as const\n\nexport const warnIfPostHogAiGatewayOtelAttributes = (attributes: Record<string, unknown> | undefined): void => {\n if (!attributes) {\n return\n }\n for (const key of OTEL_GATEWAY_URL_ATTRIBUTES) {\n const value = attributes[key]\n if (typeof value === 'string' && isPostHogAiGatewayUrl(value)) {\n warnIfPostHogAiGateway(value)\n return\n }\n }\n}\n","import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http'\nimport type { ReadableSpan } from '@opentelemetry/sdk-trace-base'\n\nimport { redactSpan } from './redact'\nimport { isAISpan } from './spans'\nimport { warnIfPostHogAiGatewayOtelAttributes } from '../gatewayWarning'\n\nconst DEFAULT_OTEL_HOST = 'https://us.i.posthog.com'\n// OpenTelemetry's ExportResultCode.SUCCESS is 0. Keep this local so loading the\n// published subpath does not require the exporter package's transitive dependencies.\nconst EXPORT_SUCCESS = 0\n\nfunction normalizeToken(value?: unknown): string {\n return typeof value === 'string' ? value.trim() : ''\n}\n\nfunction normalizeHost(value?: unknown): string {\n const normalizedValue = typeof value === 'string' ? value.trim() : ''\n return normalizedValue || DEFAULT_OTEL_HOST\n}\n\n/**\n * Options for the PostHogTraceExporter. `projectToken` is required; a blank token disables the\n * exporter as a defensive no-op. You can also optionally override the `host` URL. `host` defaults to `https://us.i.posthog.com`.\n *\n * @example\n * ```ts\n * import { PostHogTraceExporter } from '@posthog/ai/otel'\n *\n * new PostHogTraceExporter({ projectToken: 'phc_...' })\n * ```\n *\n * @example\n * ```ts\n * import { PostHogTraceExporter } from '@posthog/ai/otel'\n *\n * new PostHogTraceExporter({ projectToken: 'phc_...', host: 'https://eu.i.posthog.com' })\n * ```\n */\nexport type PostHogTraceExporterOptions = { projectToken: string; host?: string }\n\n/**\n * An OpenTelemetry `TraceExporter` that sends AI traces to PostHog's OTLP\n * ingestion endpoint. PostHog converts `gen_ai.*` spans into\n * `$ai_generation` events server-side.\n *\n * Only AI-related spans (those whose name or attribute keys start with\n * `gen_ai.`, `llm.`, `ai.`, or `traceloop.`) are exported; all other\n * spans are silently dropped.\n *\n * Use this when the API you're integrating with only accepts a\n * `TraceExporter` (e.g. Vercel's `registerOTel`) or when you need to\n * plug PostHog into an existing processor chain. Otherwise prefer\n * {@link PostHogSpanProcessor}, which is self-contained.\n *\n * `projectToken` is required; a blank token disables the exporter as a defensive no-op.\n * You can also optionally override the `host` URL.\n *\n * @example\n * ```ts\n * import { PostHogTraceExporter } from '@posthog/ai/otel'\n * import { registerOTel } from '@vercel/otel'\n *\n * registerOTel({\n * serviceName: 'my-app',\n * traceExporter: new PostHogTraceExporter({ projectToken: 'phc_...' }),\n * })\n * ```\n */\nexport class PostHogTraceExporter extends OTLPTraceExporter {\n private readonly disabled: boolean\n\n constructor(options: PostHogTraceExporterOptions) {\n const token = normalizeToken(options.projectToken)\n const disabled = !token\n const host = token ? new URL(normalizeHost(options.host)).origin : DEFAULT_OTEL_HOST\n super({\n url: `${host}/i/v0/ai/otel`,\n headers: token\n ? {\n Authorization: `Bearer ${token}`,\n }\n : {},\n })\n\n this.disabled = disabled\n if (this.disabled) {\n console.warn('[PostHogTraceExporter] projectToken is missing or blank; the exporter will be disabled.')\n }\n }\n\n override export(spans: ReadableSpan[], resultCallback: (result: { code: number; error?: Error }) => void): void {\n if (this.disabled) {\n // Intentionally report success: missing or blank tokens disable exporting as a compatibility no-op.\n // Reporting failure would make OpenTelemetry treat every span as an export error.\n resultCallback({ code: EXPORT_SUCCESS })\n return\n }\n\n const aiSpans = spans.filter(isAISpan)\n if (aiSpans.length === 0) {\n resultCallback({ code: EXPORT_SUCCESS })\n return\n }\n for (const span of aiSpans) {\n warnIfPostHogAiGatewayOtelAttributes(span.attributes)\n }\n super.export(aiSpans.map(redactSpan), resultCallback)\n }\n}\n","import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http'\nimport type { Context } from '@opentelemetry/api'\nimport { BatchSpanProcessor, type SpanProcessor, type ReadableSpan, type Span } from '@opentelemetry/sdk-trace-base'\n\nimport { redactSpan } from './redact'\nimport { isAISpan } from './spans'\nimport { warnIfPostHogAiGatewayOtelAttributes } from '../gatewayWarning'\n\nconst DEFAULT_OTEL_HOST = 'https://us.i.posthog.com'\n\nfunction normalizeToken(value?: unknown): string {\n return typeof value === 'string' ? value.trim() : ''\n}\n\nfunction normalizeHost(value?: unknown): string {\n const normalizedValue = typeof value === 'string' ? value.trim() : ''\n return normalizedValue || DEFAULT_OTEL_HOST\n}\n\nexport interface PostHogSpanProcessorOptions {\n /**\n * Your PostHog project token (the `phc_...` key). Required; a blank token disables the\n * processor as a defensive no-op.\n */\n projectToken: string\n\n /**\n * PostHog host URL. Defaults to `https://us.i.posthog.com`.\n */\n host?: string\n\n /**\n * @internal Injected processor for testing — bypasses exporter creation.\n */\n _spanProcessor?: SpanProcessor\n}\n\nclass NoopSpanProcessor implements SpanProcessor {\n onStart(_span: Span, _parentContext: Context): void {\n return\n }\n onEnd(_span: ReadableSpan): void {\n return\n }\n shutdown(): Promise<void> {\n return Promise.resolve()\n }\n forceFlush(): Promise<void> {\n return Promise.resolve()\n }\n}\n\n/**\n * An OpenTelemetry `SpanProcessor` that sends AI traces to PostHog.\n *\n * `projectToken` is required; a blank token disables the processor as a defensive no-op.\n *\n * Internally batches spans and exports them to PostHog's OTLP ingestion\n * endpoint. Only AI-related spans (those whose name or attribute keys\n * start with `gen_ai.`, `llm.`, `ai.`, or `traceloop.`) are exported;\n * all other spans are silently dropped.\n *\n * Request-scoped and serverless runtimes should keep a reference to this\n * processor and await {@link PostHogSpanProcessor.forceFlush} before the request\n * lifecycle ends. This waits for queued exports without sending one request per span.\n *\n * This is the recommended integration point when your setup accepts a\n * `SpanProcessor`. If you need a `TraceExporter` instead (e.g. for\n * Vercel's `registerOTel`), use {@link PostHogTraceExporter}.\n *\n * @example\n * ```ts\n * import { PostHogSpanProcessor } from '@posthog/ai/otel'\n * import { NodeSDK } from '@opentelemetry/sdk-node'\n *\n * const processor = new PostHogSpanProcessor({ projectToken: 'phc_...' })\n * const sdk = new NodeSDK({ spanProcessors: [processor] })\n * sdk.start()\n *\n * // In request-scoped runtimes, wait for queued exports before returning.\n * await processor.forceFlush()\n * ```\n */\nexport class PostHogSpanProcessor implements SpanProcessor {\n private readonly inner: SpanProcessor\n\n constructor(options: PostHogSpanProcessorOptions) {\n const token = normalizeToken(options.projectToken)\n if (!token) {\n console.warn('[PostHogSpanProcessor] projectToken is missing or blank; the processor will be disabled.')\n this.inner = new NoopSpanProcessor()\n return\n }\n\n if (options._spanProcessor) {\n this.inner = options._spanProcessor\n } else {\n const host = new URL(normalizeHost(options.host)).origin\n const exporter = new OTLPTraceExporter({\n url: `${host}/i/v0/ai/otel`,\n headers: {\n Authorization: `Bearer ${token}`,\n },\n })\n this.inner = new BatchSpanProcessor(exporter)\n }\n }\n\n onStart(span: Span, parentContext: Context): void {\n // Forwarded unconditionally — filtering happens in onEnd. We can't filter\n // here because the span hasn't finished yet and may not have AI attributes\n // set. BatchSpanProcessor.onStart is a no-op so this is safe.\n this.inner.onStart(span, parentContext)\n }\n\n onEnd(span: ReadableSpan): void {\n if (isAISpan(span)) {\n warnIfPostHogAiGatewayOtelAttributes(span.attributes)\n this.inner.onEnd(redactSpan(span))\n }\n }\n\n shutdown(): Promise<void> {\n return this.inner.shutdown()\n }\n\n forceFlush(): Promise<void> {\n return this.inner.forceFlush()\n }\n}\n"],"mappings":";;;AAAA,MAAM,qBAAqB;AAC3B,MAAM,qBAAqB;AAI3B,IAAa,mBAAb,MAA8B;CAC5B,UAAU,OAAe,WAAsC;EAC7D,MAAM,UAAU,mBAAmB,KAAK,KAAK;EAC7C,IAAI,SAAS,OAAO;GAAE,MAAM;GAAY,WAAW,QAAQ;EAAG;EAE9D,IAAI,MAAM,SAAS,WAAW,OAAO,EAAE,MAAM,OAAO;EAEpD,MAAM,mBAAmB,MAAM,MAAM,GAAG,SAAS;EACjD,IAAI,mBAAmB,KAAK,gBAAgB,GAC1C,OAAO,EAAE,MAAM,MAAM;OAErB,OAAO,EAAE,MAAM,OAAO;CAE1B;AACF;;;ACnBA,MAAM,iBAAiB;CAAC;CAAa;CAAc;CAAY;AAAW;AAE1E,MAAM,sCAAsB,IAAI,IAAI;CAClC;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF,CAAC;AAED,MAAM,uCAAuB,IAAI,IAAI;CACnC;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF,CAAC;AAED,MAAM,oCAAoB,IAAI,IAAI;CAAC;CAAQ;CAAc;CAAY;CAAS;AAAW,CAAC;AAE1F,MAAM,sCAAsB,IAAI,IAAI;CAAC;CAAO;CAAO;CAAO;CAAQ;CAAO;CAAO;AAAM,CAAC;AAEvF,IAAa,mBAAb,MAAa,iBAAiB;;EACJ,KAAA,QAAA,IAAI,iBAAiB,KAAA,GAAW,KAAA,CAAS;;CAEjE,YACE,QACA,KACA,mBACA;EAHiB,KAAA,SAAA;EACA,KAAA,MAAA;EACA,KAAA,oBAAA;CAChB;CAEH,iBAAqC;EACnC,OACE,KAAK,qBAAqB,KAAK,KAAK,uBAAuB,KAAK,KAAK,oBAAoB,KAAK,KAAK,aAAa;CAEpH;CAEA,uBAA2C;EACzC,IAAI,KAAK,mBAAmB,OAAO,KAAK;EACxC,IAAI,CAAC,KAAK,QAAQ,OAAO,KAAA;EACzB,KAAK,MAAM,QAAQ,gBAAgB;GACjC,MAAM,IAAI,KAAK,OAAO;GACtB,IAAI,OAAO,MAAM,UAAU,OAAO;EACpC;CAEF;CAEA,yBAA6C;EAC3C,IAAI,CAAC,KAAK,QAAQ,OAAO,KAAA;EACzB,MAAM,MAAM,KAAK,OAAO;EACxB,IAAI,OAAO,QAAQ,YAAY,oBAAoB,IAAI,IAAI,YAAY,CAAC,GACtE,OAAO,SAAS,IAAI,YAAY;CAGpC;CAEA,sBAA0C;EACxC,IAAI,CAAC,KAAK,QAAQ,OAAO,KAAA;EACzB,MAAM,IAAI,KAAK,OAAO;EACtB,IAAI,OAAO,MAAM,UAAU,OAAO,KAAA;EAClC,IAAI,MAAM,WAAW,MAAM,eAAe,MAAM,eAAe,OAAO;EACtE,IAAI,MAAM,WAAW,MAAM,eAAe,OAAO;EACjD,IAAI,MAAM,WAAW,MAAM,aAAa,OAAO;EAC/C,IAAI,kBAAkB,IAAI,CAAC,GAAG,OAAO;CAEvC;CAEA,eAAmC;EACjC,IAAI,CAAC,KAAK,KAAK,OAAO,KAAA;EACtB,MAAM,MAAM,KAAK,IAAI,YAAY;EACjC,IAAI,IAAI,SAAS,OAAO,GAAG,OAAO;EAClC,IAAI,IAAI,SAAS,OAAO,GAAG,OAAO;EAClC,IAAI,IAAI,SAAS,OAAO,GAAG,OAAO;EAClC,IAAI,IAAI,SAAS,MAAM,KAAK,IAAI,SAAS,UAAU,GAAG,OAAO;CAE/D;CAEA,6BAAsC;EACpC,IAAI,CAAC,KAAK,sBAAsB,CAAC,KAAK,UAAU,CAAC,KAAK,OAAO,CAAC,oBAAoB,IAAI,KAAK,GAAG,IAAI,OAAO;EACzG,MAAM,YAAY,KAAK,qBAAqB;EAC5C,OAAO,cAAc,KAAA,KAAa,CAAC,UAAU,YAAY,CAAC,CAAC,WAAW,OAAO;CAC/E;CAEA,gBAAyB;EACvB,IAAI,KAAK,mBAAmB,OAAO;EACnC,IAAI,KAAK,QAAQ;GACf,KAAK,MAAM,QAAQ,gBACjB,IAAI,OAAO,KAAK,OAAO,UAAU,UAAU,OAAO;GAEpD,MAAM,MAAM,KAAK,OAAO;GACxB,IAAI,OAAO,QAAQ,YAAY,oBAAoB,IAAI,IAAI,YAAY,CAAC,GAAG,OAAO;GAClF,MAAM,IAAI,KAAK,OAAO;GACtB,IAAI,OAAO,MAAM,YAAY,qBAAqB,IAAI,CAAC,GAAG,OAAO;EACnE;EACA,IAAI,KAAK,OAAO,oBAAoB,IAAI,KAAK,GAAG,GAAG,OAAO;EAC1D,OAAO;CACT;AACF;;;AC9GA,MAAM,4BAA4B;AAClC,MAAM,0BAA0B;AAEhC,IAAa,wBAAb,MAAmC;CAGjC,YAAY,aAAgD,IAAI,iBAAiB,GAAG;EAAvD,KAAA,aAAA;EAFM,KAAA,0BAAA,IAAI,QAAQ;CAEsC;CAGrF,OAAO,OAAgB,WAA6B;EAClD,KAAK,0BAAU,IAAI,QAAQ;EAC3B,OAAO,KAAK,KAAK,OAAO,YAAY,IAAI,iBAAiB,KAAA,GAAW,KAAA,GAAW,SAAS,IAAI,iBAAiB,KAAK;CACpH;CAEA,KAAa,OAAgB,KAAgC;EAC3D,IAAI,UAAU,QAAQ,UAAU,KAAA,GAAW,OAAO;EAClD,IAAI,OAAO,UAAU,UAAU,OAAO,KAAK,aAAa,OAAO,GAAG;EAClE,IAAI,OAAO,UAAU,UAAU,OAAO;EAGtC,IAAI,OAAO,eAAe,eAAe,iBAAiB,YACxD,OAAO,KAAK,eAAe,IAAI,eAAe,CAAC;EAGjD,IAAI,KAAK,QAAQ,IAAI,KAAK,GAAG,OAAO;EACpC,KAAK,QAAQ,IAAI,KAAK;EAEtB,IAAI,MAAM,QAAQ,KAAK,GACrB,OAAO,MAAM,KAAK,SAAS,KAAK,KAAK,MAAM,GAAG,CAAC;EAGjD,MAAM,MAAM;EACZ,MAAM,MAA+B,CAAC;EACtC,KAAK,MAAM,KAAK,OAAO,KAAK,GAAG,GAC7B,IAAI,KAAK,KAAK,KAAK,IAAI,IAAI,IAAI,iBAAiB,KAAK,CAAC,CAAC;EAEzD,OAAO;CACT;CAEA,aAAqB,OAAe,KAA+B;EACjE,MAAM,6BAA6B,IAAI,2BAA2B;EAClE,MAAM,mBAAmB,6BAA6B,MAAM,QAAQ,WAAW,EAAE,IAAI;EACrF,MAAM,YAAY,6BACd,KAAK,IAAI,iBAAiB,QAAQ,yBAAyB,IAC3D,IAAI,cAAc,IAChB,4BACA;EACN,MAAM,cAAc,KAAK,WAAW,UAAU,kBAAkB,SAAS;EACzE,QAAQ,YAAY,MAApB;GACE,KAAK,YACH,OAAO,KAAK,eAAe,YAAY,SAAS;GAClD,KAAK,OACH,OAAO,KAAK,eAAe,IAAI,eAAe,CAAC;GACjD,KAAK,QACH,OAAO;EACX;CACF;CAEA,eAAuB,WAA4B;EACjD,IAAI,CAAC,WAAW,OAAO;EACvB,IAAI,cAAc,4BAA4B,OAAO;EACrD,OAAO,WAAW,UAAU;CAC9B;AACF;;;AC5DA,MAAM,WAAW,IAAI,sBAAsB;AAE3C,SAAgB,WAAW,MAAkC;CAC3D,MAAM,aAAa,KAAK,aAAa,iBAAiB,KAAK,UAAU,IAAI,KAAK;CAC9E,MAAM,SAAS,KAAK,SAAS,aAAa,KAAK,MAAM,IAAI,KAAK;CAC9D,IAAI,eAAe,KAAK,cAAc,WAAW,KAAK,QACpD,OAAO;CAGT,OAAO,OAAO,OAAO,MAAM;EACzB,YAAY;GAAE,OAAO;GAAY,YAAY;GAAM,cAAc;EAAK;EACtE,QAAQ;GAAE,OAAO;GAAQ,YAAY;GAAM,cAAc;EAAK;CAChE,CAAC;AACH;AAEA,SAAS,iBAAiB,YAAoC;CAC5D,IAAI,UAAU;CACd,MAAM,MAAkB,CAAC;CACzB,KAAK,MAAM,OAAO,OAAO,KAAK,UAAU,GAAG;EACzC,MAAM,QAAQ,WAAW;EACzB,MAAM,WAAW,UAAU,KAAA,IAAY,QAAQ,qBAAqB,KAAK;EACzE,IAAI,aAAa,OACf,UAAU;EAEZ,IAAI,OAAO;CACb;CACA,OAAO,UAAU,MAAM;AACzB;AAEA,SAAS,aAAa,QAAoC;CACxD,IAAI,UAAU;CACd,MAAM,MAAM,OAAO,KAAK,UAAU;EAChC,IAAI,CAAC,MAAM,YACT,OAAO;EAET,MAAM,aAAa,iBAAiB,MAAM,UAAU;EACpD,IAAI,eAAe,MAAM,YACvB,OAAO;EAET,UAAU;EACV,OAAO;GAAE,GAAG;GAAO;EAAW;CAChC,CAAC;CACD,OAAO,UAAU,MAAM;AACzB;AAEA,SAAS,qBAAqB,OAAuC;CACnE,IAAI,OAAO,UAAU,UACnB,OAAO,aAAa,KAAK;CAE3B,IAAI,cAAc,KAAK,GAAG;EACxB,IAAI,UAAU;EACd,MAAM,MAAM,MAAM,KAAK,SAAS;GAC9B,IAAI,OAAO,SAAS,UAClB,OAAO;GAET,MAAM,WAAW,aAAa,IAAI;GAClC,IAAI,aAAa,MACf,UAAU;GAEZ,OAAO;EACT,CAAC;EACD,OAAO,UAAU,MAAM;CACzB;CACA,OAAO;AACT;AAEA,SAAS,cAAc,OAAkE;CACvF,OAAO,MAAM,QAAQ,KAAK,KAAK,MAAM,OAAO,SAAS,OAAO,SAAS,YAAY,OAAO,SAAS,SAAS;AAC5G;AAEA,SAAS,aAAa,OAAuB;CAC3C,MAAM,UAAU,MAAM,UAAU;CAChC,IAAI,QAAQ,WAAW,GAAG,KAAK,QAAQ,WAAW,GAAG,GAAG;EACtD,MAAM,WAAW,WAAW,KAAK;EACjC,IAAI,aAAa,KAAA,GACf,OAAO;CAEX;CACA,OAAO,SAAS,OAAO,KAAK;AAC9B;AAEA,SAAS,WAAW,OAAmC;CACrD,IAAI;CACJ,IAAI;EACF,SAAS,KAAK,MAAM,KAAK;CAC3B,QAAQ;EACN;CACF;CACA,IAAI,WAAW,QAAQ,OAAO,WAAW,UACvC;CAEF,MAAM,cAAc,KAAK,UAAU,SAAS,OAAO,MAAM,CAAC;CAE1D,OAAO,gBAAgB,KAAK,UAAU,MAAM,IAAI,QAAQ;AAC1D;;;AClGA,MAAM,mBAAmB;CAAC;CAAW;CAAQ;CAAO;AAAY;;;;;AAMhE,SAAgB,SAAS,MAA6B;CACpD,IAAI,iBAAiB,MAAM,WAAW,KAAK,KAAK,WAAW,MAAM,CAAC,GAChE,OAAO;CAET,MAAM,aAAa,KAAK;CACxB,IAAI,YACF,OAAO,OAAO,KAAK,UAAU,CAAC,CAAC,MAAM,QAAQ,iBAAiB,MAAM,WAAW,IAAI,WAAW,MAAM,CAAC,CAAC;CAExG,OAAO;AACT;;;ACTA,MAAa,2BAA8C;CACzD;CACA;CACA;CACA;CACA;AACF;AAGA,MAAM,mBAAmB;AAEzB,MAAM,eAAe,YAAwC;CAC3D,IAAI;EAEF,MAAM,YAAY,2BAA2B,KAAK,OAAO;EACzD,OAAO,IAAI,IAAI,YAAY,UAAU,WAAW,SAAS,CAAC,CAAC,SAAS,YAAY;CAClF,QAAQ;EACN;CACF;AACF;AAEA,MAAa,yBAAyB,YAAgD;CACpF,IAAI,CAAC,SACH,OAAO;CAET,MAAM,OAAO,YAAY,OAAO;CAChC,OAAO,SAAS,KAAA,KAAa,yBAAyB,SAAS,IAAI;AACrE;AAIA,MAAa,0BAA0B,YAA6C;CAClF,IAAI,CAAC,sBAAsB,OAAO,GAChC;CAEF,QAAQ,KACN,sLAEgC,iBAAiB,EACnD;AACF;AAMA,MAAM,8BAA8B,CAAC,kBAAkB,UAAU;AAEjE,MAAa,wCAAwC,eAA0D;CAC7G,IAAI,CAAC,YACH;CAEF,KAAK,MAAM,OAAO,6BAA6B;EAC7C,MAAM,QAAQ,WAAW;EACzB,IAAI,OAAO,UAAU,YAAY,sBAAsB,KAAK,GAAG;GAC7D,uBAAuB,KAAK;GAC5B;EACF;CACF;AACF;;;AC5DA,MAAMA,sBAAoB;AAG1B,MAAM,iBAAiB;AAEvB,SAASC,iBAAe,OAAyB;CAC/C,OAAO,OAAO,UAAU,WAAW,MAAM,KAAK,IAAI;AACpD;AAEA,SAASC,gBAAc,OAAyB;CAE9C,QADwB,OAAO,UAAU,WAAW,MAAM,KAAK,IAAI,OACzCF;AAC5B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkDA,IAAa,uBAAb,cAA0C,kBAAkB;CAG1D,YAAY,SAAsC;EAChD,MAAM,QAAQC,iBAAe,QAAQ,YAAY;EACjD,MAAM,WAAW,CAAC;EAClB,MAAM,OAAO,QAAQ,IAAI,IAAIC,gBAAc,QAAQ,IAAI,CAAC,CAAC,CAAC,SAASF;EACnE,MAAM;GACJ,KAAK,GAAG,KAAK;GACb,SAAS,QACL,EACE,eAAe,UAAU,QAC3B,IACA,CAAC;EACP,CAAC;EAED,KAAK,WAAW;EAChB,IAAI,KAAK,UACP,QAAQ,KAAK,yFAAyF;CAE1G;CAEA,OAAgB,OAAuB,gBAAyE;EAC9G,IAAI,KAAK,UAAU;GAGjB,eAAe,EAAE,MAAM,eAAe,CAAC;GACvC;EACF;EAEA,MAAM,UAAU,MAAM,OAAO,QAAQ;EACrC,IAAI,QAAQ,WAAW,GAAG;GACxB,eAAe,EAAE,MAAM,eAAe,CAAC;GACvC;EACF;EACA,KAAK,MAAM,QAAQ,SACjB,qCAAqC,KAAK,UAAU;EAEtD,MAAM,OAAO,QAAQ,IAAI,UAAU,GAAG,cAAc;CACtD;AACF;;;ACrGA,MAAM,oBAAoB;AAE1B,SAAS,eAAe,OAAyB;CAC/C,OAAO,OAAO,UAAU,WAAW,MAAM,KAAK,IAAI;AACpD;AAEA,SAAS,cAAc,OAAyB;CAE9C,QADwB,OAAO,UAAU,WAAW,MAAM,KAAK,IAAI,OACzC;AAC5B;AAoBA,IAAM,oBAAN,MAAiD;CAC/C,QAAQ,OAAa,gBAA+B,CAEpD;CACA,MAAM,OAA2B,CAEjC;CACA,WAA0B;EACxB,OAAO,QAAQ,QAAQ;CACzB;CACA,aAA4B;EAC1B,OAAO,QAAQ,QAAQ;CACzB;AACF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAiCA,IAAa,uBAAb,MAA2D;CAGzD,YAAY,SAAsC;EAChD,MAAM,QAAQ,eAAe,QAAQ,YAAY;EACjD,IAAI,CAAC,OAAO;GACV,QAAQ,KAAK,0FAA0F;GACvG,KAAK,QAAQ,IAAI,kBAAkB;GACnC;EACF;EAEA,IAAI,QAAQ,gBACV,KAAK,QAAQ,QAAQ;OAChB;GACL,MAAM,OAAO,IAAI,IAAI,cAAc,QAAQ,IAAI,CAAC,CAAC,CAAC;GAClD,MAAM,WAAW,IAAI,kBAAkB;IACrC,KAAK,GAAG,KAAK;IACb,SAAS,EACP,eAAe,UAAU,QAC3B;GACF,CAAC;GACD,KAAK,QAAQ,IAAI,mBAAmB,QAAQ;EAC9C;CACF;CAEA,QAAQ,MAAY,eAA8B;EAIhD,KAAK,MAAM,QAAQ,MAAM,aAAa;CACxC;CAEA,MAAM,MAA0B;EAC9B,IAAI,SAAS,IAAI,GAAG;GAClB,qCAAqC,KAAK,UAAU;GACpD,KAAK,MAAM,MAAM,WAAW,IAAI,CAAC;EACnC;CACF;CAEA,WAA0B;EACxB,OAAO,KAAK,MAAM,SAAS;CAC7B;CAEA,aAA4B;EAC1B,OAAO,KAAK,MAAM,WAAW;CAC/B;AACF"}
1
+ {"version":3,"file":"index.mjs","names":["DEFAULT_OTEL_HOST","normalizeToken","normalizeHost"],"sources":["../../src/sanitization/base64_recognizer.ts","../../src/sanitization/media_type_context.ts","../../src/sanitization/binary_content_redactor.ts","../../src/otel/spans.ts","../../src/otel/redact.ts","../../src/gatewayWarning.ts","../../src/otel/exporter.ts","../../src/otel/requestContext.ts","../../src/otel/processor.ts"],"sourcesContent":["const DATA_URL_PREFIX_RE = /^data:([^;,\\s]+)(?:;[^;,\\s]+)*;base64,/i\nconst BASE64_ALPHABET_RE = /^[A-Za-z0-9+/_=-]+$/\n\nexport type Base64Recognition = { kind: 'data-url'; mediaType: string } | { kind: 'raw' } | { kind: 'none' }\n\nexport class Base64Recognizer {\n recognize(value: string, minLength: number): Base64Recognition {\n const dataUrl = DATA_URL_PREFIX_RE.exec(value)\n if (dataUrl) return { kind: 'data-url', mediaType: dataUrl[1] }\n\n if (value.length < minLength) return { kind: 'none' }\n\n const confidencePrefix = value.slice(0, minLength)\n if (BASE64_ALPHABET_RE.test(confidencePrefix)) {\n return { kind: 'raw' }\n } else {\n return { kind: 'none' }\n }\n }\n}\n","const MIME_HINT_KEYS = ['mediaType', 'media_type', 'mimeType', 'mime_type'] as const\n\nconst STRONG_CONTEXT_KEYS = new Set([\n 'data',\n 'file_data',\n 'fileData',\n 'image_url',\n 'imageUrl',\n 'video_url',\n 'videoUrl',\n 'audio',\n 'audio_data',\n 'audioData',\n 'inline_data',\n 'inlineData',\n 'source',\n 'result',\n])\n\nconst STRONG_CONTEXT_TYPES = new Set([\n 'image',\n 'image_url',\n 'input_image',\n 'audio',\n 'input_audio',\n 'video',\n 'video_url',\n 'file',\n 'input_file',\n 'document',\n 'media',\n 'file-data',\n])\n\nconst FILE_FAMILY_TYPES = new Set(['file', 'input_file', 'document', 'media', 'file-data'])\n\nconst KNOWN_AUDIO_FORMATS = new Set(['wav', 'mp3', 'ogg', 'flac', 'm4a', 'aac', 'webm'])\n\nexport class MediaTypeContext {\n static readonly EMPTY = new MediaTypeContext(undefined, undefined)\n\n constructor(\n private readonly parent: Record<string, unknown> | undefined,\n private readonly key: string | undefined,\n private readonly explicitMediaType?: string\n ) {}\n\n inferMediaType(): string | undefined {\n return (\n this.inferFromSiblingMime() ?? this.inferFromSiblingFormat() ?? this.inferFromParentType() ?? this.inferFromKey()\n )\n }\n\n inferFromSiblingMime(): string | undefined {\n if (this.explicitMediaType) return this.explicitMediaType\n if (!this.parent) return undefined\n for (const hint of MIME_HINT_KEYS) {\n const v = this.parent[hint]\n if (typeof v === 'string') return v\n }\n return undefined\n }\n\n inferFromSiblingFormat(): string | undefined {\n if (!this.parent) return undefined\n const fmt = this.parent.format\n if (typeof fmt === 'string' && KNOWN_AUDIO_FORMATS.has(fmt.toLowerCase())) {\n return `audio/${fmt.toLowerCase()}`\n }\n return undefined\n }\n\n inferFromParentType(): string | undefined {\n if (!this.parent) return undefined\n const t = this.parent.type\n if (typeof t !== 'string') return undefined\n if (t === 'image' || t === 'image_url' || t === 'input_image') return 'image'\n if (t === 'audio' || t === 'input_audio') return 'audio'\n if (t === 'video' || t === 'video_url') return 'video'\n if (FILE_FAMILY_TYPES.has(t)) return 'application/octet-stream'\n return undefined\n }\n\n inferFromKey(): string | undefined {\n if (!this.key) return undefined\n const key = this.key.toLowerCase()\n if (key.includes('audio')) return 'audio'\n if (key.includes('video')) return 'video'\n if (key.includes('image')) return 'image'\n if (key.includes('file') || key.includes('document')) return 'application/octet-stream'\n return undefined\n }\n\n hasExplicitBinaryMediaType(): boolean {\n if (!this.explicitMediaType && (!this.parent || !this.key || !STRONG_CONTEXT_KEYS.has(this.key))) return false\n const mediaType = this.inferFromSiblingMime()\n return mediaType !== undefined && !mediaType.toLowerCase().startsWith('text/')\n }\n\n signalsBinary(): boolean {\n if (this.explicitMediaType) return true\n if (this.parent) {\n for (const hint of MIME_HINT_KEYS) {\n if (typeof this.parent[hint] === 'string') return true\n }\n const fmt = this.parent.format\n if (typeof fmt === 'string' && KNOWN_AUDIO_FORMATS.has(fmt.toLowerCase())) return true\n const t = this.parent.type\n if (typeof t === 'string' && STRONG_CONTEXT_TYPES.has(t)) return true\n }\n if (this.key && STRONG_CONTEXT_KEYS.has(this.key)) return true\n return false\n }\n}\n","import { Base64Recognizer } from './base64_recognizer'\nimport { MediaTypeContext } from './media_type_context'\n\nconst STRONG_CONTEXT_MIN_LENGTH = 64\nconst WEAK_CONTEXT_MIN_LENGTH = 1024\n\nexport class BinaryContentRedactor {\n private visited: WeakSet<object> = new WeakSet()\n\n constructor(private readonly recognizer: Base64Recognizer = new Base64Recognizer()) {}\n\n redact<T>(value: T, mediaType?: string): T\n redact(value: unknown, mediaType?: string): unknown {\n this.visited = new WeakSet()\n return this.walk(value, mediaType ? new MediaTypeContext(undefined, undefined, mediaType) : MediaTypeContext.EMPTY)\n }\n\n private walk(value: unknown, ctx: MediaTypeContext): unknown {\n if (value === null || value === undefined) return value\n if (typeof value === 'string') return this.redactString(value, ctx)\n if (typeof value !== 'object') return value\n\n // Buffer extends Uint8Array, so this branch catches both.\n if (typeof Uint8Array !== 'undefined' && value instanceof Uint8Array) {\n return this.placeholderFor(ctx.inferMediaType())\n }\n\n if (this.visited.has(value)) return null\n this.visited.add(value)\n\n if (Array.isArray(value)) {\n return value.map((item) => this.walk(item, ctx))\n }\n\n const obj = value as Record<string, unknown>\n const out: Record<string, unknown> = {}\n for (const k of Object.keys(obj)) {\n out[k] = this.walk(obj[k], new MediaTypeContext(obj, k))\n }\n return out\n }\n\n private redactString(value: string, ctx: MediaTypeContext): string {\n const hasExplicitBinaryMediaType = ctx.hasExplicitBinaryMediaType()\n const recognitionValue = hasExplicitBinaryMediaType ? value.replace(/[\\r\\n]/g, '') : value\n const minLength = hasExplicitBinaryMediaType\n ? Math.min(recognitionValue.length, STRONG_CONTEXT_MIN_LENGTH)\n : ctx.signalsBinary()\n ? STRONG_CONTEXT_MIN_LENGTH\n : WEAK_CONTEXT_MIN_LENGTH\n const recognition = this.recognizer.recognize(recognitionValue, minLength)\n switch (recognition.kind) {\n case 'data-url':\n return this.placeholderFor(recognition.mediaType)\n case 'raw':\n return this.placeholderFor(ctx.inferMediaType())\n case 'none':\n return value\n }\n }\n\n private placeholderFor(mediaType?: string): string {\n if (!mediaType) return '[base64 redacted]'\n if (mediaType === 'application/octet-stream') return '[base64 file redacted]'\n return `[base64 ${mediaType} redacted]`\n }\n}\n","import type { Attributes } from '@opentelemetry/api'\nimport type { ReadableSpan, TimedEvent } from '@opentelemetry/sdk-trace-base'\n\nconst AI_SPAN_PREFIXES = ['gen_ai.', 'llm.', 'ai.', 'traceloop.'] as const\n\n/**\n * Returns `true` when the span is AI-related — its name or any attribute\n * key starts with `gen_ai.`, `llm.`, `ai.`, or `traceloop.`.\n */\nexport function isAISpan(span: ReadableSpan): boolean {\n if (AI_SPAN_PREFIXES.some((prefix) => span.name.startsWith(prefix))) {\n return true\n }\n const attributes = span.attributes\n if (attributes) {\n return Object.keys(attributes).some((key) => AI_SPAN_PREFIXES.some((prefix) => key.startsWith(prefix)))\n }\n return false\n}\n\n/** Copies rather than mutates: the span is shared across every registered processor/exporter. */\nexport function withAttributes(span: ReadableSpan, attributes: Attributes, events?: TimedEvent[]): ReadableSpan {\n return Object.create(span, {\n attributes: { value: attributes, enumerable: true, configurable: true },\n ...(events ? { events: { value: events, enumerable: true, configurable: true } } : {}),\n })\n}\n","import type { Attributes, AttributeValue } from '@opentelemetry/api'\nimport type { ReadableSpan, TimedEvent } from '@opentelemetry/sdk-trace-base'\n\nimport { BinaryContentRedactor } from '../sanitization/binary_content_redactor'\nimport { withAttributes } from './spans'\n\n// Deliberately always redacts here — the OTLP export path has no per-client passthrough gate.\nconst redactor = new BinaryContentRedactor()\n\nexport function redactSpan(span: ReadableSpan): ReadableSpan {\n const attributes = span.attributes ? redactAttributes(span.attributes) : span.attributes\n const events = span.events ? redactEvents(span.events) : span.events\n if (attributes === span.attributes && events === span.events) {\n return span\n }\n return withAttributes(span, attributes, events)\n}\n\nfunction redactAttributes(attributes: Attributes): Attributes {\n let changed = false\n const out: Attributes = {}\n for (const key of Object.keys(attributes)) {\n const value = attributes[key]\n const redacted = value === undefined ? value : redactAttributeValue(value)\n if (redacted !== value) {\n changed = true\n }\n out[key] = redacted\n }\n return changed ? out : attributes\n}\n\nfunction redactEvents(events: TimedEvent[]): TimedEvent[] {\n let changed = false\n const out = events.map((event) => {\n if (!event.attributes) {\n return event\n }\n const attributes = redactAttributes(event.attributes)\n if (attributes === event.attributes) {\n return event\n }\n changed = true\n return { ...event, attributes }\n })\n return changed ? out : events\n}\n\nfunction redactAttributeValue(value: AttributeValue): AttributeValue {\n if (typeof value === 'string') {\n return redactString(value)\n }\n if (isStringArray(value)) {\n let changed = false\n const out = value.map((item) => {\n if (typeof item !== 'string') {\n return item\n }\n const redacted = redactString(item)\n if (redacted !== item) {\n changed = true\n }\n return redacted\n })\n return changed ? out : value\n }\n return value\n}\n\nfunction isStringArray(value: AttributeValue): value is Array<string | null | undefined> {\n return Array.isArray(value) && value.every((item) => typeof item !== 'number' && typeof item !== 'boolean')\n}\n\nfunction redactString(value: string): string {\n const trimmed = value.trimStart()\n if (trimmed.startsWith('{') || trimmed.startsWith('[')) {\n const redacted = redactJson(value)\n if (redacted !== undefined) {\n return redacted\n }\n }\n return redactor.redact(value)\n}\n\nfunction redactJson(value: string): string | undefined {\n let parsed: unknown\n try {\n parsed = JSON.parse(value)\n } catch {\n return undefined\n }\n if (parsed === null || typeof parsed !== 'object') {\n return undefined\n }\n const redactedStr = JSON.stringify(redactor.redact(parsed))\n // Preserve the original string (and its formatting) when nothing was redacted.\n return redactedStr === JSON.stringify(parsed) ? value : redactedStr\n}\n","// Warn when a wrapper's base_url points at the PostHog AI Gateway: the gateway\n// emits its own $ai_generation, so each call would be captured (and, for billable\n// products, billed) twice. We only warn — the wrapper's event carries data the\n// gateway never sees (groups, custom properties, trace hierarchy).\n\n// Keep in sync with the gateway's deployed hosts (see services/llm-gateway in the\n// main repo). gateway.us.posthog.com is live today; the rest are listed ahead of\n// any traffic moving to them.\nexport const POSTHOG_AI_GATEWAY_HOSTS: readonly string[] = [\n 'gateway.posthog.com',\n 'gateway.us.posthog.com',\n 'gateway.eu.posthog.com',\n 'ai-gateway.us.posthog.com',\n 'ai-gateway.eu.posthog.com',\n]\n\n// Swap for the dedicated AI Gateway page once it ships.\nconst GATEWAY_DOCS_URL = 'https://posthog.com/docs/ai-observability'\n\nconst extractHost = (baseURL: string): string | undefined => {\n try {\n // Tolerate bare hosts that omit a scheme, e.g. \"gateway.us.posthog.com/v1\".\n const hasScheme = /^[a-z][a-z0-9+.-]*:\\/\\//i.test(baseURL)\n return new URL(hasScheme ? baseURL : `https://${baseURL}`).hostname.toLowerCase()\n } catch {\n return undefined\n }\n}\n\nexport const isPostHogAiGatewayUrl = (baseURL: string | undefined | null): boolean => {\n if (!baseURL) {\n return false\n }\n const host = extractHost(baseURL)\n return host !== undefined && POSTHOG_AI_GATEWAY_HOSTS.includes(host)\n}\n\n// Warns on every gateway call by design: the misconfiguration is impossible to\n// miss that way, and a doubled bill is worse than noisy logs.\nexport const warnIfPostHogAiGateway = (baseURL: string | undefined | null): void => {\n if (!isPostHogAiGatewayUrl(baseURL)) {\n return\n }\n console.warn(\n '[PostHog] The PostHog AI wrapper is pointed at the PostHog AI Gateway. ' +\n 'Both capture $ai_generation, so every call is double-counted and double-billed. ' +\n `Use one or the other — see ${GATEWAY_DOCS_URL}.`\n )\n}\n\n// OTel spans don't pass through captureAiGeneration, so detect the gateway from\n// the span's host/URL attributes instead. These follow the GenAI / HTTP semantic\n// conventions: `server.address` is a bare host, `url.full` a full URL, both of\n// which isPostHogAiGatewayUrl accepts.\nconst OTEL_GATEWAY_URL_ATTRIBUTES = ['server.address', 'url.full'] as const\n\nexport const warnIfPostHogAiGatewayOtelAttributes = (attributes: Record<string, unknown> | undefined): void => {\n if (!attributes) {\n return\n }\n for (const key of OTEL_GATEWAY_URL_ATTRIBUTES) {\n const value = attributes[key]\n if (typeof value === 'string' && isPostHogAiGatewayUrl(value)) {\n warnIfPostHogAiGateway(value)\n return\n }\n }\n}\n","import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http'\nimport type { ReadableSpan } from '@opentelemetry/sdk-trace-base'\n\nimport { redactSpan } from './redact'\nimport { isAISpan } from './spans'\nimport { warnIfPostHogAiGatewayOtelAttributes } from '../gatewayWarning'\n\nconst DEFAULT_OTEL_HOST = 'https://us.i.posthog.com'\n// OpenTelemetry's ExportResultCode.SUCCESS is 0. Keep this local so loading the\n// published subpath does not require the exporter package's transitive dependencies.\nconst EXPORT_SUCCESS = 0\n\nfunction normalizeToken(value?: unknown): string {\n return typeof value === 'string' ? value.trim() : ''\n}\n\nfunction normalizeHost(value?: unknown): string {\n const normalizedValue = typeof value === 'string' ? value.trim() : ''\n return normalizedValue || DEFAULT_OTEL_HOST\n}\n\n/**\n * Options for the PostHogTraceExporter. `projectToken` is required; a blank token disables the\n * exporter as a defensive no-op. You can also optionally override the `host` URL. `host` defaults to `https://us.i.posthog.com`.\n *\n * @example\n * ```ts\n * import { PostHogTraceExporter } from '@posthog/ai/otel'\n *\n * new PostHogTraceExporter({ projectToken: 'phc_...' })\n * ```\n *\n * @example\n * ```ts\n * import { PostHogTraceExporter } from '@posthog/ai/otel'\n *\n * new PostHogTraceExporter({ projectToken: 'phc_...', host: 'https://eu.i.posthog.com' })\n * ```\n */\nexport type PostHogTraceExporterOptions = { projectToken: string; host?: string }\n\n/**\n * An OpenTelemetry `TraceExporter` that sends AI traces to PostHog's OTLP\n * ingestion endpoint. PostHog converts `gen_ai.*` spans into\n * `$ai_generation` events server-side.\n *\n * Only AI-related spans (those whose name or attribute keys start with\n * `gen_ai.`, `llm.`, `ai.`, or `traceloop.`) are exported; all other\n * spans are silently dropped.\n *\n * Use this when the API you're integrating with only accepts a\n * `TraceExporter` (e.g. Vercel's `registerOTel`) or when you need to\n * plug PostHog into an existing processor chain. Otherwise prefer\n * {@link PostHogSpanProcessor}, which is self-contained.\n *\n * `projectToken` is required; a blank token disables the exporter as a defensive no-op.\n * You can also optionally override the `host` URL.\n *\n * Exporters only see finished spans, so they can't add per-request PostHog context;\n * use {@link PostHogSpanProcessor} with `client` for that.\n *\n * @example\n * ```ts\n * import { PostHogTraceExporter } from '@posthog/ai/otel'\n * import { registerOTel } from '@vercel/otel'\n *\n * registerOTel({\n * serviceName: 'my-app',\n * traceExporter: new PostHogTraceExporter({ projectToken: 'phc_...' }),\n * })\n * ```\n */\nexport class PostHogTraceExporter extends OTLPTraceExporter {\n private readonly disabled: boolean\n\n constructor(options: PostHogTraceExporterOptions) {\n const token = normalizeToken(options.projectToken)\n const disabled = !token\n const host = token ? new URL(normalizeHost(options.host)).origin : DEFAULT_OTEL_HOST\n super({\n url: `${host}/i/v0/ai/otel`,\n headers: token\n ? {\n Authorization: `Bearer ${token}`,\n }\n : {},\n })\n\n this.disabled = disabled\n if (this.disabled) {\n console.warn('[PostHogTraceExporter] projectToken is missing or blank; the exporter will be disabled.')\n }\n }\n\n override export(spans: ReadableSpan[], resultCallback: (result: { code: number; error?: Error }) => void): void {\n if (this.disabled) {\n // Intentionally report success: missing or blank tokens disable exporting as a compatibility no-op.\n // Reporting failure would make OpenTelemetry treat every span as an export error.\n resultCallback({ code: EXPORT_SUCCESS })\n return\n }\n\n const aiSpans = spans.filter(isAISpan)\n if (aiSpans.length === 0) {\n resultCallback({ code: EXPORT_SUCCESS })\n return\n }\n for (const span of aiSpans) {\n warnIfPostHogAiGatewayOtelAttributes(span.attributes)\n }\n super.export(aiSpans.map(redactSpan), resultCallback)\n }\n}\n","import type { AttributeValue, Attributes } from '@opentelemetry/api'\nimport type { ReadableSpan } from '@opentelemetry/sdk-trace-base'\n\nimport { withAttributes } from './spans'\n\nexport type CapturedContext = {\n distinctId?: string\n sessionId?: string\n properties?: Record<string, unknown>\n}\n\n// Structural so declarations type-check against posthog-node versions that have no getContext.\nexport type PostHogContextSource = {\n getContext?(): CapturedContext | undefined\n}\n\nconst DISTINCT_ID_ATTRIBUTE = 'posthog.distinct_id'\nconst SESSION_ID_ATTRIBUTE = '$session_id'\n\n// Ingestion prefers posthog.distinct_id, so adding it would override the user a span already names.\nconst IDENTITY_ATTRIBUTES = [\n DISTINCT_ID_ATTRIBUTE,\n 'user.id',\n 'ai.telemetry.metadata.posthog_distinct_id',\n 'ai.settings.context.posthog_distinct_id',\n]\n\ntype Primitive = string | number | boolean\n\nfunction isPrimitive(value: unknown): value is Primitive {\n return (\n typeof value === 'string' || typeof value === 'boolean' || (typeof value === 'number' && Number.isFinite(value))\n )\n}\n\nfunction isHomogeneousPrimitiveArray(value: unknown): value is string[] | number[] | boolean[] {\n return Array.isArray(value) && value.every((item) => isPrimitive(item) && typeof item === typeof value[0])\n}\n\nfunction rejectNonFinite(_key: string, value: unknown): unknown {\n if (typeof value === 'number' && !Number.isFinite(value)) {\n throw new TypeError('non-finite number')\n }\n return value\n}\n\nfunction toAttributeValue(value: unknown): AttributeValue | undefined {\n if (value === undefined || value === null || value === '') {\n return undefined\n }\n if (isPrimitive(value) || isHomogeneousPrimitiveArray(value)) {\n return value\n }\n try {\n const json = JSON.stringify(value, rejectNonFinite)\n if (json === undefined || json === 'null') {\n return undefined\n }\n return json.startsWith('\"') ? String(JSON.parse(json)) || undefined : json\n } catch {\n return undefined\n }\n}\n\nfunction hasValues(context: CapturedContext): boolean {\n return Boolean(context.distinctId || context.sessionId || Object.keys(context.properties ?? {}).length > 0)\n}\n\nexport function captureContext(client: PostHogContextSource): CapturedContext | undefined {\n try {\n // posthog-node versions before getContext existed are still within the peer range.\n if (typeof client.getContext !== 'function') {\n return undefined\n }\n const context = client.getContext()\n return context && hasValues(context) ? context : undefined\n } catch {\n // Capturing is best effort and must never break the instrumented call.\n return undefined\n }\n}\n\nfunction toAttributes(context: CapturedContext, spanAttributes: Attributes): Attributes {\n const hasIdentity = IDENTITY_ATTRIBUTES.some((key) => spanAttributes[key] !== undefined)\n const values: Record<string, unknown> = {\n ...(context.distinctId && !hasIdentity ? { [DISTINCT_ID_ATTRIBUTE]: context.distinctId } : {}),\n ...(context.sessionId ? { [SESSION_ID_ATTRIBUTE]: context.sessionId } : {}),\n ...context.properties,\n }\n const attributes: Attributes = {}\n for (const [key, value] of Object.entries(values)) {\n if (hasIdentity && key === DISTINCT_ID_ATTRIBUTE) {\n continue\n }\n const attribute = toAttributeValue(value)\n if (attribute !== undefined) {\n attributes[key] = attribute\n }\n }\n return attributes\n}\n\nexport function withRequestContext(span: ReadableSpan, context: CapturedContext): ReadableSpan {\n try {\n const attributes = toAttributes(context, span.attributes)\n if (Object.keys(attributes).length === 0) {\n return span\n }\n return withAttributes(span, { ...attributes, ...span.attributes })\n } catch {\n // Conversion runs inside span.end() and must never break the instrumented call.\n return span\n }\n}\n","import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http'\nimport type { Context } from '@opentelemetry/api'\nimport { BatchSpanProcessor, type SpanProcessor, type ReadableSpan, type Span } from '@opentelemetry/sdk-trace-base'\n\nimport { redactSpan } from './redact'\nimport { captureContext, withRequestContext, type CapturedContext, type PostHogContextSource } from './requestContext'\nimport { isAISpan } from './spans'\nimport { warnIfPostHogAiGatewayOtelAttributes } from '../gatewayWarning'\n\nconst DEFAULT_OTEL_HOST = 'https://us.i.posthog.com'\n\nfunction normalizeToken(value?: unknown): string {\n return typeof value === 'string' ? value.trim() : ''\n}\n\nfunction normalizeHost(value?: unknown): string {\n const normalizedValue = typeof value === 'string' ? value.trim() : ''\n return normalizedValue || DEFAULT_OTEL_HOST\n}\n\nexport interface PostHogSpanProcessorOptions {\n /**\n * Your PostHog project token (the `phc_...` key). Required; a blank token disables the\n * processor as a defensive no-op.\n */\n projectToken: string\n\n /**\n * PostHog host URL. Defaults to `https://us.i.posthog.com`.\n */\n host?: string\n\n /**\n * Your posthog-node client. When set, each AI span this processor exports gets the distinct ID,\n * session ID (as `$session_id`) and properties from the context entered with `client.withContext()` or\n * `client.enterContext()`. Other processors on the provider don't see them.\n * Attributes already on the span win over properties, which win over the context's IDs.\n * The context is captured when a span starts, but its properties are read when the span ends,\n * so mutating a context object while a span is open shows up in the export.\n * Must be the same posthog-node instance you call `withContext` or `enterContext` on.\n */\n client?: PostHogContextSource\n\n /**\n * @internal Injected processor for testing — bypasses exporter creation.\n */\n _spanProcessor?: SpanProcessor\n}\n\nclass NoopSpanProcessor implements SpanProcessor {\n onStart(_span: Span, _parentContext: Context): void {\n return\n }\n onEnd(_span: ReadableSpan): void {\n return\n }\n shutdown(): Promise<void> {\n return Promise.resolve()\n }\n forceFlush(): Promise<void> {\n return Promise.resolve()\n }\n}\n\n/**\n * An OpenTelemetry `SpanProcessor` that sends AI traces to PostHog.\n *\n * `projectToken` is required; a blank token disables the processor as a defensive no-op.\n *\n * Internally batches spans and exports them to PostHog's OTLP ingestion\n * endpoint. Only AI-related spans (those whose name or attribute keys\n * start with `gen_ai.`, `llm.`, `ai.`, or `traceloop.`) are exported;\n * all other spans are silently dropped.\n *\n * Request-scoped and serverless runtimes should keep a reference to this\n * processor and await {@link PostHogSpanProcessor.forceFlush} before the request\n * lifecycle ends. This waits for queued exports without sending one request per span.\n *\n * This is the recommended integration point when your setup accepts a\n * `SpanProcessor`. If you need a `TraceExporter` instead (e.g. for\n * Vercel's `registerOTel`), use {@link PostHogTraceExporter}.\n *\n * @example\n * ```ts\n * import { PostHogSpanProcessor } from '@posthog/ai/otel'\n * import { NodeSDK } from '@opentelemetry/sdk-node'\n *\n * const processor = new PostHogSpanProcessor({ projectToken: 'phc_...' })\n * const sdk = new NodeSDK({ spanProcessors: [processor] })\n * sdk.start()\n *\n * // In request-scoped runtimes, wait for queued exports before returning.\n * await processor.forceFlush()\n * ```\n *\n * @example\n * ```ts\n * // Per request: pass your posthog-node client, then wrap the work in a context.\n * const processor = new PostHogSpanProcessor({ projectToken: 'phc_...', client: posthog })\n * await posthog.withContext({ distinctId: user.id, properties: { plan: 'pro' } }, () => handleRequest())\n * ```\n */\nexport class PostHogSpanProcessor implements SpanProcessor {\n private readonly inner: SpanProcessor\n private readonly client?: PostHogContextSource\n private readonly capturedContexts = new WeakMap<ReadableSpan, CapturedContext>()\n\n constructor(options: PostHogSpanProcessorOptions) {\n const token = normalizeToken(options.projectToken)\n if (!token) {\n console.warn('[PostHogSpanProcessor] projectToken is missing or blank; the processor will be disabled.')\n this.inner = new NoopSpanProcessor()\n return\n }\n this.client = options.client\n\n if (options._spanProcessor) {\n this.inner = options._spanProcessor\n } else {\n const host = new URL(normalizeHost(options.host)).origin\n const exporter = new OTLPTraceExporter({\n url: `${host}/i/v0/ai/otel`,\n headers: {\n Authorization: `Bearer ${token}`,\n },\n })\n this.inner = new BatchSpanProcessor(exporter)\n }\n }\n\n onStart(span: Span, parentContext: Context): void {\n const captured = this.client && captureContext(this.client)\n if (captured) {\n this.capturedContexts.set(span, captured)\n }\n // Forwarded unconditionally — filtering happens in onEnd. We can't filter\n // here because the span hasn't finished yet and may not have AI attributes\n // set. BatchSpanProcessor.onStart is a no-op so this is safe.\n this.inner.onStart(span, parentContext)\n }\n\n onEnd(span: ReadableSpan): void {\n const captured = this.capturedContexts.get(span)\n this.capturedContexts.delete(span)\n if (isAISpan(span)) {\n warnIfPostHogAiGatewayOtelAttributes(span.attributes)\n this.inner.onEnd(redactSpan(captured ? withRequestContext(span, captured) : span))\n }\n }\n\n shutdown(): Promise<void> {\n return this.inner.shutdown()\n }\n\n forceFlush(): Promise<void> {\n return this.inner.forceFlush()\n }\n}\n"],"mappings":"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"}
@@ -391,7 +391,7 @@ function sanitizeValues(obj) {
391
391
  }
392
392
  //#endregion
393
393
  //#region package.json
394
- var version = "8.13.7";
394
+ var version = "8.14.0";
395
395
  //#endregion
396
396
  //#region src/serializeError.ts
397
397
  const DEFAULT_MAX_DEPTH = 3;
@@ -390,7 +390,7 @@ function sanitizeValues(obj) {
390
390
  }
391
391
  //#endregion
392
392
  //#region package.json
393
- var version = "8.13.7";
393
+ var version = "8.14.0";
394
394
  //#endregion
395
395
  //#region src/serializeError.ts
396
396
  const DEFAULT_MAX_DEPTH = 3;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@posthog/ai",
3
- "version": "8.13.7",
3
+ "version": "8.14.0",
4
4
  "description": "PostHog Node.js AI integrations",
5
5
  "repository": {
6
6
  "type": "git",