UNPKG

@posthog/ai

Version:
1 lines 10.2 kB
{"version":3,"file":"index.mjs","sources":["../../src/otel/spans.ts","../../src/otel/exporter.ts","../../src/otel/processor.ts"],"sourcesContent":["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","import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http'\nimport type { ReadableSpan } from '@opentelemetry/sdk-trace-base'\nimport { ExportResultCode } from '@opentelemetry/core'\n\nimport { isAISpan } from './spans'\n\nconst DEFAULT_OTEL_HOST = 'https://us.i.posthog.com'\n\nfunction normalizeApiKey(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 PostHogTraceExporterOptions {\n /**\n * Your PostHog project API key.\n */\n apiKey: string\n\n /**\n * PostHog host URL. Defaults to `https://us.i.posthog.com`.\n */\n host?: string\n}\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 * @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({ apiKey: 'phc_...' }),\n * })\n * ```\n */\nexport class PostHogTraceExporter extends OTLPTraceExporter {\n constructor(options: PostHogTraceExporterOptions) {\n const apiKey = normalizeApiKey(options.apiKey)\n if (!apiKey) {\n throw new Error('PostHogTraceExporter requires an apiKey')\n }\n const host = new URL(normalizeHost(options.host)).origin\n super({\n url: `${host}/i/v0/ai/otel`,\n headers: {\n Authorization: `Bearer ${apiKey}`,\n },\n })\n }\n\n override export(spans: ReadableSpan[], resultCallback: (result: { code: number; error?: Error }) => void): void {\n const aiSpans = spans.filter(isAISpan)\n if (aiSpans.length === 0) {\n resultCallback({ code: ExportResultCode.SUCCESS })\n return\n }\n super.export(aiSpans, 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 { isAISpan } from './spans'\n\nconst DEFAULT_OTEL_HOST = 'https://us.i.posthog.com'\n\nfunction normalizeApiKey(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 API key.\n */\n apiKey: 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\n/**\n * An OpenTelemetry `SpanProcessor` that sends AI traces to PostHog.\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 * 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 sdk = new NodeSDK({\n * spanProcessors: [new PostHogSpanProcessor({ apiKey: 'phc_...' })],\n * })\n * sdk.start()\n * ```\n */\nexport class PostHogSpanProcessor implements SpanProcessor {\n private readonly inner: SpanProcessor\n\n constructor(options: PostHogSpanProcessorOptions) {\n const apiKey = normalizeApiKey(options.apiKey)\n if (!apiKey) {\n throw new Error('PostHogSpanProcessor requires an apiKey')\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 ${apiKey}`,\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 this.inner.onEnd(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"],"names":["AI_SPAN_PREFIXES","isAISpan","span","some","prefix","name","startsWith","attributes","Object","keys","key","DEFAULT_OTEL_HOST","normalizeApiKey","value","trim","normalizeHost","normalizedValue","PostHogTraceExporter","OTLPTraceExporter","constructor","options","apiKey","Error","host","URL","origin","url","headers","Authorization","export","spans","resultCallback","aiSpans","filter","length","code","ExportResultCode","SUCCESS","PostHogSpanProcessor","_spanProcessor","inner","exporter","BatchSpanProcessor","onStart","parentContext","onEnd","shutdown","forceFlush"],"mappings":"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"}