Skip to content

Commit ca91e01

Browse files
fix(convex): remove "use node" directive from Convex examples (#3321)
* style(convex): apply prettier formatting to example-convex * refactor(convex): replace @opentelemetry/sdk-node with runtime-agnostic sdk-trace-base Remove "use node" directive from all Convex example files by switching from @opentelemetry/sdk-node (Node.js-only) to @opentelemetry/sdk-trace-base with BasicTracerProvider + BatchSpanProcessor. Add a performance polyfill for Convex's V8 isolate which lacks the performance global that @opentelemetry/core expects. * fix(convex): use class-based performance polyfill with relative timing * fix(convex): move tracer provider to module scope to avoid silent no-op trace.setGlobalTracerProvider() only applies on the first call — subsequent calls are silently ignored. Since Convex V8 isolates are warm-reused, the provider must be initialized at module scope. Per-request distinctId is already passed via experimental_telemetry metadata. * refactor(convex): extract OTEL provider setup into shared otelSetup.ts Move provider creation and global registration out of action files into a shared setup file. PostHogTraceExporter is now clearly framed as a standard OTEL span processor that sits alongside other exporters in the user's OTEL config, rather than owning the entire provider. * refactor(convex): inline OTEL setup back into individual example files Keep the provider setup co-located with the action so each example is self-contained and easy to follow without jumping between files. * fix(convex): remove competitor names from comments * style(convex): format example-convex files * fix(convex): use SimpleSpanProcessor instead of BatchSpanProcessor Convex actions terminate immediately after returning, so BatchSpanProcessor never gets a chance to flush. SimpleSpanProcessor sends spans synchronously, ensuring they're exported before the action completes.
1 parent 8571c13 commit ca91e01

21 files changed

+2239
-2433
lines changed
Lines changed: 51 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
"use node"
2-
31
import { generateText } from 'ai'
42
import { openai } from '@ai-sdk/openai'
53
import { action } from '../_generated/server'
@@ -9,55 +7,55 @@ import { posthog } from '../posthog.js'
97
// Demonstrates using the Vercel AI SDK (without @convex-dev/agent)
108
// to call an LLM and capture $ai_generation events to PostHog.
119
export const generate = action({
12-
args: {
13-
prompt: v.string(),
14-
distinctId: v.optional(v.string()),
15-
},
16-
handler: async (ctx, args) => {
17-
const traceId = crypto.randomUUID()
18-
const startTime = Date.now()
19-
20-
const result = await generateText({
21-
model: openai('gpt-5-mini'),
22-
prompt: args.prompt,
23-
})
24-
25-
const latency = (Date.now() - startTime) / 1000
26-
27-
await posthog.capture(ctx, {
28-
distinctId: args.distinctId ?? 'anonymous',
29-
event: '$ai_generation',
30-
properties: {
31-
// Trace ID groups multiple generations into a single trace
32-
$ai_trace_id: traceId,
33-
34-
// Core identification
35-
$ai_provider: 'openai',
36-
$ai_model: 'gpt-5-mini',
37-
38-
// Token usage
39-
$ai_input_tokens: result.usage.inputTokens,
40-
$ai_output_tokens: result.usage.outputTokens,
41-
42-
// Cache tokens (if the provider reports them)
43-
$ai_cache_read_input_tokens: result.usage.cachedInputTokens,
44-
45-
// Performance
46-
$ai_latency: latency,
47-
48-
// Input/output content
49-
$ai_input: [{ role: 'user', content: args.prompt }],
50-
$ai_output_choices: [{ role: 'assistant', content: result.text }],
51-
52-
// Generation metadata — the AI SDK doesn't expose HTTP status directly,
53-
// so we infer success/failure from the finish reason.
54-
$ai_is_error: result.finishReason === 'error',
55-
},
56-
})
57-
58-
return {
59-
text: result.text,
60-
usage: result.usage,
61-
}
62-
},
10+
args: {
11+
prompt: v.string(),
12+
distinctId: v.optional(v.string()),
13+
},
14+
handler: async (ctx, args) => {
15+
const traceId = crypto.randomUUID()
16+
const startTime = Date.now()
17+
18+
const result = await generateText({
19+
model: openai('gpt-5-mini'),
20+
prompt: args.prompt,
21+
})
22+
23+
const latency = (Date.now() - startTime) / 1000
24+
25+
await posthog.capture(ctx, {
26+
distinctId: args.distinctId ?? 'anonymous',
27+
event: '$ai_generation',
28+
properties: {
29+
// Trace ID groups multiple generations into a single trace
30+
$ai_trace_id: traceId,
31+
32+
// Core identification
33+
$ai_provider: 'openai',
34+
$ai_model: 'gpt-5-mini',
35+
36+
// Token usage
37+
$ai_input_tokens: result.usage.inputTokens,
38+
$ai_output_tokens: result.usage.outputTokens,
39+
40+
// Cache tokens (if the provider reports them)
41+
$ai_cache_read_input_tokens: result.usage.cachedInputTokens,
42+
43+
// Performance
44+
$ai_latency: latency,
45+
46+
// Input/output content
47+
$ai_input: [{ role: 'user', content: args.prompt }],
48+
$ai_output_choices: [{ role: 'assistant', content: result.text }],
49+
50+
// Generation metadata — the AI SDK doesn't expose HTTP status directly,
51+
// so we infer success/failure from the finish reason.
52+
$ai_is_error: result.finishReason === 'error',
53+
},
54+
})
55+
56+
return {
57+
text: result.text,
58+
usage: result.usage,
59+
}
60+
},
6361
})
Lines changed: 41 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,55 @@
1-
"use node"
1+
// Convex runs in a V8 isolate without the `performance` global that
2+
// @opentelemetry/core expects. This must be imported before any OTEL module.
3+
import '../polyfills.js'
24

3-
import { NodeSDK } from '@opentelemetry/sdk-node'
5+
import { trace } from '@opentelemetry/api'
6+
import { BasicTracerProvider, SimpleSpanProcessor } from '@opentelemetry/sdk-trace-base'
47
import { resourceFromAttributes } from '@opentelemetry/resources'
58
import { generateText } from 'ai'
69
import { openai } from '@ai-sdk/openai'
710
import { PostHogTraceExporter } from '@posthog/ai/otel'
811
import { action } from '../_generated/server'
912
import { v } from 'convex/values'
1013

14+
// PostHogTraceExporter is a standard OTEL SpanExporter — add it as a span
15+
// processor alongside any other exporters in your OTEL setup.
16+
const provider = new BasicTracerProvider({
17+
resource: resourceFromAttributes({
18+
'service.name': 'example-convex',
19+
}),
20+
spanProcessors: [
21+
new SimpleSpanProcessor(
22+
new PostHogTraceExporter({
23+
apiKey: process.env.POSTHOG_API_KEY!,
24+
host: process.env.POSTHOG_HOST,
25+
})
26+
),
27+
],
28+
})
29+
trace.setGlobalTracerProvider(provider)
30+
1131
// Demonstrates using the Vercel AI SDK's experimental_telemetry with
1232
// PostHog's PostHogTraceExporter to automatically capture $ai_generation events.
1333
export const generate = action({
14-
args: {
15-
prompt: v.string(),
16-
distinctId: v.optional(v.string()),
17-
},
18-
handler: async (_ctx, args) => {
19-
const distinctId = args.distinctId ?? 'anonymous'
20-
21-
const sdk = new NodeSDK({
22-
resource: resourceFromAttributes({
23-
'service.name': 'example-convex',
24-
'user.id': distinctId,
25-
}),
26-
traceExporter: new PostHogTraceExporter({
27-
apiKey: process.env.POSTHOG_API_KEY!,
28-
host: process.env.POSTHOG_HOST,
29-
}),
30-
})
31-
sdk.start()
32-
33-
const result = await generateText({
34-
model: openai('gpt-5-mini'),
35-
prompt: args.prompt,
36-
experimental_telemetry: {
37-
isEnabled: true,
38-
functionId: 'convex-ai-sdk-otel',
39-
metadata: {
40-
posthog_distinct_id: distinctId,
41-
},
42-
},
43-
})
34+
args: {
35+
prompt: v.string(),
36+
distinctId: v.optional(v.string()),
37+
},
38+
handler: async (_ctx, args) => {
39+
const distinctId = args.distinctId ?? 'anonymous'
4440

45-
await sdk.shutdown()
41+
const result = await generateText({
42+
model: openai('gpt-5-mini'),
43+
prompt: args.prompt,
44+
experimental_telemetry: {
45+
isEnabled: true,
46+
functionId: 'convex-ai-sdk-otel',
47+
metadata: {
48+
posthog_distinct_id: distinctId,
49+
},
50+
},
51+
})
4652

47-
return { text: result.text, usage: result.usage }
48-
},
53+
return { text: result.text, usage: result.usage }
54+
},
4955
})
Lines changed: 18 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
"use node"
2-
31
import { PostHog } from 'posthog-node/edge'
42
import { withTracing } from '@posthog/ai'
53
import { generateText } from 'ai'
@@ -15,30 +13,30 @@ type WithTracingPostHog = Parameters<typeof withTracing>[1]
1513
// Initialize PostHog node client for automatic LLM tracing.
1614
// Uses Convex environment variables set via `npx convex env set`.
1715
const phClient = new PostHog(process.env.POSTHOG_API_KEY!, {
18-
host: process.env.POSTHOG_HOST || 'https://us.i.posthog.com',
16+
host: process.env.POSTHOG_HOST || 'https://us.i.posthog.com',
1917
})
2018

2119
// Demonstrates using the Vercel AI SDK with @posthog/ai's withTracing
2220
// to automatically capture $ai_generation events to PostHog.
2321
export const generate = action({
24-
args: {
25-
prompt: v.string(),
26-
distinctId: v.optional(v.string()),
27-
},
28-
handler: async (_ctx, args) => {
29-
// Wrap the model with PostHog tracing — this automatically captures
30-
// $ai_generation events with token usage, latency, and content.
31-
const model = withTracing(openai('gpt-5-mini'), phClient as unknown as WithTracingPostHog, {
32-
posthogDistinctId: args.distinctId,
33-
})
22+
args: {
23+
prompt: v.string(),
24+
distinctId: v.optional(v.string()),
25+
},
26+
handler: async (_ctx, args) => {
27+
// Wrap the model with PostHog tracing — this automatically captures
28+
// $ai_generation events with token usage, latency, and content.
29+
const model = withTracing(openai('gpt-5-mini'), phClient as unknown as WithTracingPostHog, {
30+
posthogDistinctId: args.distinctId,
31+
})
3432

35-
const result = await generateText({
36-
model,
37-
prompt: args.prompt,
38-
})
33+
const result = await generateText({
34+
model,
35+
prompt: args.prompt,
36+
})
3937

40-
await phClient.flush()
38+
await phClient.flush()
4139

42-
return { text: result.text, usage: result.usage }
43-
},
40+
return { text: result.text, usage: result.usage }
41+
},
4442
})

examples/example-convex/convex/convexAgent/manualCapture.ts

Lines changed: 59 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -6,78 +6,78 @@ import { v } from 'convex/values'
66
import { posthog } from '../posthog.js'
77

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

1414
export const generate = action({
15-
args: {
16-
prompt: v.string(),
17-
distinctId: v.optional(v.string()),
18-
},
19-
handler: async (ctx, args) => {
20-
const { thread } = await supportAgent.createThread(ctx, {})
15+
args: {
16+
prompt: v.string(),
17+
distinctId: v.optional(v.string()),
18+
},
19+
handler: async (ctx, args) => {
20+
const { thread } = await supportAgent.createThread(ctx, {})
2121

22-
const traceId = crypto.randomUUID()
23-
const startTime = Date.now()
22+
const traceId = crypto.randomUUID()
23+
const startTime = Date.now()
2424

25-
// Collect usage metadata from the usageHandler callback, then combine it
26-
// with the full result to send a comprehensive $ai_generation event.
27-
const usageData: {
28-
model?: string
29-
provider?: string
30-
agentName?: string
31-
} = {}
25+
// Collect usage metadata from the usageHandler callback, then combine it
26+
// with the full result to send a comprehensive $ai_generation event.
27+
const usageData: {
28+
model?: string
29+
provider?: string
30+
agentName?: string
31+
} = {}
3232

33-
const result = await thread.generateText(
34-
{ prompt: args.prompt },
35-
{
36-
usageHandler: async (_usageCtx, { model, provider, agentName }) => {
37-
usageData.model = model
38-
usageData.provider = provider
39-
usageData.agentName = agentName
40-
},
41-
}
42-
)
33+
const result = await thread.generateText(
34+
{ prompt: args.prompt },
35+
{
36+
usageHandler: async (_usageCtx, { model, provider, agentName }) => {
37+
usageData.model = model
38+
usageData.provider = provider
39+
usageData.agentName = agentName
40+
},
41+
}
42+
)
4343

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

46-
await posthog.capture(ctx, {
47-
distinctId: args.distinctId ?? 'anonymous',
48-
event: '$ai_generation',
49-
properties: {
50-
// Trace ID groups multiple generations into a single trace
51-
$ai_trace_id: traceId,
46+
await posthog.capture(ctx, {
47+
distinctId: args.distinctId ?? 'anonymous',
48+
event: '$ai_generation',
49+
properties: {
50+
// Trace ID groups multiple generations into a single trace
51+
$ai_trace_id: traceId,
5252

53-
// Core identification
54-
$ai_provider: usageData.provider,
55-
$ai_model: usageData.model,
56-
$ai_span_name: usageData.agentName,
53+
// Core identification
54+
$ai_provider: usageData.provider,
55+
$ai_model: usageData.model,
56+
$ai_span_name: usageData.agentName,
5757

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

62-
// Cache tokens (if the provider reports them)
63-
$ai_cache_read_input_tokens: result.totalUsage.cachedInputTokens,
62+
// Cache tokens (if the provider reports them)
63+
$ai_cache_read_input_tokens: result.totalUsage.cachedInputTokens,
6464

65-
// Performance
66-
$ai_latency: latency,
65+
// Performance
66+
$ai_latency: latency,
6767

68-
// Input/output content
69-
$ai_input: [{ role: 'user', content: args.prompt }],
70-
$ai_output_choices: [{ role: 'assistant', content: result.text }],
68+
// Input/output content
69+
$ai_input: [{ role: 'user', content: args.prompt }],
70+
$ai_output_choices: [{ role: 'assistant', content: result.text }],
7171

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

78-
return {
79-
text: result.text,
80-
usage: result.totalUsage,
81-
}
82-
},
78+
return {
79+
text: result.text,
80+
usage: result.totalUsage,
81+
}
82+
},
8383
})

0 commit comments

Comments
 (0)