Files
Richard SolomouandGitHub e38d995dec 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.
2026-04-02 19:25:41 +03:00

84 lines
2.9 KiB
TypeScript

import { Agent } from '@convex-dev/agent'
import { openai } from '@ai-sdk/openai'
import { components } from '../_generated/api'
import { action } from '../_generated/server'
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.',
})
export const generate = action({
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()
// 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 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: 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,
// Cache tokens (if the provider reports them)
$ai_cache_read_input_tokens: result.totalUsage.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.totalUsage,
}
},
})