UNPKG

@posthog/ai

Version:
155 lines (148 loc) 4.97 kB
'use strict'; var exporterTraceOtlpHttp = require('@opentelemetry/exporter-trace-otlp-http'); var core = require('@opentelemetry/core'); var sdkTraceBase = require('@opentelemetry/sdk-trace-base'); const AI_SPAN_PREFIXES = ['gen_ai.', 'llm.', 'ai.', 'traceloop.']; /** * Returns `true` when the span is AI-related — its name or any attribute * key starts with `gen_ai.`, `llm.`, `ai.`, or `traceloop.`. */ function isAISpan(span) { if (AI_SPAN_PREFIXES.some(prefix => span.name.startsWith(prefix))) { return true; } const attributes = span.attributes; if (attributes) { return Object.keys(attributes).some(key => AI_SPAN_PREFIXES.some(prefix => key.startsWith(prefix))); } return false; } const DEFAULT_OTEL_HOST$1 = 'https://us.i.posthog.com'; function normalizeApiKey$1(value) { return typeof value === 'string' ? value.trim() : ''; } function normalizeHost$1(value) { const normalizedValue = typeof value === 'string' ? value.trim() : ''; return normalizedValue || DEFAULT_OTEL_HOST$1; } /** * An OpenTelemetry `TraceExporter` that sends AI traces to PostHog's OTLP * ingestion endpoint. PostHog converts `gen_ai.*` spans into * `$ai_generation` events server-side. * * Only AI-related spans (those whose name or attribute keys start with * `gen_ai.`, `llm.`, `ai.`, or `traceloop.`) are exported; all other * spans are silently dropped. * * Use this when the API you're integrating with only accepts a * `TraceExporter` (e.g. Vercel's `registerOTel`) or when you need to * plug PostHog into an existing processor chain. Otherwise prefer * {@link PostHogSpanProcessor}, which is self-contained. * * @example * ```ts * import { PostHogTraceExporter } from '@posthog/ai/otel' * import { registerOTel } from '@vercel/otel' * * registerOTel({ * serviceName: 'my-app', * traceExporter: new PostHogTraceExporter({ apiKey: 'phc_...' }), * }) * ``` */ class PostHogTraceExporter extends exporterTraceOtlpHttp.OTLPTraceExporter { constructor(options) { const apiKey = normalizeApiKey$1(options.apiKey); if (!apiKey) { throw new Error('PostHogTraceExporter requires an apiKey'); } const host = new URL(normalizeHost$1(options.host)).origin; super({ url: `${host}/i/v0/ai/otel`, headers: { Authorization: `Bearer ${apiKey}` } }); } export(spans, resultCallback) { const aiSpans = spans.filter(isAISpan); if (aiSpans.length === 0) { resultCallback({ code: core.ExportResultCode.SUCCESS }); return; } super.export(aiSpans, resultCallback); } } const DEFAULT_OTEL_HOST = 'https://us.i.posthog.com'; function normalizeApiKey(value) { return typeof value === 'string' ? value.trim() : ''; } function normalizeHost(value) { const normalizedValue = typeof value === 'string' ? value.trim() : ''; return normalizedValue || DEFAULT_OTEL_HOST; } /** * An OpenTelemetry `SpanProcessor` that sends AI traces to PostHog. * * Internally batches spans and exports them to PostHog's OTLP ingestion * endpoint. Only AI-related spans (those whose name or attribute keys * start with `gen_ai.`, `llm.`, `ai.`, or `traceloop.`) are exported; * all other spans are silently dropped. * * This is the recommended integration point when your setup accepts a * `SpanProcessor`. If you need a `TraceExporter` instead (e.g. for * Vercel's `registerOTel`), use {@link PostHogTraceExporter}. * * @example * ```ts * import { PostHogSpanProcessor } from '@posthog/ai/otel' * import { NodeSDK } from '@opentelemetry/sdk-node' * * const sdk = new NodeSDK({ * spanProcessors: [new PostHogSpanProcessor({ apiKey: 'phc_...' })], * }) * sdk.start() * ``` */ class PostHogSpanProcessor { constructor(options) { const apiKey = normalizeApiKey(options.apiKey); if (!apiKey) { throw new Error('PostHogSpanProcessor requires an apiKey'); } if (options._spanProcessor) { this.inner = options._spanProcessor; } else { const host = new URL(normalizeHost(options.host)).origin; const exporter = new exporterTraceOtlpHttp.OTLPTraceExporter({ url: `${host}/i/v0/ai/otel`, headers: { Authorization: `Bearer ${apiKey}` } }); this.inner = new sdkTraceBase.BatchSpanProcessor(exporter); } } onStart(span, parentContext) { // Forwarded unconditionally — filtering happens in onEnd. We can't filter // here because the span hasn't finished yet and may not have AI attributes // set. BatchSpanProcessor.onStart is a no-op so this is safe. this.inner.onStart(span, parentContext); } onEnd(span) { if (isAISpan(span)) { this.inner.onEnd(span); } } shutdown() { return this.inner.shutdown(); } forceFlush() { return this.inner.forceFlush(); } } exports.PostHogSpanProcessor = PostHogSpanProcessor; exports.PostHogTraceExporter = PostHogTraceExporter; //# sourceMappingURL=index.cjs.map