Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 51 additions & 53 deletions examples/example-convex/convex/aiSdk/manualCapture.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
"use node"

import { generateText } from 'ai'
import { openai } from '@ai-sdk/openai'
import { action } from '../_generated/server'
Expand All @@ -9,55 +7,55 @@ import { posthog } from '../posthog.js'
// Demonstrates using the Vercel AI SDK (without @convex-dev/agent)
// to call an LLM and capture $ai_generation events to PostHog.
export const generate = action({
args: {
prompt: v.string(),
distinctId: v.optional(v.string()),
},
handler: async (ctx, args) => {
const traceId = crypto.randomUUID()
const startTime = Date.now()

const result = await generateText({
model: openai('gpt-5-mini'),
prompt: args.prompt,
})

const latency = (Date.now() - startTime) / 1000

await posthog.capture(ctx, {
distinctId: args.distinctId ?? 'anonymous',
event: '$ai_generation',
properties: {
// Trace ID groups multiple generations into a single trace
$ai_trace_id: traceId,

// Core identification
$ai_provider: 'openai',
$ai_model: 'gpt-5-mini',

// Token usage
$ai_input_tokens: result.usage.inputTokens,
$ai_output_tokens: result.usage.outputTokens,

// Cache tokens (if the provider reports them)
$ai_cache_read_input_tokens: result.usage.cachedInputTokens,

// Performance
$ai_latency: latency,

// Input/output content
$ai_input: [{ role: 'user', content: args.prompt }],
$ai_output_choices: [{ role: 'assistant', content: result.text }],

// Generation metadata — the AI SDK doesn't expose HTTP status directly,
// so we infer success/failure from the finish reason.
$ai_is_error: result.finishReason === 'error',
},
})

return {
text: result.text,
usage: result.usage,
}
},
args: {
prompt: v.string(),
distinctId: v.optional(v.string()),
},
handler: async (ctx, args) => {
const traceId = crypto.randomUUID()
const startTime = Date.now()

const result = await generateText({
model: openai('gpt-5-mini'),
prompt: args.prompt,
})

const latency = (Date.now() - startTime) / 1000

await posthog.capture(ctx, {
distinctId: args.distinctId ?? 'anonymous',
event: '$ai_generation',
properties: {
// Trace ID groups multiple generations into a single trace
$ai_trace_id: traceId,

// Core identification
$ai_provider: 'openai',
$ai_model: 'gpt-5-mini',

// Token usage
$ai_input_tokens: result.usage.inputTokens,
$ai_output_tokens: result.usage.outputTokens,

// Cache tokens (if the provider reports them)
$ai_cache_read_input_tokens: result.usage.cachedInputTokens,

// Performance
$ai_latency: latency,

// Input/output content
$ai_input: [{ role: 'user', content: args.prompt }],
$ai_output_choices: [{ role: 'assistant', content: result.text }],

// Generation metadata — the AI SDK doesn't expose HTTP status directly,
// so we infer success/failure from the finish reason.
$ai_is_error: result.finishReason === 'error',
},
})

return {
text: result.text,
usage: result.usage,
}
},
})
76 changes: 41 additions & 35 deletions examples/example-convex/convex/aiSdk/openTelemetry.ts
Original file line number Diff line number Diff line change
@@ -1,49 +1,55 @@
"use node"
// Convex runs in a V8 isolate without the `performance` global that
// @opentelemetry/core expects. This must be imported before any OTEL module.
import '../polyfills.js'

import { NodeSDK } from '@opentelemetry/sdk-node'
import { trace } from '@opentelemetry/api'
import { BasicTracerProvider, SimpleSpanProcessor } from '@opentelemetry/sdk-trace-base'
import { resourceFromAttributes } from '@opentelemetry/resources'
import { generateText } from 'ai'
import { openai } from '@ai-sdk/openai'
import { PostHogTraceExporter } from '@posthog/ai/otel'
import { action } from '../_generated/server'
import { v } from 'convex/values'

// PostHogTraceExporter is a standard OTEL SpanExporter — add it as a span
// processor alongside any other exporters in your OTEL setup.
const provider = new BasicTracerProvider({
resource: resourceFromAttributes({
'service.name': 'example-convex',
}),
spanProcessors: [
new SimpleSpanProcessor(
new PostHogTraceExporter({
apiKey: process.env.POSTHOG_API_KEY!,
host: process.env.POSTHOG_HOST,
})
),
],
})
trace.setGlobalTracerProvider(provider)

// Demonstrates using the Vercel AI SDK's experimental_telemetry with
// PostHog's PostHogTraceExporter to automatically capture $ai_generation events.
export const generate = action({
args: {
prompt: v.string(),
distinctId: v.optional(v.string()),
},
handler: async (_ctx, args) => {
const distinctId = args.distinctId ?? 'anonymous'

const sdk = new NodeSDK({
resource: resourceFromAttributes({
'service.name': 'example-convex',
'user.id': distinctId,
}),
traceExporter: new PostHogTraceExporter({
apiKey: process.env.POSTHOG_API_KEY!,
host: process.env.POSTHOG_HOST,
}),
})
sdk.start()

const result = await generateText({
model: openai('gpt-5-mini'),
prompt: args.prompt,
experimental_telemetry: {
isEnabled: true,
functionId: 'convex-ai-sdk-otel',
metadata: {
posthog_distinct_id: distinctId,
},
},
})
args: {
prompt: v.string(),
distinctId: v.optional(v.string()),
},
handler: async (_ctx, args) => {
const distinctId = args.distinctId ?? 'anonymous'

await sdk.shutdown()
const result = await generateText({
model: openai('gpt-5-mini'),
prompt: args.prompt,
experimental_telemetry: {
isEnabled: true,
functionId: 'convex-ai-sdk-otel',
metadata: {
posthog_distinct_id: distinctId,
},
},
})

return { text: result.text, usage: result.usage }
},
return { text: result.text, usage: result.usage }
},
})
38 changes: 18 additions & 20 deletions examples/example-convex/convex/aiSdk/withTracing.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
"use node"

import { PostHog } from 'posthog-node/edge'
import { withTracing } from '@posthog/ai'
import { generateText } from 'ai'
Expand All @@ -15,30 +13,30 @@ type WithTracingPostHog = Parameters<typeof withTracing>[1]
// Initialize PostHog node client for automatic LLM tracing.
// Uses Convex environment variables set via `npx convex env set`.
const phClient = new PostHog(process.env.POSTHOG_API_KEY!, {
host: process.env.POSTHOG_HOST || 'https://us.i.posthog.com',
host: process.env.POSTHOG_HOST || 'https://us.i.posthog.com',
})

// Demonstrates using the Vercel AI SDK with @posthog/ai's withTracing
// to automatically capture $ai_generation events to PostHog.
export const generate = action({
args: {
prompt: v.string(),
distinctId: v.optional(v.string()),
},
handler: async (_ctx, args) => {
// Wrap the model with PostHog tracing — this automatically captures
// $ai_generation events with token usage, latency, and content.
const model = withTracing(openai('gpt-5-mini'), phClient as unknown as WithTracingPostHog, {
posthogDistinctId: args.distinctId,
})
args: {
prompt: v.string(),
distinctId: v.optional(v.string()),
},
handler: async (_ctx, args) => {
// Wrap the model with PostHog tracing — this automatically captures
// $ai_generation events with token usage, latency, and content.
const model = withTracing(openai('gpt-5-mini'), phClient as unknown as WithTracingPostHog, {
posthogDistinctId: args.distinctId,
})

const result = await generateText({
model,
prompt: args.prompt,
})
const result = await generateText({
model,
prompt: args.prompt,
})

await phClient.flush()
await phClient.flush()

return { text: result.text, usage: result.usage }
},
return { text: result.text, usage: result.usage }
},
})
118 changes: 59 additions & 59 deletions examples/example-convex/convex/convexAgent/manualCapture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,78 +6,78 @@ import { v } from 'convex/values'
import { posthog } from '../posthog.js'

const supportAgent = new Agent(components.agent, {
name: 'support-agent',
languageModel: openai('gpt-5-mini'),
instructions: 'You are a helpful support agent. Answer questions concisely.',
name: 'support-agent',
languageModel: openai('gpt-5-mini'),
instructions: 'You are a helpful support agent. Answer questions concisely.',
})

export const generate = action({
args: {
prompt: v.string(),
distinctId: v.optional(v.string()),
},
handler: async (ctx, args) => {
const { thread } = await supportAgent.createThread(ctx, {})
args: {
prompt: v.string(),
distinctId: v.optional(v.string()),
},
handler: async (ctx, args) => {
const { thread } = await supportAgent.createThread(ctx, {})

const traceId = crypto.randomUUID()
const startTime = Date.now()
const traceId = crypto.randomUUID()
const startTime = Date.now()

// Collect usage metadata from the usageHandler callback, then combine it
// with the full result to send a comprehensive $ai_generation event.
const usageData: {
model?: string
provider?: string
agentName?: string
} = {}
// Collect usage metadata from the usageHandler callback, then combine it
// with the full result to send a comprehensive $ai_generation event.
const usageData: {
model?: string
provider?: string
agentName?: string
} = {}

const result = await thread.generateText(
{ prompt: args.prompt },
{
usageHandler: async (_usageCtx, { model, provider, agentName }) => {
usageData.model = model
usageData.provider = provider
usageData.agentName = agentName
},
}
)
const result = await thread.generateText(
{ prompt: args.prompt },
{
usageHandler: async (_usageCtx, { model, provider, agentName }) => {
usageData.model = model
usageData.provider = provider
usageData.agentName = agentName
},
}
)

const latency = (Date.now() - startTime) / 1000
const latency = (Date.now() - startTime) / 1000

await posthog.capture(ctx, {
distinctId: args.distinctId ?? 'anonymous',
event: '$ai_generation',
properties: {
// Trace ID groups multiple generations into a single trace
$ai_trace_id: traceId,
await posthog.capture(ctx, {
distinctId: args.distinctId ?? 'anonymous',
event: '$ai_generation',
properties: {
// Trace ID groups multiple generations into a single trace
$ai_trace_id: traceId,

// Core identification
$ai_provider: usageData.provider,
$ai_model: usageData.model,
$ai_span_name: usageData.agentName,
// Core identification
$ai_provider: usageData.provider,
$ai_model: usageData.model,
$ai_span_name: usageData.agentName,

// Token usage (from totalUsage to account for multi-step tool calls)
$ai_input_tokens: result.totalUsage.inputTokens,
$ai_output_tokens: result.totalUsage.outputTokens,
// Token usage (from totalUsage to account for multi-step tool calls)
$ai_input_tokens: result.totalUsage.inputTokens,
$ai_output_tokens: result.totalUsage.outputTokens,

// Cache tokens (if the provider reports them)
$ai_cache_read_input_tokens: result.totalUsage.cachedInputTokens,
// Cache tokens (if the provider reports them)
$ai_cache_read_input_tokens: result.totalUsage.cachedInputTokens,

// Performance
$ai_latency: latency,
// Performance
$ai_latency: latency,

// Input/output content
$ai_input: [{ role: 'user', content: args.prompt }],
$ai_output_choices: [{ role: 'assistant', content: result.text }],
// Input/output content
$ai_input: [{ role: 'user', content: args.prompt }],
$ai_output_choices: [{ role: 'assistant', content: result.text }],

// Generation metadata — the AI SDK doesn't expose HTTP status directly,
// so we infer success/failure from the finish reason.
$ai_is_error: result.finishReason === 'error',
},
})
// Generation metadata — the AI SDK doesn't expose HTTP status directly,
// so we infer success/failure from the finish reason.
$ai_is_error: result.finishReason === 'error',
},
})

return {
text: result.text,
usage: result.totalUsage,
}
},
return {
text: result.text,
usage: result.totalUsage,
}
},
})
Loading
Loading