@posthog/ai
Version:
PostHog Node.js AI integrations
155 lines (148 loc) • 4.97 kB
JavaScript
;
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