-
Notifications
You must be signed in to change notification settings - Fork 344
feat(aio): public beta captureAi with dedicated AI capture lane #4289
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| --- | ||
| '@posthog/core': minor | ||
| 'posthog-node': minor | ||
| '@posthog/ai': minor | ||
| --- | ||
|
|
||
| Public beta `captureAi()` / `captureAiImmediate()`: AI events on a dedicated isolated endpoint with the event UUID returned. New `enableFullAiCapture` option replaces the internal `_useAiLane` / `_enableMultimodalCapture`; wrappers route through the AI endpoint and skip redaction/truncation when set (privacy mode still wins). |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| import type { EventMessage } from 'posthog-node' | ||
|
|
||
| /** @internal */ | ||
| export type FullAiCaptureGate = { | ||
| readonly enableFullAiCapture?: boolean | ||
| } | ||
|
|
||
| /** @internal */ | ||
| export interface AiLaneCapableClient extends FullAiCaptureGate { | ||
| capture(props: EventMessage): void | ||
| captureImmediate(props: EventMessage): Promise<void> | ||
| captureAi?(props: EventMessage): string | undefined | ||
| captureAiImmediate?(props: EventMessage): Promise<string | undefined> | ||
| } | ||
|
|
||
| /** @internal */ | ||
| export function isFullAiCaptureEnabled(client?: FullAiCaptureGate): boolean { | ||
| return client?.enableFullAiCapture === true | ||
| } | ||
|
|
||
| /** @internal */ | ||
| export function captureAiEvent(client: AiLaneCapableClient, event: EventMessage): void { | ||
| if (isFullAiCaptureEnabled(client) && typeof client.captureAi === 'function') { | ||
| client.captureAi(event) | ||
| return | ||
| } | ||
| client.capture(event) | ||
| } | ||
|
|
||
| /** @internal */ | ||
| export async function captureAiEventImmediate(client: AiLaneCapableClient, event: EventMessage): Promise<void> { | ||
| if (isFullAiCaptureEnabled(client) && typeof client.captureAiImmediate === 'function') { | ||
| await client.captureAiImmediate(event) | ||
| return | ||
| } | ||
| await client.captureImmediate(event) | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -13,6 +13,7 @@ import { sanitizeLangChain } from '../sanitization' | |
| import { stringifyError } from '../serializeError' | ||
| import { warnIfPostHogAiGateway } from '../gatewayWarning' | ||
| import { isObject } from '../typeGuards' | ||
| import { captureAiEvent } from '../captureAiEvent' | ||
|
|
||
| // Mirror LangGraph's isGraphBubbleUp guard without adding LangGraph as a dependency. Every | ||
| // LangGraph control-flow exception (GraphInterrupt, NodeInterrupt, ParentCommand, GraphDrained, | ||
|
|
@@ -338,7 +339,7 @@ export class LangChainCallbackHandler extends BaseCallbackHandler { | |
| const runNameFound = this._getLangchainRunName(serialized, { extraParams, runName }) || 'generation' | ||
| const generation: GenerationMetadata = { | ||
| name: runNameFound, | ||
| input: sanitizeLangChain(messages), | ||
| input: sanitizeLangChain(messages, this.client), | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. blocking: Apply the gate to trace and span state This updates generation input, but
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed |
||
| startTime: Date.now(), | ||
| } | ||
| if (extraParams) { | ||
|
|
@@ -388,7 +389,7 @@ export class LangChainCallbackHandler extends BaseCallbackHandler { | |
|
|
||
| private _safeCapture(message: EventMessage): void { | ||
| try { | ||
| this.client.capture(message) | ||
| captureAiEvent(this.client, message) | ||
| } catch { | ||
| // Telemetry delivery must never affect the LangChain callback lifecycle. | ||
| } | ||
|
|
@@ -426,7 +427,7 @@ export class LangChainCallbackHandler extends BaseCallbackHandler { | |
| $ai_lib: 'posthog-ai', | ||
| $ai_lib_version: version, | ||
| $ai_trace_id: traceId, | ||
| $ai_input_state: withPrivacyMode(this.client, this.privacyMode, sanitizeLangChain(run.input)), | ||
| $ai_input_state: withPrivacyMode(this.client, this.privacyMode, sanitizeLangChain(run.input, this.client)), | ||
| $ai_latency: latency, | ||
| $ai_span_name: run.name, | ||
| $ai_span_id: runId, | ||
|
|
@@ -450,15 +451,19 @@ export class LangChainCallbackHandler extends BaseCallbackHandler { | |
| eventProperties['$ai_output_state'] = withPrivacyMode( | ||
| this.client, | ||
| this.privacyMode, | ||
| sanitizeLangChain({ __interrupt__: interrupts }) | ||
| sanitizeLangChain({ __interrupt__: interrupts }, this.client) | ||
| ) | ||
| } | ||
| } else { | ||
| eventProperties['$ai_error'] = stringifyError(outputs) | ||
| eventProperties['$ai_is_error'] = true | ||
| } | ||
| } else if (outputs !== undefined) { | ||
| eventProperties['$ai_output_state'] = withPrivacyMode(this.client, this.privacyMode, sanitizeLangChain(outputs)) | ||
| eventProperties['$ai_output_state'] = withPrivacyMode( | ||
| this.client, | ||
| this.privacyMode, | ||
| sanitizeLangChain(outputs, this.client) | ||
| ) | ||
| } | ||
| this._safeCapture({ | ||
| distinctId: this.distinctId ? this.distinctId.toString() : runId, | ||
|
|
@@ -685,7 +690,7 @@ export class LangChainCallbackHandler extends BaseCallbackHandler { | |
| } | ||
|
|
||
| // Sanitize the message content to redact base64 images | ||
| return sanitizeLangChain(messageDict) as Record<string, any> | ||
| return sanitizeLangChain(messageDict, this.client) as Record<string, any> | ||
| } | ||
|
|
||
| private _extractStopReason(output: LLMResult): string | undefined { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
blocking: Discard the dedicated queue during reset
This activates
ai_capture_queue, butdiscardClient()clears onlyqueueandai_queue. A later/resetcan therefore send an event that should have been discarded whenshutdown()drains this active route. Please clearai_capture_queuetoo and cover/capture_aifollowed by/reset.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed