diff --git a/examples/example-convex/convex/aiSdk/manualCapture.ts b/examples/example-convex/convex/aiSdk/manualCapture.ts index b59a9dc17e..092c58f8ea 100644 --- a/examples/example-convex/convex/aiSdk/manualCapture.ts +++ b/examples/example-convex/convex/aiSdk/manualCapture.ts @@ -1,5 +1,3 @@ -"use node" - import { generateText } from 'ai' import { openai } from '@ai-sdk/openai' import { action } from '../_generated/server' @@ -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, + } + }, }) diff --git a/examples/example-convex/convex/aiSdk/openTelemetry.ts b/examples/example-convex/convex/aiSdk/openTelemetry.ts index 6d201289dc..644973f419 100644 --- a/examples/example-convex/convex/aiSdk/openTelemetry.ts +++ b/examples/example-convex/convex/aiSdk/openTelemetry.ts @@ -1,6 +1,9 @@ -"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' @@ -8,42 +11,45 @@ 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 } + }, }) diff --git a/examples/example-convex/convex/aiSdk/withTracing.ts b/examples/example-convex/convex/aiSdk/withTracing.ts index 448daa84bc..5cf6d37f10 100644 --- a/examples/example-convex/convex/aiSdk/withTracing.ts +++ b/examples/example-convex/convex/aiSdk/withTracing.ts @@ -1,5 +1,3 @@ -"use node" - import { PostHog } from 'posthog-node/edge' import { withTracing } from '@posthog/ai' import { generateText } from 'ai' @@ -15,30 +13,30 @@ type WithTracingPostHog = Parameters[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 } + }, }) diff --git a/examples/example-convex/convex/convexAgent/manualCapture.ts b/examples/example-convex/convex/convexAgent/manualCapture.ts index c766b896eb..bfc6285e49 100644 --- a/examples/example-convex/convex/convexAgent/manualCapture.ts +++ b/examples/example-convex/convex/convexAgent/manualCapture.ts @@ -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, + } + }, }) diff --git a/examples/example-convex/convex/convexAgent/openTelemetry.ts b/examples/example-convex/convex/convexAgent/openTelemetry.ts index 358a2ace68..aaff8a43ca 100644 --- a/examples/example-convex/convex/convexAgent/openTelemetry.ts +++ b/examples/example-convex/convex/convexAgent/openTelemetry.ts @@ -1,6 +1,9 @@ -"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 { Agent } from '@convex-dev/agent' import { openai } from '@ai-sdk/openai' @@ -9,53 +12,56 @@ import { components } from '../_generated/api' 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 @convex-dev/agent with the Vercel AI SDK's // experimental_telemetry and 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 supportAgent = new Agent(components.agent, { - name: 'support-agent', - languageModel: openai('gpt-5-mini'), - instructions: 'You are a helpful support agent. Answer questions concisely.', - }) + args: { + prompt: v.string(), + distinctId: v.optional(v.string()), + }, + handler: async (ctx, args) => { + const distinctId = args.distinctId ?? 'anonymous' - const { thread } = await supportAgent.createThread(ctx, {}) + const supportAgent = new Agent(components.agent, { + name: 'support-agent', + languageModel: openai('gpt-5-mini'), + instructions: 'You are a helpful support agent. Answer questions concisely.', + }) - const result = await thread.generateText({ - prompt: args.prompt, - experimental_telemetry: { - isEnabled: true, - functionId: 'convex-agent-otel', - metadata: { - posthog_distinct_id: distinctId, - }, - }, - }) + const { thread } = await supportAgent.createThread(ctx, {}) - await sdk.shutdown() + const result = await thread.generateText({ + prompt: args.prompt, + experimental_telemetry: { + isEnabled: true, + functionId: 'convex-agent-otel', + metadata: { + posthog_distinct_id: distinctId, + }, + }, + }) - return { - text: result.text, - usage: result.totalUsage, - } - }, + return { + text: result.text, + usage: result.totalUsage, + } + }, }) diff --git a/examples/example-convex/convex/convexAgent/withTracing.ts b/examples/example-convex/convex/convexAgent/withTracing.ts index 656a1a6cfc..30fbe8d72f 100644 --- a/examples/example-convex/convex/convexAgent/withTracing.ts +++ b/examples/example-convex/convex/convexAgent/withTracing.ts @@ -1,5 +1,3 @@ -"use node" - import { PostHog } from 'posthog-node/edge' import { withTracing } from '@posthog/ai' import { Agent } from '@convex-dev/agent' @@ -16,38 +14,38 @@ type WithTracingPostHog = Parameters[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 @convex-dev/agent 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 before passing it to the agent. - // Every LLM call the agent makes will automatically capture $ai_generation events. - const model = withTracing(openai('gpt-5-mini'), phClient as unknown as WithTracingPostHog, { - posthogDistinctId: args.distinctId, - }) - - const supportAgent = new Agent(components.agent, { - name: 'support-agent', - languageModel: model, - instructions: 'You are a helpful support agent. Answer questions concisely.', - }) - - const { thread } = await supportAgent.createThread(ctx, {}) - - const result = await thread.generateText({ prompt: args.prompt }) - - await phClient.flush() - - return { - text: result.text, - usage: result.totalUsage, - } - }, + args: { + prompt: v.string(), + distinctId: v.optional(v.string()), + }, + handler: async (ctx, args) => { + // Wrap the model with PostHog tracing before passing it to the agent. + // Every LLM call the agent makes will automatically capture $ai_generation events. + const model = withTracing(openai('gpt-5-mini'), phClient as unknown as WithTracingPostHog, { + posthogDistinctId: args.distinctId, + }) + + const supportAgent = new Agent(components.agent, { + name: 'support-agent', + languageModel: model, + instructions: 'You are a helpful support agent. Answer questions concisely.', + }) + + const { thread } = await supportAgent.createThread(ctx, {}) + + const result = await thread.generateText({ prompt: args.prompt }) + + await phClient.flush() + + return { + text: result.text, + usage: result.totalUsage, + } + }, }) diff --git a/examples/example-convex/convex/example.test.ts b/examples/example-convex/convex/example.test.ts index 6299b69930..a8e66254b5 100644 --- a/examples/example-convex/convex/example.test.ts +++ b/examples/example-convex/convex/example.test.ts @@ -11,807 +11,809 @@ let fetchCalls: Array<{ url: string; body: unknown }> = [] const originalFetch = global.fetch function mockFetch(responseByUrl?: Record) { - fetchCalls = [] - return jest.fn(async (url: string | URL, init?: RequestInit) => { - const urlStr = url.toString() - let body: unknown - if (init?.body) { - let rawText: string - if (init.body instanceof Blob) { - const headers = init.headers as Record | undefined - if (headers?.['Content-Encoding'] === 'gzip') { - const ds = new DecompressionStream('gzip') - rawText = await new Response(init.body.stream().pipeThrough(ds)).text() - } else { - rawText = await init.body.text() + fetchCalls = [] + return jest.fn(async (url: string | URL, init?: RequestInit) => { + const urlStr = url.toString() + let body: unknown + if (init?.body) { + let rawText: string + if (init.body instanceof Blob) { + const headers = init.headers as Record | undefined + if (headers?.['Content-Encoding'] === 'gzip') { + const ds = new DecompressionStream('gzip') + rawText = await new Response(init.body.stream().pipeThrough(ds)).text() + } else { + rawText = await init.body.text() + } + } else { + rawText = init.body as string + } + try { + body = JSON.parse(rawText) + } catch { + body = rawText + } } - } else { - rawText = init.body as string - } - try { - body = JSON.parse(rawText) - } catch { - body = rawText - } - } - fetchCalls.push({ url: urlStr, body }) - - if (responseByUrl) { - for (const [pattern, response] of Object.entries(responseByUrl)) { - if (urlStr.includes(pattern)) { - return new Response(JSON.stringify(response), { - status: 200, - headers: { 'Content-Type': 'application/json' }, - }) + fetchCalls.push({ url: urlStr, body }) + + if (responseByUrl) { + for (const [pattern, response] of Object.entries(responseByUrl)) { + if (urlStr.includes(pattern)) { + return new Response(JSON.stringify(response), { + status: 200, + headers: { 'Content-Type': 'application/json' }, + }) + } + } } - } - } - return new Response(JSON.stringify({ status: 1 }), { - status: 200, - headers: { 'Content-Type': 'application/json' }, - }) - }) as unknown as typeof fetch + return new Response(JSON.stringify({ status: 1 }), { + status: 200, + headers: { 'Content-Type': 'application/json' }, + }) + }) as unknown as typeof fetch } function batchCalls() { - return fetchCalls.filter((c) => c.url.includes('/batch')) + return fetchCalls.filter((c) => c.url.includes('/batch')) } function flagsCalls() { - return fetchCalls.filter((c) => c.url.includes('/flags')) + return fetchCalls.filter((c) => c.url.includes('/flags')) } // Extract the first event from the first batch call function firstBatchEvent(): Record { - const batches = batchCalls() - const batch = batches[0]?.body as { batch: Record[] } - return batch?.batch?.[0] ?? {} + const batches = batchCalls() + const batch = batches[0]?.body as { batch: Record[] } + return batch?.batch?.[0] ?? {} } describe('capture', () => { - beforeEach(() => { - process.env.POSTHOG_API_KEY = 'phc_test_key' - process.env.POSTHOG_HOST = 'https://test.posthog.com' - }) - - afterEach(() => { - global.fetch = originalFetch - delete process.env.POSTHOG_API_KEY - delete process.env.POSTHOG_HOST - fetchCalls = [] - }) - - test('sends event to PostHog API with correct distinct_id and event name', async () => { - global.fetch = mockFetch() - const t = initConvexTest() - - const result = await t.mutation(api.example.testCapture, { - distinctId: 'user-123', - event: 'button_clicked', - }) - expect(result).toEqual({ success: true }) - jest.runAllTimers() - await t.finishInProgressScheduledFunctions() - - expect(batchCalls().length).toBeGreaterThanOrEqual(1) - const batch = batchCalls()[0].body as { api_key: string } - expect(batch.api_key).toBe('phc_test_key') - - const event = firstBatchEvent() - expect(event.distinct_id).toBe('user-123') - expect(event.event).toBe('button_clicked') - }) - - test('sends properties and groups', async () => { - global.fetch = mockFetch() - const t = initConvexTest() - - await t.mutation(api.example.testCapture, { - distinctId: 'user-456', - event: 'purchase', - properties: { plan: 'pro', amount: 99 }, - groups: { company: 'acme' }, - }) - jest.runAllTimers() - await t.finishInProgressScheduledFunctions() - - const event = firstBatchEvent() - const props = event.properties as Record - expect(props.plan).toBe('pro') - expect(props.amount).toBe(99) - expect(props.$groups).toEqual({ company: 'acme' }) - }) - - test('beforeSend enriches properties with environment', async () => { - global.fetch = mockFetch() - const t = initConvexTest() - - await t.mutation(api.example.testCapture, { - distinctId: 'user-123', - event: 'test', - properties: { foo: 'bar' }, - }) - jest.runAllTimers() - await t.finishInProgressScheduledFunctions() - - const props = firstBatchEvent().properties as Record - expect(props.environment).toBe('example-app') - expect(props.foo).toBe('bar') - }) - - test('sends disableGeoip flag', async () => { - global.fetch = mockFetch() - const t = initConvexTest() - - await t.mutation(api.example.testCapture, { - distinctId: 'user-123', - event: 'test', - disableGeoip: true, - }) - jest.runAllTimers() - await t.finishInProgressScheduledFunctions() - - const props = firstBatchEvent().properties as Record - expect(props.$geoip_disable).toBe(true) - }) - - test('sends custom uuid', async () => { - global.fetch = mockFetch() - const t = initConvexTest() - - await t.mutation(api.example.testCapture, { - distinctId: 'user-123', - event: 'test', - uuid: 'custom-uuid-abc', - }) - jest.runAllTimers() - await t.finishInProgressScheduledFunctions() - - const event = firstBatchEvent() - expect(event.uuid).toBe('custom-uuid-abc') - }) - - test('sends timestamp', async () => { - global.fetch = mockFetch() - const t = initConvexTest() - - await t.mutation(api.example.testCapture, { - distinctId: 'user-123', - event: 'test', - timestamp: '2024-06-15T12:00:00Z', - }) - jest.runAllTimers() - await t.finishInProgressScheduledFunctions() - - const event = firstBatchEvent() - expect(event.timestamp).toContain('2024-06-15') - }) + beforeEach(() => { + process.env.POSTHOG_API_KEY = 'phc_test_key' + process.env.POSTHOG_HOST = 'https://test.posthog.com' + }) + + afterEach(() => { + global.fetch = originalFetch + delete process.env.POSTHOG_API_KEY + delete process.env.POSTHOG_HOST + fetchCalls = [] + }) + + test('sends event to PostHog API with correct distinct_id and event name', async () => { + global.fetch = mockFetch() + const t = initConvexTest() + + const result = await t.mutation(api.example.testCapture, { + distinctId: 'user-123', + event: 'button_clicked', + }) + expect(result).toEqual({ success: true }) + jest.runAllTimers() + await t.finishInProgressScheduledFunctions() + + expect(batchCalls().length).toBeGreaterThanOrEqual(1) + const batch = batchCalls()[0].body as { api_key: string } + expect(batch.api_key).toBe('phc_test_key') + + const event = firstBatchEvent() + expect(event.distinct_id).toBe('user-123') + expect(event.event).toBe('button_clicked') + }) + + test('sends properties and groups', async () => { + global.fetch = mockFetch() + const t = initConvexTest() + + await t.mutation(api.example.testCapture, { + distinctId: 'user-456', + event: 'purchase', + properties: { plan: 'pro', amount: 99 }, + groups: { company: 'acme' }, + }) + jest.runAllTimers() + await t.finishInProgressScheduledFunctions() + + const event = firstBatchEvent() + const props = event.properties as Record + expect(props.plan).toBe('pro') + expect(props.amount).toBe(99) + expect(props.$groups).toEqual({ company: 'acme' }) + }) + + test('beforeSend enriches properties with environment', async () => { + global.fetch = mockFetch() + const t = initConvexTest() + + await t.mutation(api.example.testCapture, { + distinctId: 'user-123', + event: 'test', + properties: { foo: 'bar' }, + }) + jest.runAllTimers() + await t.finishInProgressScheduledFunctions() + + const props = firstBatchEvent().properties as Record + expect(props.environment).toBe('example-app') + expect(props.foo).toBe('bar') + }) + + test('sends disableGeoip flag', async () => { + global.fetch = mockFetch() + const t = initConvexTest() + + await t.mutation(api.example.testCapture, { + distinctId: 'user-123', + event: 'test', + disableGeoip: true, + }) + jest.runAllTimers() + await t.finishInProgressScheduledFunctions() + + const props = firstBatchEvent().properties as Record + expect(props.$geoip_disable).toBe(true) + }) + + test('sends custom uuid', async () => { + global.fetch = mockFetch() + const t = initConvexTest() + + await t.mutation(api.example.testCapture, { + distinctId: 'user-123', + event: 'test', + uuid: 'custom-uuid-abc', + }) + jest.runAllTimers() + await t.finishInProgressScheduledFunctions() + + const event = firstBatchEvent() + expect(event.uuid).toBe('custom-uuid-abc') + }) + + test('sends timestamp', async () => { + global.fetch = mockFetch() + const t = initConvexTest() + + await t.mutation(api.example.testCapture, { + distinctId: 'user-123', + event: 'test', + timestamp: '2024-06-15T12:00:00Z', + }) + jest.runAllTimers() + await t.finishInProgressScheduledFunctions() + + const event = firstBatchEvent() + expect(event.timestamp).toContain('2024-06-15') + }) }) describe('identify', () => { - beforeEach(() => { - process.env.POSTHOG_API_KEY = 'phc_test_key' - process.env.POSTHOG_HOST = 'https://test.posthog.com' - }) - - afterEach(() => { - global.fetch = originalFetch - delete process.env.POSTHOG_API_KEY - delete process.env.POSTHOG_HOST - fetchCalls = [] - }) - - test('sends $identify event', async () => { - global.fetch = mockFetch() - const t = initConvexTest() - - const result = await t.mutation(api.example.testIdentify, { - distinctId: 'user-123', - }) - expect(result).toEqual({ success: true }) - jest.runAllTimers() - await t.finishInProgressScheduledFunctions() - - expect(batchCalls().length).toBeGreaterThanOrEqual(1) - const event = firstBatchEvent() - expect(event.event).toBe('$identify') - expect(event.distinct_id).toBe('user-123') - }) - - test('sends user properties', async () => { - global.fetch = mockFetch() - const t = initConvexTest() - - await t.mutation(api.example.testIdentify, { - distinctId: 'user-123', - properties: { - name: 'Test User', - email: 'test@example.com', - }, - }) - jest.runAllTimers() - await t.finishInProgressScheduledFunctions() - - const event = firstBatchEvent() - // posthog-node puts properties into $set inside event.properties - const props = event.properties as Record - const $set = props.$set as Record - expect($set.name).toBe('Test User') - expect($set.email).toBe('test@example.com') - }) - - test('sends disableGeoip', async () => { - global.fetch = mockFetch() - const t = initConvexTest() - - await t.mutation(api.example.testIdentify, { - distinctId: 'user-123', - disableGeoip: true, - }) - jest.runAllTimers() - await t.finishInProgressScheduledFunctions() - - const props = firstBatchEvent().properties as Record - expect(props.$geoip_disable).toBe(true) - }) + beforeEach(() => { + process.env.POSTHOG_API_KEY = 'phc_test_key' + process.env.POSTHOG_HOST = 'https://test.posthog.com' + }) + + afterEach(() => { + global.fetch = originalFetch + delete process.env.POSTHOG_API_KEY + delete process.env.POSTHOG_HOST + fetchCalls = [] + }) + + test('sends $identify event', async () => { + global.fetch = mockFetch() + const t = initConvexTest() + + const result = await t.mutation(api.example.testIdentify, { + distinctId: 'user-123', + }) + expect(result).toEqual({ success: true }) + jest.runAllTimers() + await t.finishInProgressScheduledFunctions() + + expect(batchCalls().length).toBeGreaterThanOrEqual(1) + const event = firstBatchEvent() + expect(event.event).toBe('$identify') + expect(event.distinct_id).toBe('user-123') + }) + + test('sends user properties', async () => { + global.fetch = mockFetch() + const t = initConvexTest() + + await t.mutation(api.example.testIdentify, { + distinctId: 'user-123', + properties: { + name: 'Test User', + email: 'test@example.com', + }, + }) + jest.runAllTimers() + await t.finishInProgressScheduledFunctions() + + const event = firstBatchEvent() + // posthog-node puts properties into $set inside event.properties + const props = event.properties as Record + const $set = props.$set as Record + expect($set.name).toBe('Test User') + expect($set.email).toBe('test@example.com') + }) + + test('sends disableGeoip', async () => { + global.fetch = mockFetch() + const t = initConvexTest() + + await t.mutation(api.example.testIdentify, { + distinctId: 'user-123', + disableGeoip: true, + }) + jest.runAllTimers() + await t.finishInProgressScheduledFunctions() + + const props = firstBatchEvent().properties as Record + expect(props.$geoip_disable).toBe(true) + }) }) describe('groupIdentify', () => { - beforeEach(() => { - process.env.POSTHOG_API_KEY = 'phc_test_key' - process.env.POSTHOG_HOST = 'https://test.posthog.com' - }) - - afterEach(() => { - global.fetch = originalFetch - delete process.env.POSTHOG_API_KEY - delete process.env.POSTHOG_HOST - fetchCalls = [] - }) - - test('sends $groupidentify event with group type and key', async () => { - global.fetch = mockFetch() - const t = initConvexTest() - - const result = await t.mutation(api.example.testGroupIdentify, { - groupType: 'company', - groupKey: 'acme', - }) - expect(result).toEqual({ success: true }) - jest.runAllTimers() - await t.finishInProgressScheduledFunctions() - - expect(batchCalls().length).toBeGreaterThanOrEqual(1) - const event = firstBatchEvent() - expect(event.event).toBe('$groupidentify') - const props = event.properties as Record - expect(props.$group_type).toBe('company') - expect(props.$group_key).toBe('acme') - }) - - test('sends group properties via $group_set', async () => { - global.fetch = mockFetch() - const t = initConvexTest() - - await t.mutation(api.example.testGroupIdentify, { - groupType: 'company', - groupKey: 'acme', - properties: { industry: 'Technology', size: 100 }, - }) - jest.runAllTimers() - await t.finishInProgressScheduledFunctions() - - const props = firstBatchEvent().properties as Record - const groupSet = props.$group_set as Record - expect(groupSet.industry).toBe('Technology') - expect(groupSet.size).toBe(100) - }) - - test('uses distinctId override when provided', async () => { - global.fetch = mockFetch() - const t = initConvexTest() - - await t.mutation(api.example.testGroupIdentify, { - groupType: 'company', - groupKey: 'acme', - distinctId: 'override-user', - }) - jest.runAllTimers() - await t.finishInProgressScheduledFunctions() - - expect(firstBatchEvent().distinct_id).toBe('override-user') - }) + beforeEach(() => { + process.env.POSTHOG_API_KEY = 'phc_test_key' + process.env.POSTHOG_HOST = 'https://test.posthog.com' + }) + + afterEach(() => { + global.fetch = originalFetch + delete process.env.POSTHOG_API_KEY + delete process.env.POSTHOG_HOST + fetchCalls = [] + }) + + test('sends $groupidentify event with group type and key', async () => { + global.fetch = mockFetch() + const t = initConvexTest() + + const result = await t.mutation(api.example.testGroupIdentify, { + groupType: 'company', + groupKey: 'acme', + }) + expect(result).toEqual({ success: true }) + jest.runAllTimers() + await t.finishInProgressScheduledFunctions() + + expect(batchCalls().length).toBeGreaterThanOrEqual(1) + const event = firstBatchEvent() + expect(event.event).toBe('$groupidentify') + const props = event.properties as Record + expect(props.$group_type).toBe('company') + expect(props.$group_key).toBe('acme') + }) + + test('sends group properties via $group_set', async () => { + global.fetch = mockFetch() + const t = initConvexTest() + + await t.mutation(api.example.testGroupIdentify, { + groupType: 'company', + groupKey: 'acme', + properties: { industry: 'Technology', size: 100 }, + }) + jest.runAllTimers() + await t.finishInProgressScheduledFunctions() + + const props = firstBatchEvent().properties as Record + const groupSet = props.$group_set as Record + expect(groupSet.industry).toBe('Technology') + expect(groupSet.size).toBe(100) + }) + + test('uses distinctId override when provided', async () => { + global.fetch = mockFetch() + const t = initConvexTest() + + await t.mutation(api.example.testGroupIdentify, { + groupType: 'company', + groupKey: 'acme', + distinctId: 'override-user', + }) + jest.runAllTimers() + await t.finishInProgressScheduledFunctions() + + expect(firstBatchEvent().distinct_id).toBe('override-user') + }) }) describe('alias', () => { - beforeEach(() => { - process.env.POSTHOG_API_KEY = 'phc_test_key' - process.env.POSTHOG_HOST = 'https://test.posthog.com' - }) - - afterEach(() => { - global.fetch = originalFetch - delete process.env.POSTHOG_API_KEY - delete process.env.POSTHOG_HOST - fetchCalls = [] - }) - - test('sends $create_alias event', async () => { - global.fetch = mockFetch() - const t = initConvexTest() - - const result = await t.mutation(api.example.testAlias, { - distinctId: 'user-123', - alias: 'anon-456', - }) - expect(result).toEqual({ success: true }) - jest.runAllTimers() - await t.finishInProgressScheduledFunctions() - - expect(batchCalls().length).toBeGreaterThanOrEqual(1) - const event = firstBatchEvent() - expect(event.event).toBe('$create_alias') - const props = event.properties as Record - expect(props.distinct_id).toBe('user-123') - expect(props.alias).toBe('anon-456') - }) - - test('sends disableGeoip', async () => { - global.fetch = mockFetch() - const t = initConvexTest() - - await t.mutation(api.example.testAlias, { - distinctId: 'user-123', - alias: 'anon-456', - disableGeoip: true, - }) - jest.runAllTimers() - await t.finishInProgressScheduledFunctions() - - const props = firstBatchEvent().properties as Record - expect(props.$geoip_disable).toBe(true) - }) + beforeEach(() => { + process.env.POSTHOG_API_KEY = 'phc_test_key' + process.env.POSTHOG_HOST = 'https://test.posthog.com' + }) + + afterEach(() => { + global.fetch = originalFetch + delete process.env.POSTHOG_API_KEY + delete process.env.POSTHOG_HOST + fetchCalls = [] + }) + + test('sends $create_alias event', async () => { + global.fetch = mockFetch() + const t = initConvexTest() + + const result = await t.mutation(api.example.testAlias, { + distinctId: 'user-123', + alias: 'anon-456', + }) + expect(result).toEqual({ success: true }) + jest.runAllTimers() + await t.finishInProgressScheduledFunctions() + + expect(batchCalls().length).toBeGreaterThanOrEqual(1) + const event = firstBatchEvent() + expect(event.event).toBe('$create_alias') + const props = event.properties as Record + expect(props.distinct_id).toBe('user-123') + expect(props.alias).toBe('anon-456') + }) + + test('sends disableGeoip', async () => { + global.fetch = mockFetch() + const t = initConvexTest() + + await t.mutation(api.example.testAlias, { + distinctId: 'user-123', + alias: 'anon-456', + disableGeoip: true, + }) + jest.runAllTimers() + await t.finishInProgressScheduledFunctions() + + const props = firstBatchEvent().properties as Record + expect(props.$geoip_disable).toBe(true) + }) }) describe('captureException', () => { - beforeEach(() => { - process.env.POSTHOG_API_KEY = 'phc_test_key' - process.env.POSTHOG_HOST = 'https://test.posthog.com' - }) - - afterEach(() => { - global.fetch = originalFetch - delete process.env.POSTHOG_API_KEY - delete process.env.POSTHOG_HOST - fetchCalls = [] - }) - - test('sends $exception event with Error object', async () => { - global.fetch = mockFetch() - const t = initConvexTest() - - const result = await t.mutation(api.example.testCaptureException, { - errorMessage: 'Something went wrong', - errorType: 'error', - }) - expect(result).toEqual({ success: true }) - jest.runAllTimers() - await t.finishInProgressScheduledFunctions() - - expect(batchCalls().length).toBeGreaterThanOrEqual(1) - const event = firstBatchEvent() - expect(event.event).toBe('$exception') - const props = event.properties as Record - // posthog-node v5 uses $exception_list instead of $exception_message - const exceptionList = props.$exception_list as Array<{ - value: string - type: string - }> - expect(exceptionList[0].value).toBe('Something went wrong') - }) - - test('sends $exception event with string error', async () => { - global.fetch = mockFetch() - const t = initConvexTest() - - await t.mutation(api.example.testCaptureException, { - errorMessage: 'string error', - errorType: 'string', - }) - jest.runAllTimers() - await t.finishInProgressScheduledFunctions() - - const props = firstBatchEvent().properties as Record - const exceptionList = props.$exception_list as Array<{ - value: string - }> - expect(exceptionList[0].value).toBe('string error') - }) - - test('sends $exception event with object error', async () => { - global.fetch = mockFetch() - const t = initConvexTest() - - await t.mutation(api.example.testCaptureException, { - errorMessage: 'obj error', - errorType: 'object', - }) - jest.runAllTimers() - await t.finishInProgressScheduledFunctions() - - const props = firstBatchEvent().properties as Record - const exceptionList = props.$exception_list as Array<{ - value: string - }> - expect(exceptionList[0].value).toBe('obj error') - }) - - test('includes additional properties', async () => { - global.fetch = mockFetch() - const t = initConvexTest() - - await t.mutation(api.example.testCaptureException, { - errorMessage: 'test', - additionalProperties: { page: '/checkout', step: 3 }, - }) - jest.runAllTimers() - await t.finishInProgressScheduledFunctions() - - const props = firstBatchEvent().properties as Record - expect(props.page).toBe('/checkout') - expect(props.step).toBe(3) - }) - - test('uses distinctId when provided', async () => { - global.fetch = mockFetch() - const t = initConvexTest() - - await t.mutation(api.example.testCaptureException, { - errorMessage: 'test', - distinctId: 'specific-user', - }) - jest.runAllTimers() - await t.finishInProgressScheduledFunctions() - - expect(firstBatchEvent().distinct_id).toBe('specific-user') - }) + beforeEach(() => { + process.env.POSTHOG_API_KEY = 'phc_test_key' + process.env.POSTHOG_HOST = 'https://test.posthog.com' + }) + + afterEach(() => { + global.fetch = originalFetch + delete process.env.POSTHOG_API_KEY + delete process.env.POSTHOG_HOST + fetchCalls = [] + }) + + test('sends $exception event with Error object', async () => { + global.fetch = mockFetch() + const t = initConvexTest() + + const result = await t.mutation(api.example.testCaptureException, { + errorMessage: 'Something went wrong', + errorType: 'error', + }) + expect(result).toEqual({ success: true }) + jest.runAllTimers() + await t.finishInProgressScheduledFunctions() + + expect(batchCalls().length).toBeGreaterThanOrEqual(1) + const event = firstBatchEvent() + expect(event.event).toBe('$exception') + const props = event.properties as Record + // posthog-node v5 uses $exception_list instead of $exception_message + const exceptionList = props.$exception_list as Array<{ + value: string + type: string + }> + expect(exceptionList[0].value).toBe('Something went wrong') + }) + + test('sends $exception event with string error', async () => { + global.fetch = mockFetch() + const t = initConvexTest() + + await t.mutation(api.example.testCaptureException, { + errorMessage: 'string error', + errorType: 'string', + }) + jest.runAllTimers() + await t.finishInProgressScheduledFunctions() + + const props = firstBatchEvent().properties as Record + const exceptionList = props.$exception_list as Array<{ + value: string + }> + expect(exceptionList[0].value).toBe('string error') + }) + + test('sends $exception event with object error', async () => { + global.fetch = mockFetch() + const t = initConvexTest() + + await t.mutation(api.example.testCaptureException, { + errorMessage: 'obj error', + errorType: 'object', + }) + jest.runAllTimers() + await t.finishInProgressScheduledFunctions() + + const props = firstBatchEvent().properties as Record + const exceptionList = props.$exception_list as Array<{ + value: string + }> + expect(exceptionList[0].value).toBe('obj error') + }) + + test('includes additional properties', async () => { + global.fetch = mockFetch() + const t = initConvexTest() + + await t.mutation(api.example.testCaptureException, { + errorMessage: 'test', + additionalProperties: { page: '/checkout', step: 3 }, + }) + jest.runAllTimers() + await t.finishInProgressScheduledFunctions() + + const props = firstBatchEvent().properties as Record + expect(props.page).toBe('/checkout') + expect(props.step).toBe(3) + }) + + test('uses distinctId when provided', async () => { + global.fetch = mockFetch() + const t = initConvexTest() + + await t.mutation(api.example.testCaptureException, { + errorMessage: 'test', + distinctId: 'specific-user', + }) + jest.runAllTimers() + await t.finishInProgressScheduledFunctions() + + expect(firstBatchEvent().distinct_id).toBe('specific-user') + }) }) const flagsResponse = (flags: Record = {}, payloads: Record = {}) => ({ - '/flags': { - featureFlags: flags, - featureFlagPayloads: payloads, - }, + '/flags': { + featureFlags: flags, + featureFlagPayloads: payloads, + }, }) describe('getFeatureFlag', () => { - beforeEach(() => { - process.env.POSTHOG_API_KEY = 'phc_test_key' - process.env.POSTHOG_HOST = 'https://test.posthog.com' - }) - - afterEach(() => { - global.fetch = originalFetch - delete process.env.POSTHOG_API_KEY - delete process.env.POSTHOG_HOST - fetchCalls = [] - }) - - test('returns flag value', async () => { - global.fetch = mockFetch(flagsResponse({ 'test-flag': 'variant-a' })) - const t = initConvexTest() + beforeEach(() => { + process.env.POSTHOG_API_KEY = 'phc_test_key' + process.env.POSTHOG_HOST = 'https://test.posthog.com' + }) - const result = await t.action(api.example.testGetFeatureFlag, { - distinctId: 'user-123', - flagKey: 'test-flag', + afterEach(() => { + global.fetch = originalFetch + delete process.env.POSTHOG_API_KEY + delete process.env.POSTHOG_HOST + fetchCalls = [] }) - expect(result).toEqual({ flagKey: 'test-flag', value: 'variant-a' }) - }) + test('returns flag value', async () => { + global.fetch = mockFetch(flagsResponse({ 'test-flag': 'variant-a' })) + const t = initConvexTest() - test('returns boolean flag', async () => { - global.fetch = mockFetch(flagsResponse({ 'bool-flag': true })) - const t = initConvexTest() + const result = await t.action(api.example.testGetFeatureFlag, { + distinctId: 'user-123', + flagKey: 'test-flag', + }) - const result = await t.action(api.example.testGetFeatureFlag, { - distinctId: 'user-123', - flagKey: 'bool-flag', + expect(result).toEqual({ flagKey: 'test-flag', value: 'variant-a' }) }) - expect(result).toEqual({ flagKey: 'bool-flag', value: true }) - }) + test('returns boolean flag', async () => { + global.fetch = mockFetch(flagsResponse({ 'bool-flag': true })) + const t = initConvexTest() - test('returns null for non-existent flag', async () => { - global.fetch = mockFetch(flagsResponse()) - const t = initConvexTest() + const result = await t.action(api.example.testGetFeatureFlag, { + distinctId: 'user-123', + flagKey: 'bool-flag', + }) - const result = await t.action(api.example.testGetFeatureFlag, { - distinctId: 'user-123', - flagKey: 'missing', + expect(result).toEqual({ flagKey: 'bool-flag', value: true }) }) - expect(result).toEqual({ flagKey: 'missing', value: null }) - }) + test('returns null for non-existent flag', async () => { + global.fetch = mockFetch(flagsResponse()) + const t = initConvexTest() - test('sends groups and person properties to /flags', async () => { - global.fetch = mockFetch(flagsResponse({ 'test-flag': true })) - const t = initConvexTest() + const result = await t.action(api.example.testGetFeatureFlag, { + distinctId: 'user-123', + flagKey: 'missing', + }) - await t.action(api.example.testGetFeatureFlag, { - distinctId: 'user-123', - flagKey: 'test-flag', - groups: { company: 'acme' }, - personProperties: { email: 'test@example.com' }, - groupProperties: { company: { industry: 'tech' } }, + expect(result).toEqual({ flagKey: 'missing', value: null }) }) - const calls = flagsCalls() - expect(calls.length).toBeGreaterThanOrEqual(1) - const body = calls[0].body as Record - expect(body.distinct_id).toBe('user-123') - expect(body.groups).toEqual({ company: 'acme' }) - expect(body.person_properties).toMatchObject({ - email: 'test@example.com', - }) - expect(body.group_properties).toMatchObject({ - company: { industry: 'tech' }, + test('sends groups and person properties to /flags', async () => { + global.fetch = mockFetch(flagsResponse({ 'test-flag': true })) + const t = initConvexTest() + + await t.action(api.example.testGetFeatureFlag, { + distinctId: 'user-123', + flagKey: 'test-flag', + groups: { company: 'acme' }, + personProperties: { email: 'test@example.com' }, + groupProperties: { company: { industry: 'tech' } }, + }) + + const calls = flagsCalls() + expect(calls.length).toBeGreaterThanOrEqual(1) + const body = calls[0].body as Record + expect(body.distinct_id).toBe('user-123') + expect(body.groups).toEqual({ company: 'acme' }) + expect(body.person_properties).toMatchObject({ + email: 'test@example.com', + }) + expect(body.group_properties).toMatchObject({ + company: { industry: 'tech' }, + }) }) - }) }) describe('isFeatureEnabled', () => { - beforeEach(() => { - process.env.POSTHOG_API_KEY = 'phc_test_key' - process.env.POSTHOG_HOST = 'https://test.posthog.com' - }) - - afterEach(() => { - global.fetch = originalFetch - delete process.env.POSTHOG_API_KEY - delete process.env.POSTHOG_HOST - fetchCalls = [] - }) - - test('returns true for enabled flag', async () => { - global.fetch = mockFetch(flagsResponse({ 'test-flag': true })) - const t = initConvexTest() + beforeEach(() => { + process.env.POSTHOG_API_KEY = 'phc_test_key' + process.env.POSTHOG_HOST = 'https://test.posthog.com' + }) - const result = await t.action(api.example.testIsFeatureEnabled, { - distinctId: 'user-123', - flagKey: 'test-flag', + afterEach(() => { + global.fetch = originalFetch + delete process.env.POSTHOG_API_KEY + delete process.env.POSTHOG_HOST + fetchCalls = [] }) - expect(result).toEqual({ flagKey: 'test-flag', enabled: true }) - }) + test('returns true for enabled flag', async () => { + global.fetch = mockFetch(flagsResponse({ 'test-flag': true })) + const t = initConvexTest() - test('returns true for string variant (truthy)', async () => { - global.fetch = mockFetch(flagsResponse({ 'test-flag': 'variant-a' })) - const t = initConvexTest() + const result = await t.action(api.example.testIsFeatureEnabled, { + distinctId: 'user-123', + flagKey: 'test-flag', + }) - const result = await t.action(api.example.testIsFeatureEnabled, { - distinctId: 'user-123', - flagKey: 'test-flag', + expect(result).toEqual({ flagKey: 'test-flag', enabled: true }) }) - expect(result).toEqual({ flagKey: 'test-flag', enabled: true }) - }) + test('returns true for string variant (truthy)', async () => { + global.fetch = mockFetch(flagsResponse({ 'test-flag': 'variant-a' })) + const t = initConvexTest() - test('returns null for non-existent flag', async () => { - global.fetch = mockFetch(flagsResponse()) - const t = initConvexTest() + const result = await t.action(api.example.testIsFeatureEnabled, { + distinctId: 'user-123', + flagKey: 'test-flag', + }) - const result = await t.action(api.example.testIsFeatureEnabled, { - distinctId: 'user-123', - flagKey: 'missing', + expect(result).toEqual({ flagKey: 'test-flag', enabled: true }) }) - expect(result).toEqual({ flagKey: 'missing', enabled: null }) - }) -}) + test('returns null for non-existent flag', async () => { + global.fetch = mockFetch(flagsResponse()) + const t = initConvexTest() -describe('getFeatureFlagPayload', () => { - beforeEach(() => { - process.env.POSTHOG_API_KEY = 'phc_test_key' - process.env.POSTHOG_HOST = 'https://test.posthog.com' - }) - - afterEach(() => { - global.fetch = originalFetch - delete process.env.POSTHOG_API_KEY - delete process.env.POSTHOG_HOST - fetchCalls = [] - }) + const result = await t.action(api.example.testIsFeatureEnabled, { + distinctId: 'user-123', + flagKey: 'missing', + }) - test('returns payload for flag', async () => { - global.fetch = mockFetch(flagsResponse({ 'test-flag': true }, { 'test-flag': { key: 'value' } })) - const t = initConvexTest() + expect(result).toEqual({ flagKey: 'missing', enabled: null }) + }) +}) - const result = await t.action(api.example.testGetFeatureFlagPayload, { - distinctId: 'user-123', - flagKey: 'test-flag', +describe('getFeatureFlagPayload', () => { + beforeEach(() => { + process.env.POSTHOG_API_KEY = 'phc_test_key' + process.env.POSTHOG_HOST = 'https://test.posthog.com' }) - expect(result).toEqual({ - flagKey: 'test-flag', - payload: { key: 'value' }, + afterEach(() => { + global.fetch = originalFetch + delete process.env.POSTHOG_API_KEY + delete process.env.POSTHOG_HOST + fetchCalls = [] }) - }) - test('returns null when no payload exists', async () => { - global.fetch = mockFetch(flagsResponse({ 'test-flag': true })) - const t = initConvexTest() + test('returns payload for flag', async () => { + global.fetch = mockFetch(flagsResponse({ 'test-flag': true }, { 'test-flag': { key: 'value' } })) + const t = initConvexTest() + + const result = await t.action(api.example.testGetFeatureFlagPayload, { + distinctId: 'user-123', + flagKey: 'test-flag', + }) - const result = await t.action(api.example.testGetFeatureFlagPayload, { - distinctId: 'user-123', - flagKey: 'test-flag', + expect(result).toEqual({ + flagKey: 'test-flag', + payload: { key: 'value' }, + }) }) - expect(result).toEqual({ flagKey: 'test-flag', payload: null }) - }) + test('returns null when no payload exists', async () => { + global.fetch = mockFetch(flagsResponse({ 'test-flag': true })) + const t = initConvexTest() - test('accepts matchValue parameter', async () => { - global.fetch = mockFetch(flagsResponse({ 'test-flag': 'variant-a' }, { 'test-flag': 'payload-data' })) - const t = initConvexTest() + const result = await t.action(api.example.testGetFeatureFlagPayload, { + distinctId: 'user-123', + flagKey: 'test-flag', + }) - const result = await t.action(api.example.testGetFeatureFlagPayload, { - distinctId: 'user-123', - flagKey: 'test-flag', - matchValue: 'variant-a', + expect(result).toEqual({ flagKey: 'test-flag', payload: null }) }) - expect(result.flagKey).toBe('test-flag') - expect(result.payload).toBe('payload-data') - }) + test('accepts matchValue parameter', async () => { + global.fetch = mockFetch(flagsResponse({ 'test-flag': 'variant-a' }, { 'test-flag': 'payload-data' })) + const t = initConvexTest() + + const result = await t.action(api.example.testGetFeatureFlagPayload, { + distinctId: 'user-123', + flagKey: 'test-flag', + matchValue: 'variant-a', + }) + + expect(result.flagKey).toBe('test-flag') + expect(result.payload).toBe('payload-data') + }) }) describe('getFeatureFlagResult', () => { - beforeEach(() => { - process.env.POSTHOG_API_KEY = 'phc_test_key' - process.env.POSTHOG_HOST = 'https://test.posthog.com' - }) - - afterEach(() => { - global.fetch = originalFetch - delete process.env.POSTHOG_API_KEY - delete process.env.POSTHOG_HOST - fetchCalls = [] - }) - - test('returns full result with variant and payload', async () => { - global.fetch = mockFetch(flagsResponse({ 'test-flag': 'variant-a' }, { 'test-flag': { config: true } })) - const t = initConvexTest() + beforeEach(() => { + process.env.POSTHOG_API_KEY = 'phc_test_key' + process.env.POSTHOG_HOST = 'https://test.posthog.com' + }) - const result = await t.action(api.example.testGetFeatureFlagResult, { - distinctId: 'user-123', - flagKey: 'test-flag', + afterEach(() => { + global.fetch = originalFetch + delete process.env.POSTHOG_API_KEY + delete process.env.POSTHOG_HOST + fetchCalls = [] }) - expect(result.flagKey).toBe('test-flag') - expect(result.result).not.toBeNull() - expect(result.result!.key).toBe('test-flag') - expect(result.result!.enabled).toBe(true) - expect(result.result!.variant).toBe('variant-a') - expect(result.result!.payload).toEqual({ config: true }) - }) + test('returns full result with variant and payload', async () => { + global.fetch = mockFetch(flagsResponse({ 'test-flag': 'variant-a' }, { 'test-flag': { config: true } })) + const t = initConvexTest() - test('returns null for non-existent flag', async () => { - global.fetch = mockFetch(flagsResponse()) - const t = initConvexTest() + const result = await t.action(api.example.testGetFeatureFlagResult, { + distinctId: 'user-123', + flagKey: 'test-flag', + }) - const result = await t.action(api.example.testGetFeatureFlagResult, { - distinctId: 'user-123', - flagKey: 'missing', + expect(result.flagKey).toBe('test-flag') + expect(result.result).not.toBeNull() + expect(result.result!.key).toBe('test-flag') + expect(result.result!.enabled).toBe(true) + expect(result.result!.variant).toBe('variant-a') + expect(result.result!.payload).toEqual({ config: true }) }) - expect(result).toEqual({ flagKey: 'missing', result: null }) - }) -}) + test('returns null for non-existent flag', async () => { + global.fetch = mockFetch(flagsResponse()) + const t = initConvexTest() -describe('getAllFlags', () => { - beforeEach(() => { - process.env.POSTHOG_API_KEY = 'phc_test_key' - process.env.POSTHOG_HOST = 'https://test.posthog.com' - }) - - afterEach(() => { - global.fetch = originalFetch - delete process.env.POSTHOG_API_KEY - delete process.env.POSTHOG_HOST - fetchCalls = [] - }) + const result = await t.action(api.example.testGetFeatureFlagResult, { + distinctId: 'user-123', + flagKey: 'missing', + }) - test('returns all flags', async () => { - global.fetch = mockFetch( - flagsResponse({ - 'flag-a': true, - 'flag-b': 'variant-1', - 'flag-c': false, - }) - ) - const t = initConvexTest() + expect(result).toEqual({ flagKey: 'missing', result: null }) + }) +}) - const result = await t.action(api.example.testGetAllFlags, { - distinctId: 'user-123', +describe('getAllFlags', () => { + beforeEach(() => { + process.env.POSTHOG_API_KEY = 'phc_test_key' + process.env.POSTHOG_HOST = 'https://test.posthog.com' }) - expect(result.flags).toEqual({ - 'flag-a': true, - 'flag-b': 'variant-1', - 'flag-c': false, + afterEach(() => { + global.fetch = originalFetch + delete process.env.POSTHOG_API_KEY + delete process.env.POSTHOG_HOST + fetchCalls = [] }) - }) - test('sends groups and properties to /flags', async () => { - global.fetch = mockFetch(flagsResponse({ 'test-flag': true })) - const t = initConvexTest() + test('returns all flags', async () => { + global.fetch = mockFetch( + flagsResponse({ + 'flag-a': true, + 'flag-b': 'variant-1', + 'flag-c': false, + }) + ) + const t = initConvexTest() - await t.action(api.example.testGetAllFlags, { - distinctId: 'user-123', - groups: { company: 'acme' }, - personProperties: { plan: 'pro' }, - groupProperties: { company: { size: '100' } }, + const result = await t.action(api.example.testGetAllFlags, { + distinctId: 'user-123', + }) + + expect(result.flags).toEqual({ + 'flag-a': true, + 'flag-b': 'variant-1', + 'flag-c': false, + }) }) - const body = flagsCalls()[0].body as Record - expect(body.groups).toEqual({ company: 'acme' }) - expect(body.person_properties).toMatchObject({ plan: 'pro' }) - expect(body.group_properties).toMatchObject({ company: { size: '100' } }) - }) + test('sends groups and properties to /flags', async () => { + global.fetch = mockFetch(flagsResponse({ 'test-flag': true })) + const t = initConvexTest() - test('accepts flagKeys filter', async () => { - global.fetch = mockFetch(flagsResponse({ 'flag-a': true })) - const t = initConvexTest() + await t.action(api.example.testGetAllFlags, { + distinctId: 'user-123', + groups: { company: 'acme' }, + personProperties: { plan: 'pro' }, + groupProperties: { company: { size: '100' } }, + }) - const result = await t.action(api.example.testGetAllFlags, { - distinctId: 'user-123', - flagKeys: ['flag-a', 'flag-b'], + const body = flagsCalls()[0].body as Record + expect(body.groups).toEqual({ company: 'acme' }) + expect(body.person_properties).toMatchObject({ plan: 'pro' }) + expect(body.group_properties).toMatchObject({ company: { size: '100' } }) }) - expect(result.flags).toBeDefined() - }) -}) - -describe('getAllFlagsAndPayloads', () => { - beforeEach(() => { - process.env.POSTHOG_API_KEY = 'phc_test_key' - process.env.POSTHOG_HOST = 'https://test.posthog.com' - }) - - afterEach(() => { - global.fetch = originalFetch - delete process.env.POSTHOG_API_KEY - delete process.env.POSTHOG_HOST - fetchCalls = [] - }) + test('accepts flagKeys filter', async () => { + global.fetch = mockFetch(flagsResponse({ 'flag-a': true })) + const t = initConvexTest() - test('returns flags and payloads', async () => { - global.fetch = mockFetch(flagsResponse({ 'flag-a': true, 'flag-b': 'variant' }, { 'flag-a': { config: 'value' } })) - const t = initConvexTest() + const result = await t.action(api.example.testGetAllFlags, { + distinctId: 'user-123', + flagKeys: ['flag-a', 'flag-b'], + }) - const result = await t.action(api.example.testGetAllFlagsAndPayloads, { - distinctId: 'user-123', + expect(result.flags).toBeDefined() }) +}) - expect(result.featureFlags).toEqual({ - 'flag-a': true, - 'flag-b': 'variant', +describe('getAllFlagsAndPayloads', () => { + beforeEach(() => { + process.env.POSTHOG_API_KEY = 'phc_test_key' + process.env.POSTHOG_HOST = 'https://test.posthog.com' }) - expect(result.featureFlagPayloads).toEqual({ - 'flag-a': { config: 'value' }, + + afterEach(() => { + global.fetch = originalFetch + delete process.env.POSTHOG_API_KEY + delete process.env.POSTHOG_HOST + fetchCalls = [] }) - }) - test('accepts flagKeys filter', async () => { - global.fetch = mockFetch(flagsResponse({ 'flag-a': true }, { 'flag-a': 'payload' })) - const t = initConvexTest() + test('returns flags and payloads', async () => { + global.fetch = mockFetch( + flagsResponse({ 'flag-a': true, 'flag-b': 'variant' }, { 'flag-a': { config: 'value' } }) + ) + const t = initConvexTest() - const result = await t.action(api.example.testGetAllFlagsAndPayloads, { - distinctId: 'user-123', - flagKeys: ['flag-a'], + const result = await t.action(api.example.testGetAllFlagsAndPayloads, { + distinctId: 'user-123', + }) + + expect(result.featureFlags).toEqual({ + 'flag-a': true, + 'flag-b': 'variant', + }) + expect(result.featureFlagPayloads).toEqual({ + 'flag-a': { config: 'value' }, + }) }) - expect(result.featureFlags).toBeDefined() - expect(result.featureFlagPayloads).toBeDefined() - }) + test('accepts flagKeys filter', async () => { + global.fetch = mockFetch(flagsResponse({ 'flag-a': true }, { 'flag-a': 'payload' })) + const t = initConvexTest() + + const result = await t.action(api.example.testGetAllFlagsAndPayloads, { + distinctId: 'user-123', + flagKeys: ['flag-a'], + }) + + expect(result.featureFlags).toBeDefined() + expect(result.featureFlagPayloads).toBeDefined() + }) }) diff --git a/examples/example-convex/convex/example.ts b/examples/example-convex/convex/example.ts index 752cfbdbee..cb24f5a6eb 100644 --- a/examples/example-convex/convex/example.ts +++ b/examples/example-convex/convex/example.ts @@ -8,242 +8,242 @@ import { v } from 'convex/values' // user is not signed in. export const testCapture = mutation({ - args: { - distinctId: v.optional(v.string()), - event: v.string(), - properties: v.optional(v.any()), - groups: v.optional(v.any()), - sendFeatureFlags: v.optional(v.boolean()), - timestamp: v.optional(v.string()), - uuid: v.optional(v.string()), - disableGeoip: v.optional(v.boolean()), - }, - handler: async (ctx, args) => { - await posthog.capture(ctx, { - distinctId: args.distinctId, - event: args.event, - properties: args.properties, - groups: args.groups, - sendFeatureFlags: args.sendFeatureFlags, - timestamp: args.timestamp ? new Date(args.timestamp) : undefined, - uuid: args.uuid || undefined, - disableGeoip: args.disableGeoip, - }) - return { success: true } - }, + args: { + distinctId: v.optional(v.string()), + event: v.string(), + properties: v.optional(v.any()), + groups: v.optional(v.any()), + sendFeatureFlags: v.optional(v.boolean()), + timestamp: v.optional(v.string()), + uuid: v.optional(v.string()), + disableGeoip: v.optional(v.boolean()), + }, + handler: async (ctx, args) => { + await posthog.capture(ctx, { + distinctId: args.distinctId, + event: args.event, + properties: args.properties, + groups: args.groups, + sendFeatureFlags: args.sendFeatureFlags, + timestamp: args.timestamp ? new Date(args.timestamp) : undefined, + uuid: args.uuid || undefined, + disableGeoip: args.disableGeoip, + }) + return { success: true } + }, }) export const testIdentify = mutation({ - args: { - distinctId: v.optional(v.string()), - properties: v.optional(v.any()), - disableGeoip: v.optional(v.boolean()), - }, - handler: async (ctx, args) => { - await posthog.identify(ctx, { - distinctId: args.distinctId, - properties: args.properties, - disableGeoip: args.disableGeoip, - }) - return { success: true } - }, + args: { + distinctId: v.optional(v.string()), + properties: v.optional(v.any()), + disableGeoip: v.optional(v.boolean()), + }, + handler: async (ctx, args) => { + await posthog.identify(ctx, { + distinctId: args.distinctId, + properties: args.properties, + disableGeoip: args.disableGeoip, + }) + return { success: true } + }, }) export const testGroupIdentify = mutation({ - args: { - groupType: v.string(), - groupKey: v.string(), - properties: v.optional(v.any()), - distinctId: v.optional(v.string()), - disableGeoip: v.optional(v.boolean()), - }, - handler: async (ctx, args) => { - await posthog.groupIdentify(ctx, { - groupType: args.groupType, - groupKey: args.groupKey, - properties: args.properties, - distinctId: args.distinctId || undefined, - disableGeoip: args.disableGeoip, - }) - return { success: true } - }, + args: { + groupType: v.string(), + groupKey: v.string(), + properties: v.optional(v.any()), + distinctId: v.optional(v.string()), + disableGeoip: v.optional(v.boolean()), + }, + handler: async (ctx, args) => { + await posthog.groupIdentify(ctx, { + groupType: args.groupType, + groupKey: args.groupKey, + properties: args.properties, + distinctId: args.distinctId || undefined, + disableGeoip: args.disableGeoip, + }) + return { success: true } + }, }) export const testAlias = mutation({ - args: { - distinctId: v.optional(v.string()), - alias: v.string(), - disableGeoip: v.optional(v.boolean()), - }, - handler: async (ctx, args) => { - await posthog.alias(ctx, { - distinctId: args.distinctId, - alias: args.alias, - disableGeoip: args.disableGeoip, - }) - return { success: true } - }, + args: { + distinctId: v.optional(v.string()), + alias: v.string(), + disableGeoip: v.optional(v.boolean()), + }, + handler: async (ctx, args) => { + await posthog.alias(ctx, { + distinctId: args.distinctId, + alias: args.alias, + disableGeoip: args.disableGeoip, + }) + return { success: true } + }, }) export const testCaptureException = mutation({ - args: { - errorMessage: v.string(), - errorType: v.optional(v.union(v.literal('error'), v.literal('string'), v.literal('object'))), - distinctId: v.optional(v.string()), - additionalProperties: v.optional(v.any()), - }, - handler: async (ctx, args) => { - let error: unknown - switch (args.errorType ?? 'error') { - case 'error': - error = new Error(args.errorMessage) - break - case 'string': - error = args.errorMessage - break - case 'object': - error = { message: args.errorMessage } - break - } - - await posthog.captureException(ctx, { - error, - distinctId: args.distinctId || undefined, - additionalProperties: args.additionalProperties, - }) - return { success: true } - }, + args: { + errorMessage: v.string(), + errorType: v.optional(v.union(v.literal('error'), v.literal('string'), v.literal('object'))), + distinctId: v.optional(v.string()), + additionalProperties: v.optional(v.any()), + }, + handler: async (ctx, args) => { + let error: unknown + switch (args.errorType ?? 'error') { + case 'error': + error = new Error(args.errorMessage) + break + case 'string': + error = args.errorMessage + break + case 'object': + error = { message: args.errorMessage } + break + } + + await posthog.captureException(ctx, { + error, + distinctId: args.distinctId || undefined, + additionalProperties: args.additionalProperties, + }) + return { success: true } + }, }) export const testThrowError = mutation({ - args: { - errorMessage: v.string(), - }, - handler: async (_ctx, args) => { - throw new Error(args.errorMessage) - }, + args: { + errorMessage: v.string(), + }, + handler: async (_ctx, args) => { + throw new Error(args.errorMessage) + }, }) // --- Feature flag methods (actions) --- const featureFlagArgs = { - distinctId: v.optional(v.string()), - flagKey: v.string(), - groups: v.optional(v.any()), - personProperties: v.optional(v.any()), - groupProperties: v.optional(v.any()), - sendFeatureFlagEvents: v.optional(v.boolean()), - disableGeoip: v.optional(v.boolean()), + distinctId: v.optional(v.string()), + flagKey: v.string(), + groups: v.optional(v.any()), + personProperties: v.optional(v.any()), + groupProperties: v.optional(v.any()), + sendFeatureFlagEvents: v.optional(v.boolean()), + disableGeoip: v.optional(v.boolean()), } function featureFlagOptions(args: { - groups?: unknown - personProperties?: unknown - groupProperties?: unknown - sendFeatureFlagEvents?: boolean - disableGeoip?: boolean + groups?: unknown + personProperties?: unknown + groupProperties?: unknown + sendFeatureFlagEvents?: boolean + disableGeoip?: boolean }) { - return { - groups: args.groups as Record | undefined, - personProperties: args.personProperties as Record | undefined, - groupProperties: args.groupProperties as Record> | undefined, - sendFeatureFlagEvents: args.sendFeatureFlagEvents, - disableGeoip: args.disableGeoip, - } + return { + groups: args.groups as Record | undefined, + personProperties: args.personProperties as Record | undefined, + groupProperties: args.groupProperties as Record> | undefined, + sendFeatureFlagEvents: args.sendFeatureFlagEvents, + disableGeoip: args.disableGeoip, + } } export const testGetFeatureFlag = action({ - args: featureFlagArgs, - handler: async (ctx, args) => { - const value = await posthog.getFeatureFlag(ctx, { - key: args.flagKey, - distinctId: args.distinctId, - ...featureFlagOptions(args), - }) - return { flagKey: args.flagKey, value } - }, + args: featureFlagArgs, + handler: async (ctx, args) => { + const value = await posthog.getFeatureFlag(ctx, { + key: args.flagKey, + distinctId: args.distinctId, + ...featureFlagOptions(args), + }) + return { flagKey: args.flagKey, value } + }, }) export const testIsFeatureEnabled = action({ - args: featureFlagArgs, - handler: async (ctx, args) => { - const enabled = await posthog.isFeatureEnabled(ctx, { - key: args.flagKey, - distinctId: args.distinctId, - ...featureFlagOptions(args), - }) - return { flagKey: args.flagKey, enabled } - }, + args: featureFlagArgs, + handler: async (ctx, args) => { + const enabled = await posthog.isFeatureEnabled(ctx, { + key: args.flagKey, + distinctId: args.distinctId, + ...featureFlagOptions(args), + }) + return { flagKey: args.flagKey, enabled } + }, }) export const testGetFeatureFlagPayload = action({ - args: { - ...featureFlagArgs, - matchValue: v.optional(v.union(v.boolean(), v.string())), - }, - handler: async (ctx, args) => { - const payload = await posthog.getFeatureFlagPayload(ctx, { - key: args.flagKey, - distinctId: args.distinctId, - matchValue: args.matchValue, - ...featureFlagOptions(args), - }) - return { flagKey: args.flagKey, payload } - }, + args: { + ...featureFlagArgs, + matchValue: v.optional(v.union(v.boolean(), v.string())), + }, + handler: async (ctx, args) => { + const payload = await posthog.getFeatureFlagPayload(ctx, { + key: args.flagKey, + distinctId: args.distinctId, + matchValue: args.matchValue, + ...featureFlagOptions(args), + }) + return { flagKey: args.flagKey, payload } + }, }) export const testGetFeatureFlagResult = action({ - args: featureFlagArgs, - handler: async (ctx, args) => { - const result = await posthog.getFeatureFlagResult(ctx, { - key: args.flagKey, - distinctId: args.distinctId, - ...featureFlagOptions(args), - }) - return { flagKey: args.flagKey, result } - }, + args: featureFlagArgs, + handler: async (ctx, args) => { + const result = await posthog.getFeatureFlagResult(ctx, { + key: args.flagKey, + distinctId: args.distinctId, + ...featureFlagOptions(args), + }) + return { flagKey: args.flagKey, result } + }, }) export const testGetAllFlags = action({ - args: { - distinctId: v.optional(v.string()), - groups: v.optional(v.any()), - personProperties: v.optional(v.any()), - groupProperties: v.optional(v.any()), - disableGeoip: v.optional(v.boolean()), - flagKeys: v.optional(v.array(v.string())), - }, - handler: async (ctx, args) => { - const flags = await posthog.getAllFlags(ctx, { - distinctId: args.distinctId, - groups: args.groups as Record | undefined, - personProperties: args.personProperties as Record | undefined, - groupProperties: args.groupProperties as Record> | undefined, - disableGeoip: args.disableGeoip, - flagKeys: args.flagKeys, - }) - return { flags } - }, + args: { + distinctId: v.optional(v.string()), + groups: v.optional(v.any()), + personProperties: v.optional(v.any()), + groupProperties: v.optional(v.any()), + disableGeoip: v.optional(v.boolean()), + flagKeys: v.optional(v.array(v.string())), + }, + handler: async (ctx, args) => { + const flags = await posthog.getAllFlags(ctx, { + distinctId: args.distinctId, + groups: args.groups as Record | undefined, + personProperties: args.personProperties as Record | undefined, + groupProperties: args.groupProperties as Record> | undefined, + disableGeoip: args.disableGeoip, + flagKeys: args.flagKeys, + }) + return { flags } + }, }) export const testGetAllFlagsAndPayloads = action({ - args: { - distinctId: v.optional(v.string()), - groups: v.optional(v.any()), - personProperties: v.optional(v.any()), - groupProperties: v.optional(v.any()), - disableGeoip: v.optional(v.boolean()), - flagKeys: v.optional(v.array(v.string())), - }, - handler: async (ctx, args) => { - const result = await posthog.getAllFlagsAndPayloads(ctx, { - distinctId: args.distinctId, - groups: args.groups as Record | undefined, - personProperties: args.personProperties as Record | undefined, - groupProperties: args.groupProperties as Record> | undefined, - disableGeoip: args.disableGeoip, - flagKeys: args.flagKeys, - }) - return result - }, + args: { + distinctId: v.optional(v.string()), + groups: v.optional(v.any()), + personProperties: v.optional(v.any()), + groupProperties: v.optional(v.any()), + disableGeoip: v.optional(v.boolean()), + flagKeys: v.optional(v.array(v.string())), + }, + handler: async (ctx, args) => { + const result = await posthog.getAllFlagsAndPayloads(ctx, { + distinctId: args.distinctId, + groups: args.groups as Record | undefined, + personProperties: args.personProperties as Record | undefined, + groupProperties: args.groupProperties as Record> | undefined, + disableGeoip: args.disableGeoip, + flagKeys: args.flagKeys, + }) + return result + }, }) diff --git a/examples/example-convex/convex/polyfills.ts b/examples/example-convex/convex/polyfills.ts new file mode 100644 index 0000000000..9c3f87bc09 --- /dev/null +++ b/examples/example-convex/convex/polyfills.ts @@ -0,0 +1,14 @@ +// Convex runs in a V8 isolate that may not provide globals that +// @opentelemetry/core expects at module evaluation time. This file +// must be imported before any OTEL module. + +// polyfill performance without using node:perf_hooks +if (typeof performance === 'undefined') { + class MockPerformance { + timeOrigin = Date.now() + now() { + return Date.now() - this.timeOrigin + } + } + globalThis.performance = new MockPerformance() as unknown as typeof performance +} diff --git a/examples/example-convex/convex/posthog.ts b/examples/example-convex/convex/posthog.ts index 4ae069c651..cadde635a5 100644 --- a/examples/example-convex/convex/posthog.ts +++ b/examples/example-convex/convex/posthog.ts @@ -2,20 +2,20 @@ import { PostHog } from '@posthog/convex' import { components } from './_generated/api' export const posthog = new PostHog(components.posthog, { - // Automatically resolve the current user's identity from Convex auth. - // Falls back to an explicit distinctId if the user is not signed in. - identify: async (ctx) => { - const identity = await ctx.auth?.getUserIdentity() - if (!identity) return null - return { distinctId: identity.subject } - }, - beforeSend: (event) => { - return { - ...event, - properties: { - ...event.properties, - environment: 'example-app', - }, - } - }, + // Automatically resolve the current user's identity from Convex auth. + // Falls back to an explicit distinctId if the user is not signed in. + identify: async (ctx) => { + const identity = await ctx.auth?.getUserIdentity() + if (!identity) return null + return { distinctId: identity.subject } + }, + beforeSend: (event) => { + return { + ...event, + properties: { + ...event.properties, + environment: 'example-app', + }, + } + }, }) diff --git a/examples/example-convex/convex/schema.ts b/examples/example-convex/convex/schema.ts index db2876cea2..92da8b35cf 100644 --- a/examples/example-convex/convex/schema.ts +++ b/examples/example-convex/convex/schema.ts @@ -1,5 +1,5 @@ import { defineSchema } from 'convex/server' export default defineSchema({ - // Any tables used by the example app go here. + // Any tables used by the example app go here. }) diff --git a/examples/example-convex/convex/setup.test.ts b/examples/example-convex/convex/setup.test.ts index 94bbab526c..a7c11ade9a 100644 --- a/examples/example-convex/convex/setup.test.ts +++ b/examples/example-convex/convex/setup.test.ts @@ -8,9 +8,9 @@ const modules = import.meta.glob('./**/*.*s') // When users want to write tests that use your component, they need to // explicitly register it with its schema and modules. export function initConvexTest() { - const t = convexTest(schema, modules) - component.register(t) - return t + const t = convexTest(schema, modules) + component.register(t) + return t } test('setup', () => {}) diff --git a/examples/example-convex/convex/tsconfig.json b/examples/example-convex/convex/tsconfig.json index fb5930d17a..20f1d90377 100644 --- a/examples/example-convex/convex/tsconfig.json +++ b/examples/example-convex/convex/tsconfig.json @@ -1,25 +1,25 @@ { - /* This TypeScript project config describes the environment that - * Convex functions run in and is used to typecheck them. - * You can modify it, but some settings are required to use Convex. - */ - "compilerOptions": { - /* These settings are not required by Convex and can be modified. */ - "allowJs": true, - "strict": true, - "moduleResolution": "Bundler", - "jsx": "react-jsx", - "skipLibCheck": true, - "allowSyntheticDefaultImports": true, + /* This TypeScript project config describes the environment that + * Convex functions run in and is used to typecheck them. + * You can modify it, but some settings are required to use Convex. + */ + "compilerOptions": { + /* These settings are not required by Convex and can be modified. */ + "allowJs": true, + "strict": true, + "moduleResolution": "Bundler", + "jsx": "react-jsx", + "skipLibCheck": true, + "allowSyntheticDefaultImports": true, - /* These compiler options are required by Convex */ - "target": "ESNext", - "lib": ["ES2021", "dom"], - "forceConsistentCasingInFileNames": true, - "module": "ESNext", - "isolatedModules": true, - "noEmit": true - }, - "include": ["./**/*"], - "exclude": ["./_generated", "./**/*.test.ts"] + /* These compiler options are required by Convex */ + "target": "ESNext", + "lib": ["ES2021", "dom"], + "forceConsistentCasingInFileNames": true, + "module": "ESNext", + "isolatedModules": true, + "noEmit": true + }, + "include": ["./**/*"], + "exclude": ["./_generated", "./**/*.test.ts"] } diff --git a/examples/example-convex/package.json b/examples/example-convex/package.json index 405d82ced9..1770da57aa 100644 --- a/examples/example-convex/package.json +++ b/examples/example-convex/package.json @@ -12,9 +12,10 @@ "dependencies": { "@ai-sdk/openai": "^2.0.98", "@convex-dev/agent": "^0.3.2", - "@opentelemetry/exporter-trace-otlp-http": "^0.213.0", + "@opentelemetry/api": "^1.9.0", + "@opentelemetry/exporter-trace-otlp-http": "^0.214.0", "@opentelemetry/resources": "^2.6.0", - "@opentelemetry/sdk-node": "^0.213.0", + "@opentelemetry/sdk-trace-base": "^2.6.0", "@posthog/ai": "^7.9.5", "@posthog/convex": "*", "ai": "^5.0.150", diff --git a/examples/example-convex/pnpm-lock.yaml b/examples/example-convex/pnpm-lock.yaml index e6a4f8f578..9d9a476290 100644 --- a/examples/example-convex/pnpm-lock.yaml +++ b/examples/example-convex/pnpm-lock.yaml @@ -16,18 +16,21 @@ importers: '@convex-dev/agent': specifier: ^0.3.2 version: 0.3.2(@ai-sdk/provider-utils@3.0.22(zod@4.3.6))(ai@5.0.151(zod@4.3.6))(convex-helpers@0.1.114(@standard-schema/spec@1.1.0)(convex@1.32.0(react@19.2.4))(react@19.2.4)(typescript@5.9.3)(zod@4.3.6))(convex@1.32.0(react@19.2.4))(react@19.2.4) + '@opentelemetry/api': + specifier: ^1.9.0 + version: 1.9.0 '@opentelemetry/exporter-trace-otlp-http': - specifier: ^0.213.0 - version: 0.213.0(@opentelemetry/api@1.9.0) + specifier: ^0.214.0 + version: 0.214.0(@opentelemetry/api@1.9.0) '@opentelemetry/resources': specifier: ^2.6.0 version: 2.6.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-node': - specifier: ^0.213.0 - version: 0.213.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': + specifier: ^2.6.0 + version: 2.6.0(@opentelemetry/api@1.9.0) '@posthog/ai': specifier: file:../../target/posthog-ai.tgz - version: file:../../target/posthog-ai.tgz(@ai-sdk/provider@2.0.1)(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.213.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0))(posthog-node@file:../../target/posthog-node.tgz)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(ws@8.19.0) + version: file:../../target/posthog-ai.tgz(@ai-sdk/provider@2.0.1)(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-http@0.214.0(@opentelemetry/api@1.9.0))(@opentelemetry/exporter-trace-otlp-proto@0.213.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0))(posthog-node@file:../../target/posthog-node.tgz)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(ws@8.19.0) '@posthog/convex': specifier: file:../../target/posthog-convex.tgz version: file:../../target/posthog-convex.tgz(convex@1.32.0(react@19.2.4)) @@ -93,8 +96,8 @@ packages: resolution: {integrity: sha512-KCUwswvsC5VsW2PWFqF8eJgSCu5Ysj7m1TxiHTVA6g7k360bk0RNQENT8KTMAYEs+8fWPD3Uu4dEmzGHc+jGng==} engines: {node: '>=18'} - '@anthropic-ai/sdk@0.74.0': - resolution: {integrity: sha512-srbJV7JKsc5cQ6eVuFzjZO7UR3xEPJqPamHFIe29bs38Ij2IripoAhC0S5NslNbaFUYqBKypmmpzMTpqfHEUDw==} + '@anthropic-ai/sdk@0.78.0': + resolution: {integrity: sha512-PzQhR715td/m1UaaN5hHXjYB8Gl2lF9UVhrrGrZeysiF6Rb74Wc9GCB8hzLdzmQtBd1qe89F9OptgB9Za1Ib5w==} hasBin: true peerDependencies: zod: ^3.25.0 || ^4.0.0 @@ -525,15 +528,6 @@ packages: '@modelcontextprotocol/sdk': optional: true - '@grpc/grpc-js@1.14.3': - resolution: {integrity: sha512-Iq8QQQ/7X3Sac15oB6p0FmUg/klxQvXLeileoqrTRGJYLV+/9tubbr9ipz0GKHjmXVsgFPo/+W+2cA8eNcR+XA==} - engines: {node: '>=12.10.0'} - - '@grpc/proto-loader@0.8.0': - resolution: {integrity: sha512-rc1hOQtjIWGxcxpb9aHAfLpIctjEnsDehj0DAiVfBlmT84uvR0uUtN2hEi/ecvWVjXUGf5qPF4qEgiLOx1YIMQ==} - engines: {node: '>=6'} - hasBin: true - '@isaacs/cliui@8.0.2': resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} @@ -554,9 +548,6 @@ packages: '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} - '@js-sdsl/ordered-map@4.4.2': - resolution: {integrity: sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==} - '@langchain/core@1.1.31': resolution: {integrity: sha512-FxsgIUONjKaRpjx59sISgmb0OMCbAetPGyhzjGa2kX0y1f8LZ5xm9VB2db7W9HYWyLvzRWcMA51Uu4OSTJmtZQ==} engines: {node: '>=20'} @@ -596,78 +587,28 @@ packages: resolution: {integrity: sha512-zRM5/Qj6G84Ej3F1yt33xBVY/3tnMxtL1fiDIxYbDWYaZ/eudVw3/PBiZ8G7JwUxXxjW8gU4g6LnOyfGKYHYgw==} engines: {node: '>=8.0.0'} + '@opentelemetry/api-logs@0.214.0': + resolution: {integrity: sha512-40lSJeqYO8Uz2Yj7u94/SJWE/wONa7rmMKjI1ZcIjgf3MHNHv1OZUCrCETGuaRF62d5pQD1wKIW+L4lmSMTzZA==} + engines: {node: '>=8.0.0'} + '@opentelemetry/api@1.9.0': resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==} engines: {node: '>=8.0.0'} - '@opentelemetry/configuration@0.213.0': - resolution: {integrity: sha512-MfVgZiUuwL1d3bPPvXcEkVHGTGNUGoqGK97lfwBuRoKttcVGGqDyxTCCVa5MGbirtBQkUTysXMBUVWPaq7zbWw==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.9.0 - - '@opentelemetry/context-async-hooks@2.6.0': - resolution: {integrity: sha512-L8UyDwqpTcbkIK5cgwDRDYDoEhQoj8wp8BwsO19w3LB1Z41yEQm2VJyNfAi9DrLP/YTqXqWpKHyZfR9/tFYo1Q==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': '>=1.0.0 <1.10.0' - '@opentelemetry/core@2.6.0': resolution: {integrity: sha512-HLM1v2cbZ4TgYN6KEOj+Bbj8rAKriOdkF9Ed3tG25FoprSiQl7kYc+RRT6fUZGOvx0oMi5U67GoFdT+XUn8zEg==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': '>=1.0.0 <1.10.0' - '@opentelemetry/exporter-logs-otlp-grpc@0.213.0': - resolution: {integrity: sha512-QiRZzvayEOFnenSXi85Eorgy5WTqyNQ+E7gjl6P6r+W3IUIwAIH8A9/BgMWfP056LwmdrBL6+qvnwaIEmug6Yg==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - - '@opentelemetry/exporter-logs-otlp-http@0.213.0': - resolution: {integrity: sha512-vqDVSpLp09ZzcFIdb7QZrEFPxUlO3GzdhBKLstq3jhYB5ow3+ZtV5V0ngSdi/0BZs+J5WPiN1+UDV4X5zD/GzA==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - - '@opentelemetry/exporter-logs-otlp-proto@0.213.0': - resolution: {integrity: sha512-gQk41nqfK3KhDk8jbSo3LR/fQBlV7f6Q5xRcfDmL1hZlbgXQPdVFV9/rIfYUrCoq1OM+2NnKnFfGjBt6QpLSsA==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - - '@opentelemetry/exporter-metrics-otlp-grpc@0.213.0': - resolution: {integrity: sha512-Z8gYKUAU48qwm+a1tjnGv9xbE7a5lukVIwgF6Z5i3VPXPVMe4Sjra0nN3zU7m277h+V+ZpsPGZJ2Xf0OTkL7/w==} + '@opentelemetry/core@2.6.1': + resolution: {integrity: sha512-8xHSGWpJP9wBxgBpnqGL0R3PbdWQndL1Qp50qrg71+B28zK5OQmUgcDKLJgzyAAV38t4tOyLMGDD60LneR5W8g==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: - '@opentelemetry/api': ^1.3.0 - - '@opentelemetry/exporter-metrics-otlp-http@0.213.0': - resolution: {integrity: sha512-yw3fTIw4KQIRXC/ZyYQq5gtA3Ogfdfz/g5HVgleobQAcjUUE8Nj3spGMx8iQPp+S+u6/js7BixufRkXhzLmpJA==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - - '@opentelemetry/exporter-metrics-otlp-proto@0.213.0': - resolution: {integrity: sha512-geHF+zZaDb0/WRkJTxR8o8dG4fCWT/Wq7HBdNZCxwH5mxhwRi/5f37IDYH7nvU+dwU6IeY4Pg8TPI435JCiNkg==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - - '@opentelemetry/exporter-prometheus@0.213.0': - resolution: {integrity: sha512-FyV3/JfKGAgx+zJUwCHdjQHbs+YeGd2fOWvBHYrW6dmfv/w89lb8WhJTSZEoWgP525jwv/gFeBttlGu1flebdA==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - - '@opentelemetry/exporter-trace-otlp-grpc@0.213.0': - resolution: {integrity: sha512-L8y6piP4jBIIx1Nv7/9hkx25ql6/Cro/kQrs+f9e8bPF0Ar5Dm991v7PnbtubKz6Q4fT872H56QXUWVnz/Cs4Q==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.3.0 + '@opentelemetry/api': '>=1.0.0 <1.10.0' - '@opentelemetry/exporter-trace-otlp-http@0.213.0': - resolution: {integrity: sha512-tnRmJD39aWrE/Sp7F6AbRNAjKHToDkAqBi6i0lESpGWz3G+f4bhVAV6mgSXH2o18lrDVJXo6jf9bAywQw43wRA==} + '@opentelemetry/exporter-trace-otlp-http@0.214.0': + resolution: {integrity: sha512-kIN8nTBMgV2hXzV/a20BCFilPZdAIMYYJGSgfMMRm/Xa+07y5hRDS2Vm12A/z8Cdu3Sq++ZvJfElokX2rkgGgw==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 @@ -678,26 +619,14 @@ packages: peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/exporter-zipkin@2.6.0': - resolution: {integrity: sha512-AFP77OQMLfw/Jzh6WT2PtrywstNjdoyT9t9lYrYdk1s4igsvnMZ8DkZKCwxsItC01D+4Lydgrb+Wy0bAvpp8xg==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.0.0 - - '@opentelemetry/instrumentation@0.213.0': - resolution: {integrity: sha512-3i9NdkET/KvQomeh7UaR/F4r9P25Rx6ooALlWXPIjypcEOUxksCmVu0zA70NBJWlrMW1rPr/LRidFAflLI+s/w==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - '@opentelemetry/otlp-exporter-base@0.213.0': resolution: {integrity: sha512-MegxAP1/n09Ob2dQvY5NBDVjAFkZRuKtWKxYev1R2M8hrsgXzQGkaMgoEKeUOyQ0FUyYcO29UOnYdQWmWa0PXg==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/otlp-grpc-exporter-base@0.213.0': - resolution: {integrity: sha512-XgRGuLE9usFNlnw2lgMIM4HTwpcIyjdU/xPoJ8v3LbBLBfjaDkIugjc9HoWa7ZSJ/9Bhzgvm/aD0bGdYUFgnTw==} + '@opentelemetry/otlp-exporter-base@0.214.0': + resolution: {integrity: sha512-u1Gdv0/E9wP+apqWf7Wv2npXmgJtxsW2XL0TEv9FZloTZRuMBKmu8cYVXwS4Hm3q/f/3FuCnPTgiwYvIqRSpRg==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 @@ -708,20 +637,20 @@ packages: peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/propagator-b3@2.6.0': - resolution: {integrity: sha512-SguK4jMmRvQ0c0dxAMl6K+Eu1+01X0OP7RLiIuHFjOS8hlB23ZYNnhnbAdSQEh5xVXQmH0OAS0TnmVI+6vB2Kg==} + '@opentelemetry/otlp-transformer@0.214.0': + resolution: {integrity: sha512-DSaYcuBRh6uozfsWN3R8HsN0yDhCuWP7tOFdkUOVaWD1KVJg8m4qiLUsg/tNhTLS9HUYUcwNpwL2eroLtsZZ/w==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: - '@opentelemetry/api': '>=1.0.0 <1.10.0' + '@opentelemetry/api': ^1.3.0 - '@opentelemetry/propagator-jaeger@2.6.0': - resolution: {integrity: sha512-KGWJuvp9X8X36bhHgIhWEnHAzXDInFr+Fvo9IQhhuu6pXLT8mF7HzFyx/X+auZUITvPaZhM39Phj3vK12MbhwA==} + '@opentelemetry/resources@2.6.0': + resolution: {integrity: sha512-D4y/+OGe3JSuYUCBxtH5T9DSAWNcvCb/nQWIga8HNtXTVPQn59j0nTBAgaAXxUVBDl40mG3Tc76b46wPlZaiJQ==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: - '@opentelemetry/api': '>=1.0.0 <1.10.0' + '@opentelemetry/api': '>=1.3.0 <1.10.0' - '@opentelemetry/resources@2.6.0': - resolution: {integrity: sha512-D4y/+OGe3JSuYUCBxtH5T9DSAWNcvCb/nQWIga8HNtXTVPQn59j0nTBAgaAXxUVBDl40mG3Tc76b46wPlZaiJQ==} + '@opentelemetry/resources@2.6.1': + resolution: {integrity: sha512-lID/vxSuKWXM55XhAKNoYXu9Cutoq5hFdkbTdI/zDKQktXzcWBVhNsOkiZFTMU9UtEWuGRNe0HUgmsFldIdxVA==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': '>=1.3.0 <1.10.0' @@ -732,17 +661,23 @@ packages: peerDependencies: '@opentelemetry/api': '>=1.4.0 <1.10.0' + '@opentelemetry/sdk-logs@0.214.0': + resolution: {integrity: sha512-zf6acnScjhsaBUU22zXZ/sLWim1dfhUAbGXdMmHmNG3LfBnQ3DKsOCITb2IZwoUsNNMTogqFKBnlIPPftUgGwA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.4.0 <1.10.0' + '@opentelemetry/sdk-metrics@2.6.0': resolution: {integrity: sha512-CicxWZxX6z35HR83jl+PLgtFgUrKRQ9LCXyxgenMnz5A1lgYWfAog7VtdOvGkJYyQgMNPhXQwkYrDLujk7z1Iw==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': '>=1.9.0 <1.10.0' - '@opentelemetry/sdk-node@0.213.0': - resolution: {integrity: sha512-8s7SQtY8DIAjraXFrUf0+I90SBAUQbsMWMtUGKmusswRHWXtKJx42aJQMoxEtC82Csqj+IlBH6FoP8XmmUDSrQ==} + '@opentelemetry/sdk-metrics@2.6.1': + resolution: {integrity: sha512-9t9hJHX15meBy2NmTJxL+NJfXmnausR2xUDvE19XQce0Qi/GBtDGamU8nS1RMbdgDmhgpm3VaOu2+fiS/SfTpQ==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: - '@opentelemetry/api': '>=1.3.0 <1.10.0' + '@opentelemetry/api': '>=1.9.0 <1.10.0' '@opentelemetry/sdk-trace-base@2.6.0': resolution: {integrity: sha512-g/OZVkqlxllgFM7qMKqbPV9c1DUPhQ7d4n3pgZFcrnrNft9eJXZM2TNHTPYREJBrtNdRytYyvwjgL5geDKl3EQ==} @@ -750,11 +685,11 @@ packages: peerDependencies: '@opentelemetry/api': '>=1.3.0 <1.10.0' - '@opentelemetry/sdk-trace-node@2.6.0': - resolution: {integrity: sha512-YhswtasmsbIGEFvLGvR9p/y3PVRTfFf+mgY8van4Ygpnv4sA3vooAjvh+qAn9PNWxs4/IwGGqiQS0PPsaRJ0vQ==} + '@opentelemetry/sdk-trace-base@2.6.1': + resolution: {integrity: sha512-r86ut4T1e8vNwB35CqCcKd45yzqH6/6Wzvpk2/cZB8PsPLlZFTvrh8yfOS3CYZYcUmAx4hHTZJ8AO8Dj8nrdhw==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: - '@opentelemetry/api': '>=1.0.0 <1.10.0' + '@opentelemetry/api': '>=1.3.0 <1.10.0' '@opentelemetry/semantic-conventions@1.40.0': resolution: {integrity: sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw==} @@ -765,25 +700,31 @@ packages: engines: {node: '>=14'} '@posthog/ai@file:../../target/posthog-ai.tgz': - resolution: {integrity: sha512-uIk6JT8LwL/0t3WIEPp/KeSTmT+ezm432xcX5LxMkuBMlYB8EzMfsCpuTt8XD24Af5ggiQKJva34iJ698sy7ZQ==, tarball: file:../../target/posthog-ai.tgz} - version: 7.8.10 + resolution: {integrity: sha512-YS0LpDA/fIajXSVNs/GXfWcmWG9jzV1LJkuOXqfvU4Elhv+5FxTiX6aPD+nWmRi/W/9CG4CdVbHjIVMBI07w5w==, tarball: file:../../target/posthog-ai.tgz} + version: 7.12.8 engines: {node: ^20.20.0 || >=22.22.0} peerDependencies: '@ai-sdk/provider': ^2.0.0 || ^3.0.0 + '@opentelemetry/api': ^1.9.0 + '@opentelemetry/exporter-trace-otlp-http': ^0.200.0 posthog-node: ^5.0.0 peerDependenciesMeta: '@ai-sdk/provider': optional: true + '@opentelemetry/api': + optional: true + '@opentelemetry/exporter-trace-otlp-http': + optional: true '@posthog/convex@file:../../target/posthog-convex.tgz': - resolution: {integrity: sha512-ccBrzTIqMT+pq4Ixp5hVjLDUKHDdFE6DYAJYqI/HIP4nIRZlvCIf0zFbjMN+ndgX9q7xBapIHYU40ALyrWebgQ==, tarball: file:../../target/posthog-convex.tgz} - version: 0.1.8 + resolution: {integrity: sha512-Yp5Tq25hLpH2rUSehEqYwa40TUDXU9131Rq3g2EOK35+XQD8qgzsKf1+2IdYMceeMpeJf97/ssex8ODayOBUhg==, tarball: file:../../target/posthog-convex.tgz} + version: 0.1.19 peerDependencies: convex: ^1.31.7 '@posthog/core@file:../../target/posthog-core.tgz': - resolution: {integrity: sha512-iNC53tOGhUAUrhbU8gOYRBK9Czb4XUOnGiE0se/YCmQAJ4o8NtyGlY9KrOK7V1hQij+J02Cq0Kr3cDNFVi45yQ==, tarball: file:../../target/posthog-core.tgz} - version: 1.22.0 + resolution: {integrity: sha512-RTSMgbKFlXJYKXl+b2Ig00EGFT6xAbIC8t//5ov4owNYSjyR1TgBrYmd18qrfcVOVZB52ILhnhXJVV7beIzDQg==, tarball: file:../../target/posthog-core.tgz} + version: 1.24.6 '@protobufjs/aspromise@1.1.2': resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==} @@ -1007,16 +948,6 @@ packages: peerDependencies: vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 - acorn-import-attributes@1.9.5: - resolution: {integrity: sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==} - peerDependencies: - acorn: ^8 - - acorn@8.16.0: - resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==} - engines: {node: '>=0.4.0'} - hasBin: true - agent-base@7.1.4: resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} engines: {node: '>= 14'} @@ -1083,13 +1014,6 @@ packages: resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} - cjs-module-lexer@2.2.0: - resolution: {integrity: sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==} - - cliui@8.0.1: - resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} - engines: {node: '>=12'} - color-convert@2.0.1: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} engines: {node: '>=7.0.0'} @@ -1245,10 +1169,6 @@ packages: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} engines: {node: '>=6.9.0'} - get-caller-file@2.0.5: - resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} - engines: {node: 6.* || 8.* || >= 10.*} - glob@10.5.0: resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==} deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me @@ -1266,10 +1186,6 @@ packages: resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} engines: {node: '>= 14'} - import-in-the-middle@3.0.0: - resolution: {integrity: sha512-OnGy+eYT7wVejH2XWgLRgbmzujhhVIATQH0ztIeRilwHBjTeG3pD+XnH3PKX0r9gJ0BuJmJ68q/oh9qgXnNDQg==} - engines: {node: '>=18'} - is-fullwidth-code-point@3.0.0: resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} engines: {node: '>=8'} @@ -1347,9 +1263,6 @@ packages: openai: optional: true - lodash.camelcase@4.3.0: - resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==} - long@5.3.2: resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==} @@ -1371,9 +1284,6 @@ packages: resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} engines: {node: '>=16 || 14 >=14.17'} - module-details-from-path@1.0.4: - resolution: {integrity: sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w==} - ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -1475,9 +1385,14 @@ packages: engines: {node: ^10 || ^12 || >=14} posthog-node@file:../../target/posthog-node.tgz: - resolution: {integrity: sha512-Exz53XrnxE/fCkRNCdS28+N1nWnDVA6kWUzYXbPXB7Yp63pPO+n1GreV0TwQLDsh5Tb6/BqnUlk7lWWxUHSljw==, tarball: file:../../target/posthog-node.tgz} - version: 5.24.15 + resolution: {integrity: sha512-HnNlFR3+DqYCDZdMUtNEOBUbFz9KfS69uVZvt6nDRcTdXwH45O3XuUN3nF6dGhnNjpYvMRibO8EpAwZzO47JIA==, tarball: file:../../target/posthog-node.tgz} + version: 5.28.11 engines: {node: ^20.20.0 || >=22.22.0} + peerDependencies: + rxjs: ^7.0.0 + peerDependenciesMeta: + rxjs: + optional: true prettier@3.8.1: resolution: {integrity: sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==} @@ -1505,14 +1420,6 @@ packages: resolution: {integrity: sha512-qpt8EwugBWDw2cgE2W+/3oxC+KTez2uSVR8JU9Q36TXPAGCaozfQUs59v4j4GFpWTaw0i6hAZSvOmu1J0uOEUg==} engines: {node: ^18.17.0 || >=20.5.0} - require-directory@2.1.1: - resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} - engines: {node: '>=0.10.0'} - - require-in-the-middle@8.0.1: - resolution: {integrity: sha512-QT7FVMXfWOYFbeRBF6nu+I6tr2Tf3u0q8RIEjNob/heKY/nh7drD/k7eeMFmSQgnTtCzLDcCu/XEnpW2wk4xCQ==} - engines: {node: '>=9.3.0 || >=8.10.0 <9.0.0'} - retry@0.13.1: resolution: {integrity: sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==} engines: {node: '>= 4'} @@ -1699,10 +1606,6 @@ packages: utf-8-validate: optional: true - y18n@5.0.8: - resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} - engines: {node: '>=10'} - yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} @@ -1711,14 +1614,6 @@ packages: engines: {node: '>= 14.6'} hasBin: true - yargs-parser@21.1.1: - resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} - engines: {node: '>=12'} - - yargs@17.7.2: - resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} - engines: {node: '>=12'} - zod@4.3.6: resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==} @@ -1748,7 +1643,7 @@ snapshots: dependencies: json-schema: 0.4.0 - '@anthropic-ai/sdk@0.74.0(zod@4.3.6)': + '@anthropic-ai/sdk@0.78.0(zod@4.3.6)': dependencies: json-schema-to-ts: 3.1.1 optionalDependencies: @@ -2047,18 +1942,6 @@ snapshots: - supports-color - utf-8-validate - '@grpc/grpc-js@1.14.3': - dependencies: - '@grpc/proto-loader': 0.8.0 - '@js-sdsl/ordered-map': 4.4.2 - - '@grpc/proto-loader@0.8.0': - dependencies: - lodash.camelcase: 4.3.0 - long: 5.3.2 - protobufjs: 7.5.4 - yargs: 17.7.2 - '@isaacs/cliui@8.0.2': dependencies: string-width: 5.1.2 @@ -2087,8 +1970,6 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 - '@js-sdsl/ordered-map@4.4.2': {} - '@langchain/core@1.1.31(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.213.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0)(zod@4.3.6))': dependencies: '@cfworker/json-schema': 4.1.1 @@ -2139,112 +2020,32 @@ snapshots: '@opentelemetry/api-logs@0.213.0': dependencies: '@opentelemetry/api': 1.9.0 + optional: true - '@opentelemetry/api@1.9.0': {} - - '@opentelemetry/configuration@0.213.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/api-logs@0.214.0': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.6.0(@opentelemetry/api@1.9.0) - yaml: 2.8.2 - '@opentelemetry/context-async-hooks@2.6.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 + '@opentelemetry/api@1.9.0': {} '@opentelemetry/core@2.6.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/semantic-conventions': 1.40.0 - '@opentelemetry/exporter-logs-otlp-grpc@0.213.0(@opentelemetry/api@1.9.0)': - dependencies: - '@grpc/grpc-js': 1.14.3 - '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.6.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-exporter-base': 0.213.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-grpc-exporter-base': 0.213.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-transformer': 0.213.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-logs': 0.213.0(@opentelemetry/api@1.9.0) - - '@opentelemetry/exporter-logs-otlp-http@0.213.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/api-logs': 0.213.0 - '@opentelemetry/core': 2.6.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-exporter-base': 0.213.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-transformer': 0.213.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-logs': 0.213.0(@opentelemetry/api@1.9.0) - - '@opentelemetry/exporter-logs-otlp-proto@0.213.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/core@2.6.1(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/api-logs': 0.213.0 - '@opentelemetry/core': 2.6.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-exporter-base': 0.213.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-transformer': 0.213.0(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 2.6.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-logs': 0.213.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-trace-base': 2.6.0(@opentelemetry/api@1.9.0) - - '@opentelemetry/exporter-metrics-otlp-grpc@0.213.0(@opentelemetry/api@1.9.0)': - dependencies: - '@grpc/grpc-js': 1.14.3 - '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.6.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-metrics-otlp-http': 0.213.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-exporter-base': 0.213.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-grpc-exporter-base': 0.213.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-transformer': 0.213.0(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 2.6.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-metrics': 2.6.0(@opentelemetry/api@1.9.0) - - '@opentelemetry/exporter-metrics-otlp-http@0.213.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.6.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-exporter-base': 0.213.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-transformer': 0.213.0(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 2.6.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-metrics': 2.6.0(@opentelemetry/api@1.9.0) - - '@opentelemetry/exporter-metrics-otlp-proto@0.213.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.6.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-metrics-otlp-http': 0.213.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-exporter-base': 0.213.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-transformer': 0.213.0(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 2.6.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-metrics': 2.6.0(@opentelemetry/api@1.9.0) - - '@opentelemetry/exporter-prometheus@0.213.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.6.0(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 2.6.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-metrics': 2.6.0(@opentelemetry/api@1.9.0) '@opentelemetry/semantic-conventions': 1.40.0 - '@opentelemetry/exporter-trace-otlp-grpc@0.213.0(@opentelemetry/api@1.9.0)': - dependencies: - '@grpc/grpc-js': 1.14.3 - '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.6.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-exporter-base': 0.213.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-grpc-exporter-base': 0.213.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-transformer': 0.213.0(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 2.6.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-trace-base': 2.6.0(@opentelemetry/api@1.9.0) - - '@opentelemetry/exporter-trace-otlp-http@0.213.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/exporter-trace-otlp-http@0.214.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.6.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-exporter-base': 0.213.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-transformer': 0.213.0(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 2.6.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-trace-base': 2.6.0(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.6.1(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.214.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.214.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.6.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.6.1(@opentelemetry/api@1.9.0) '@opentelemetry/exporter-trace-otlp-proto@0.213.0(@opentelemetry/api@1.9.0)': dependencies: @@ -2254,37 +2055,20 @@ snapshots: '@opentelemetry/otlp-transformer': 0.213.0(@opentelemetry/api@1.9.0) '@opentelemetry/resources': 2.6.0(@opentelemetry/api@1.9.0) '@opentelemetry/sdk-trace-base': 2.6.0(@opentelemetry/api@1.9.0) - - '@opentelemetry/exporter-zipkin@2.6.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.6.0(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 2.6.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-trace-base': 2.6.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.40.0 - - '@opentelemetry/instrumentation@0.213.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/api-logs': 0.213.0 - import-in-the-middle: 3.0.0 - require-in-the-middle: 8.0.1 - transitivePeerDependencies: - - supports-color + optional: true '@opentelemetry/otlp-exporter-base@0.213.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/core': 2.6.0(@opentelemetry/api@1.9.0) '@opentelemetry/otlp-transformer': 0.213.0(@opentelemetry/api@1.9.0) + optional: true - '@opentelemetry/otlp-grpc-exporter-base@0.213.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/otlp-exporter-base@0.214.0(@opentelemetry/api@1.9.0)': dependencies: - '@grpc/grpc-js': 1.14.3 '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.6.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-exporter-base': 0.213.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-transformer': 0.213.0(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.6.1(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.214.0(@opentelemetry/api@1.9.0) '@opentelemetry/otlp-transformer@0.213.0(@opentelemetry/api@1.9.0)': dependencies: @@ -2296,21 +2080,29 @@ snapshots: '@opentelemetry/sdk-metrics': 2.6.0(@opentelemetry/api@1.9.0) '@opentelemetry/sdk-trace-base': 2.6.0(@opentelemetry/api@1.9.0) protobufjs: 7.5.4 + optional: true - '@opentelemetry/propagator-b3@2.6.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/otlp-transformer@0.214.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.6.0(@opentelemetry/api@1.9.0) + '@opentelemetry/api-logs': 0.214.0 + '@opentelemetry/core': 2.6.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.6.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.214.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 2.6.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.6.1(@opentelemetry/api@1.9.0) + protobufjs: 7.5.4 - '@opentelemetry/propagator-jaeger@2.6.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/resources@2.6.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/core': 2.6.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.40.0 - '@opentelemetry/resources@2.6.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/resources@2.6.1(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.6.0(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.6.1(@opentelemetry/api@1.9.0) '@opentelemetry/semantic-conventions': 1.40.0 '@opentelemetry/sdk-logs@0.213.0(@opentelemetry/api@1.9.0)': @@ -2320,42 +2112,28 @@ snapshots: '@opentelemetry/core': 2.6.0(@opentelemetry/api@1.9.0) '@opentelemetry/resources': 2.6.0(@opentelemetry/api@1.9.0) '@opentelemetry/semantic-conventions': 1.40.0 + optional: true + + '@opentelemetry/sdk-logs@0.214.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.214.0 + '@opentelemetry/core': 2.6.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.6.1(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.40.0 '@opentelemetry/sdk-metrics@2.6.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/core': 2.6.0(@opentelemetry/api@1.9.0) '@opentelemetry/resources': 2.6.0(@opentelemetry/api@1.9.0) + optional: true - '@opentelemetry/sdk-node@0.213.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/sdk-metrics@2.6.1(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/api-logs': 0.213.0 - '@opentelemetry/configuration': 0.213.0(@opentelemetry/api@1.9.0) - '@opentelemetry/context-async-hooks': 2.6.0(@opentelemetry/api@1.9.0) - '@opentelemetry/core': 2.6.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-logs-otlp-grpc': 0.213.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-logs-otlp-http': 0.213.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-logs-otlp-proto': 0.213.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-metrics-otlp-grpc': 0.213.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-metrics-otlp-http': 0.213.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-metrics-otlp-proto': 0.213.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-prometheus': 0.213.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-trace-otlp-grpc': 0.213.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-trace-otlp-http': 0.213.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-trace-otlp-proto': 0.213.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-zipkin': 2.6.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation': 0.213.0(@opentelemetry/api@1.9.0) - '@opentelemetry/propagator-b3': 2.6.0(@opentelemetry/api@1.9.0) - '@opentelemetry/propagator-jaeger': 2.6.0(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 2.6.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-logs': 0.213.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-metrics': 2.6.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-trace-base': 2.6.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-trace-node': 2.6.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.40.0 - transitivePeerDependencies: - - supports-color + '@opentelemetry/core': 2.6.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.6.1(@opentelemetry/api@1.9.0) '@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0)': dependencies: @@ -2364,21 +2142,21 @@ snapshots: '@opentelemetry/resources': 2.6.0(@opentelemetry/api@1.9.0) '@opentelemetry/semantic-conventions': 1.40.0 - '@opentelemetry/sdk-trace-node@2.6.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/sdk-trace-base@2.6.1(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/context-async-hooks': 2.6.0(@opentelemetry/api@1.9.0) - '@opentelemetry/core': 2.6.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-trace-base': 2.6.0(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.6.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.6.1(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.40.0 '@opentelemetry/semantic-conventions@1.40.0': {} '@pkgjs/parseargs@0.11.0': optional: true - '@posthog/ai@file:../../target/posthog-ai.tgz(@ai-sdk/provider@2.0.1)(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.213.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0))(posthog-node@file:../../target/posthog-node.tgz)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(ws@8.19.0)': + '@posthog/ai@file:../../target/posthog-ai.tgz(@ai-sdk/provider@2.0.1)(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-http@0.214.0(@opentelemetry/api@1.9.0))(@opentelemetry/exporter-trace-otlp-proto@0.213.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0))(posthog-node@file:../../target/posthog-node.tgz)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(ws@8.19.0)': dependencies: - '@anthropic-ai/sdk': 0.74.0(zod@4.3.6) + '@anthropic-ai/sdk': 0.78.0(zod@4.3.6) '@google/genai': 1.44.0 '@langchain/core': 1.1.31(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.213.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0))(openai@6.27.0(ws@8.19.0)(zod@4.3.6)) '@posthog/core': file:../../target/posthog-core.tgz @@ -2389,9 +2167,10 @@ snapshots: zod: 4.3.6 optionalDependencies: '@ai-sdk/provider': 2.0.1 + '@opentelemetry/api': 1.9.0 + '@opentelemetry/exporter-trace-otlp-http': 0.214.0(@opentelemetry/api@1.9.0) transitivePeerDependencies: - '@modelcontextprotocol/sdk' - - '@opentelemetry/api' - '@opentelemetry/exporter-trace-otlp-proto' - '@opentelemetry/sdk-trace-base' - bufferutil @@ -2407,10 +2186,10 @@ snapshots: '@posthog/core': file:../../target/posthog-core.tgz convex: 1.32.0(react@19.2.4) posthog-node: file:../../target/posthog-node.tgz + transitivePeerDependencies: + - rxjs - '@posthog/core@file:../../target/posthog-core.tgz': - dependencies: - cross-spawn: 7.0.6 + '@posthog/core@file:../../target/posthog-core.tgz': {} '@protobufjs/aspromise@1.1.2': {} @@ -2572,12 +2351,6 @@ snapshots: transitivePeerDependencies: - supports-color - acorn-import-attributes@1.9.5(acorn@8.16.0): - dependencies: - acorn: 8.16.0 - - acorn@8.16.0: {} - agent-base@7.1.4: {} ai@5.0.151(zod@4.3.6): @@ -2628,14 +2401,6 @@ snapshots: chalk@5.6.2: {} - cjs-module-lexer@2.2.0: {} - - cliui@8.0.1: - dependencies: - string-width: 4.2.3 - strip-ansi: 6.0.1 - wrap-ansi: 7.0.0 - color-convert@2.0.1: dependencies: color-name: 1.1.4 @@ -2804,8 +2569,6 @@ snapshots: gensync@1.0.0-beta.2: {} - get-caller-file@2.0.5: {} - glob@10.5.0: dependencies: foreground-child: 3.3.1 @@ -2835,13 +2598,6 @@ snapshots: transitivePeerDependencies: - supports-color - import-in-the-middle@3.0.0: - dependencies: - acorn: 8.16.0 - acorn-import-attributes: 1.9.5(acorn@8.16.0) - cjs-module-lexer: 2.2.0 - module-details-from-path: 1.0.4 - is-fullwidth-code-point@3.0.0: {} is-network-error@1.3.1: {} @@ -2921,8 +2677,6 @@ snapshots: '@opentelemetry/sdk-trace-base': 2.6.0(@opentelemetry/api@1.9.0) openai: 6.27.0(ws@8.19.0)(zod@4.3.6) - lodash.camelcase@4.3.0: {} - long@5.3.2: {} lru-cache@10.4.3: {} @@ -2939,8 +2693,6 @@ snapshots: minipass@7.1.3: {} - module-details-from-path@1.0.4: {} - ms@2.1.3: {} mustache@4.2.0: {} @@ -3058,15 +2810,6 @@ snapshots: json-parse-even-better-errors: 4.0.0 npm-normalize-package-bin: 4.0.0 - require-directory@2.1.1: {} - - require-in-the-middle@8.0.1: - dependencies: - debug: 4.4.3 - module-details-from-path: 1.0.4 - transitivePeerDependencies: - - supports-color - retry@0.13.1: {} rimraf@5.0.10: @@ -3208,22 +2951,9 @@ snapshots: ws@8.19.0: {} - y18n@5.0.8: {} - yallist@3.1.1: {} - yaml@2.8.2: {} - - yargs-parser@21.1.1: {} - - yargs@17.7.2: - dependencies: - cliui: 8.0.1 - escalade: 3.2.0 - get-caller-file: 2.0.5 - require-directory: 2.1.1 - string-width: 4.2.3 - y18n: 5.0.8 - yargs-parser: 21.1.1 + yaml@2.8.2: + optional: true zod@4.3.6: {} diff --git a/examples/example-convex/src/App.css b/examples/example-convex/src/App.css index 53b84a5db2..49a20693e4 100644 --- a/examples/example-convex/src/App.css +++ b/examples/example-convex/src/App.css @@ -1,435 +1,432 @@ *, *::before, *::after { - box-sizing: border-box; - margin: 0; + box-sizing: border-box; + margin: 0; } :root { - --bg-0: #0b0f19; - --bg-1: #111827; - --bg-2: #1e293b; - --bg-3: #334155; - --border: #2d3748; - --border-focus: #475569; - --text-0: #f1f5f9; - --text-1: #cbd5e1; - --text-2: #94a3b8; - --text-3: #64748b; - --font-mono: - "SF Mono", "Cascadia Code", "Fira Code", "JetBrains Mono", ui-monospace, - monospace; - --font-sans: system-ui, -apple-system, sans-serif; - --radius: 6px; - --radius-lg: 10px; + --bg-0: #0b0f19; + --bg-1: #111827; + --bg-2: #1e293b; + --bg-3: #334155; + --border: #2d3748; + --border-focus: #475569; + --text-0: #f1f5f9; + --text-1: #cbd5e1; + --text-2: #94a3b8; + --text-3: #64748b; + --font-mono: 'SF Mono', 'Cascadia Code', 'Fira Code', 'JetBrains Mono', ui-monospace, monospace; + --font-sans: system-ui, -apple-system, sans-serif; + --radius: 6px; + --radius-lg: 10px; } body { - background: var(--bg-0); - color: var(--text-1); - font-family: var(--font-sans); - line-height: 1.5; - -webkit-font-smoothing: antialiased; + background: var(--bg-0); + color: var(--text-1); + font-family: var(--font-sans); + line-height: 1.5; + -webkit-font-smoothing: antialiased; } #root { - max-width: 720px; - margin: 0 auto; - padding: 2rem 1.5rem 4rem; - text-align: left; + max-width: 720px; + margin: 0 auto; + padding: 2rem 1.5rem 4rem; + text-align: left; } /* Header */ .app-header { - margin-bottom: 2rem; + margin-bottom: 2rem; } .app-header h1 { - font-size: 1.5rem; - font-weight: 600; - letter-spacing: -0.02em; - margin-bottom: 0.25rem; + font-size: 1.5rem; + font-weight: 600; + letter-spacing: -0.02em; + margin-bottom: 0.25rem; } .brand-post { - color: var(--text-0); + color: var(--text-0); } .brand-hog { - color: #f59e0b; + color: #f59e0b; } .brand-sep { - color: var(--text-3); - margin: 0 0.35em; - font-weight: 300; + color: var(--text-3); + margin: 0 0.35em; + font-weight: 300; } .brand-convex { - color: var(--text-2); + color: var(--text-2); } .subtitle { - font-family: var(--font-mono); - font-size: 0.8rem; - color: var(--text-3); - text-transform: uppercase; - letter-spacing: 0.1em; + font-family: var(--font-mono); + font-size: 0.8rem; + color: var(--text-3); + text-transform: uppercase; + letter-spacing: 0.1em; } /* Shared inputs */ .shared-inputs { - background: var(--bg-1); - border: 1px solid var(--border); - border-radius: var(--radius-lg); - padding: 1rem 1.25rem; - margin-bottom: 1.5rem; + background: var(--bg-1); + border: 1px solid var(--border); + border-radius: var(--radius-lg); + padding: 1rem 1.25rem; + margin-bottom: 1.5rem; } .shared-inputs .field { - max-width: 320px; + max-width: 320px; } /* Sections */ .sections { - display: flex; - flex-direction: column; - gap: 0.75rem; - margin-bottom: 1.5rem; + display: flex; + flex-direction: column; + gap: 0.75rem; + margin-bottom: 1.5rem; } .sdk-section { - background: var(--bg-1); - border: 1px solid var(--border); - border-radius: var(--radius-lg); - overflow: hidden; + background: var(--bg-1); + border: 1px solid var(--border); + border-radius: var(--radius-lg); + overflow: hidden; } .section-toggle { - width: 100%; - display: flex; - align-items: center; - gap: 0.75rem; - padding: 0.875rem 1.25rem; - background: none; - border: none; - color: var(--text-1); - font-family: var(--font-sans); - font-size: 0.9rem; - font-weight: 500; - cursor: pointer; - text-align: left; - transition: background 150ms; + width: 100%; + display: flex; + align-items: center; + gap: 0.75rem; + padding: 0.875rem 1.25rem; + background: none; + border: none; + color: var(--text-1); + font-family: var(--font-sans); + font-size: 0.9rem; + font-weight: 500; + cursor: pointer; + text-align: left; + transition: background 150ms; } .section-toggle:hover { - background: var(--bg-2); + background: var(--bg-2); } .section-num { - display: inline-flex; - align-items: center; - justify-content: center; - width: 1.5rem; - height: 1.5rem; - border-radius: 4px; - background: color-mix(in srgb, var(--accent) 15%, transparent); - color: var(--accent); - font-family: var(--font-mono); - font-size: 0.75rem; - font-weight: 600; - flex-shrink: 0; + display: inline-flex; + align-items: center; + justify-content: center; + width: 1.5rem; + height: 1.5rem; + border-radius: 4px; + background: color-mix(in srgb, var(--accent) 15%, transparent); + color: var(--accent); + font-family: var(--font-mono); + font-size: 0.75rem; + font-weight: 600; + flex-shrink: 0; } .section-label { - flex: 1; + flex: 1; } .chevron { - color: var(--text-3); - transition: transform 200ms; - flex-shrink: 0; + color: var(--text-3); + transition: transform 200ms; + flex-shrink: 0; } .chevron.open { - transform: rotate(180deg); + transform: rotate(180deg); } .section-content { - padding: 0 1.25rem 1.25rem; - border-top: 1px solid var(--border); + padding: 0 1.25rem 1.25rem; + border-top: 1px solid var(--border); } /* Field grid */ .field-grid { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 0.75rem; - padding-top: 1rem; + display: grid; + grid-template-columns: 1fr 1fr; + gap: 0.75rem; + padding-top: 1rem; } .field { - display: flex; - flex-direction: column; - gap: 0.35rem; + display: flex; + flex-direction: column; + gap: 0.35rem; } .field--wide { - grid-column: 1 / -1; + grid-column: 1 / -1; } .field-label { - font-size: 0.78rem; - font-weight: 500; - color: var(--text-2); + font-size: 0.78rem; + font-weight: 500; + color: var(--text-2); } .field-hint { - color: var(--text-3); - font-weight: 400; - margin-left: 0.5em; - font-size: 0.72rem; + color: var(--text-3); + font-weight: 400; + margin-left: 0.5em; + font-size: 0.72rem; } /* Inputs */ -input[type="text"], +input[type='text'], input:not([type]), textarea, select { - background: var(--bg-0); - border: 1px solid var(--border); - border-radius: var(--radius); - color: var(--text-0); - font-family: var(--font-mono); - font-size: 0.8rem; - padding: 0.5rem 0.625rem; - outline: none; - transition: - border-color 150ms, - box-shadow 150ms; - width: 100%; + background: var(--bg-0); + border: 1px solid var(--border); + border-radius: var(--radius); + color: var(--text-0); + font-family: var(--font-mono); + font-size: 0.8rem; + padding: 0.5rem 0.625rem; + outline: none; + transition: + border-color 150ms, + box-shadow 150ms; + width: 100%; } input:focus, textarea:focus, select:focus { - border-color: var(--accent, var(--border-focus)); - box-shadow: 0 0 0 2px color-mix(in srgb, var(--accent, var(--border-focus)) 15%, transparent); + border-color: var(--accent, var(--border-focus)); + box-shadow: 0 0 0 2px color-mix(in srgb, var(--accent, var(--border-focus)) 15%, transparent); } textarea { - resize: vertical; - min-height: 2.5rem; - line-height: 1.4; + resize: vertical; + min-height: 2.5rem; + line-height: 1.4; } select { - cursor: pointer; - appearance: none; - background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath d='M3 4.5L6 7.5L9 4.5' fill='none' stroke='%2394a3b8' stroke-width='1.5'/%3E%3C/svg%3E"); - background-repeat: no-repeat; - background-position: right 0.5rem center; - padding-right: 1.75rem; + cursor: pointer; + appearance: none; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath d='M3 4.5L6 7.5L9 4.5' fill='none' stroke='%2394a3b8' stroke-width='1.5'/%3E%3C/svg%3E"); + background-repeat: no-repeat; + background-position: right 0.5rem center; + padding-right: 1.75rem; } ::placeholder { - color: var(--text-3); + color: var(--text-3); } /* Checkboxes */ .checkbox-row { - grid-column: 1 / -1; - display: flex; - gap: 1.25rem; - flex-wrap: wrap; + grid-column: 1 / -1; + display: flex; + gap: 1.25rem; + flex-wrap: wrap; } .checkbox { - display: flex; - align-items: center; - gap: 0.4rem; - font-size: 0.8rem; - color: var(--text-2); - cursor: pointer; - user-select: none; + display: flex; + align-items: center; + gap: 0.4rem; + font-size: 0.8rem; + color: var(--text-2); + cursor: pointer; + user-select: none; } -.checkbox input[type="checkbox"] { - width: 1rem; - height: 1rem; - accent-color: var(--accent, #60a5fa); - cursor: pointer; +.checkbox input[type='checkbox'] { + width: 1rem; + height: 1rem; + accent-color: var(--accent, #60a5fa); + cursor: pointer; } /* Action buttons */ .actions { - display: flex; - gap: 0.5rem; - margin-top: 1rem; - justify-content: flex-end; + display: flex; + gap: 0.5rem; + margin-top: 1rem; + justify-content: flex-end; } .actions--wrap { - flex-wrap: wrap; + flex-wrap: wrap; } .btn { - background: color-mix(in srgb, var(--accent, #60a5fa) 15%, var(--bg-2)); - border: 1px solid color-mix(in srgb, var(--accent, #60a5fa) 30%, var(--border)); - border-radius: var(--radius); - color: var(--accent, #60a5fa); - font-family: var(--font-mono); - font-size: 0.78rem; - font-weight: 500; - padding: 0.5rem 1rem; - cursor: pointer; - transition: - background 150ms, - border-color 150ms; - white-space: nowrap; + background: color-mix(in srgb, var(--accent, #60a5fa) 15%, var(--bg-2)); + border: 1px solid color-mix(in srgb, var(--accent, #60a5fa) 30%, var(--border)); + border-radius: var(--radius); + color: var(--accent, #60a5fa); + font-family: var(--font-mono); + font-size: 0.78rem; + font-weight: 500; + padding: 0.5rem 1rem; + cursor: pointer; + transition: + background 150ms, + border-color 150ms; + white-space: nowrap; } .btn:hover { - background: color-mix(in srgb, var(--accent, #60a5fa) 25%, var(--bg-2)); - border-color: var(--accent, #60a5fa); + background: color-mix(in srgb, var(--accent, #60a5fa) 25%, var(--bg-2)); + border-color: var(--accent, #60a5fa); } .btn:active { - background: color-mix(in srgb, var(--accent, #60a5fa) 35%, var(--bg-2)); + background: color-mix(in srgb, var(--accent, #60a5fa) 35%, var(--bg-2)); } .btn--loading { - opacity: 0.7; - cursor: wait; - animation: btn-pulse 1s ease-in-out infinite; + opacity: 0.7; + cursor: wait; + animation: btn-pulse 1s ease-in-out infinite; } .btn--success { - background: color-mix(in srgb, #34d399 20%, var(--bg-2)); - border-color: #34d399; - color: #34d399; - animation: btn-flash 400ms ease-out; + background: color-mix(in srgb, #34d399 20%, var(--bg-2)); + border-color: #34d399; + color: #34d399; + animation: btn-flash 400ms ease-out; } .btn--error { - background: color-mix(in srgb, #f87171 20%, var(--bg-2)); - border-color: #f87171; - color: #f87171; - animation: btn-flash 400ms ease-out; + background: color-mix(in srgb, #f87171 20%, var(--bg-2)); + border-color: #f87171; + color: #f87171; + animation: btn-flash 400ms ease-out; } @keyframes btn-pulse { + 0%, + 100% { + opacity: 0.7; + } - 0%, - 100% { - opacity: 0.7; - } - - 50% { - opacity: 0.4; - } + 50% { + opacity: 0.4; + } } @keyframes btn-flash { - 0% { - filter: brightness(1.5); - } + 0% { + filter: brightness(1.5); + } - 100% { - filter: brightness(1); - } + 100% { + filter: brightness(1); + } } .btn--ghost { - background: transparent; - border-color: transparent; + background: transparent; + border-color: transparent; } .btn--ghost:hover { - background: var(--bg-2); - border-color: var(--border); + background: var(--bg-2); + border-color: var(--border); } /* Log panel */ .log-panel { - background: var(--bg-1); - border: 1px solid var(--border); - border-radius: var(--radius-lg); - overflow: hidden; + background: var(--bg-1); + border: 1px solid var(--border); + border-radius: var(--radius-lg); + overflow: hidden; } .log-header { - display: flex; - align-items: center; - justify-content: space-between; - padding: 0.625rem 1.25rem; - border-bottom: 1px solid var(--border); + display: flex; + align-items: center; + justify-content: space-between; + padding: 0.625rem 1.25rem; + border-bottom: 1px solid var(--border); } .log-title { - font-family: var(--font-mono); - font-size: 0.78rem; - font-weight: 600; - color: var(--text-2); - text-transform: uppercase; - letter-spacing: 0.08em; + font-family: var(--font-mono); + font-size: 0.78rem; + font-weight: 600; + color: var(--text-2); + text-transform: uppercase; + letter-spacing: 0.08em; } .log-output { - font-family: var(--font-mono); - font-size: 0.78rem; - line-height: 1.6; - color: var(--text-2); - padding: 1rem 1.25rem; - max-height: 300px; - overflow: auto; - white-space: pre-wrap; - word-break: break-all; + font-family: var(--font-mono); + font-size: 0.78rem; + line-height: 1.6; + color: var(--text-2); + padding: 1rem 1.25rem; + max-height: 300px; + overflow: auto; + white-space: pre-wrap; + word-break: break-all; } .log-output::-webkit-scrollbar { - width: 6px; + width: 6px; } .log-output::-webkit-scrollbar-track { - background: transparent; + background: transparent; } .log-output::-webkit-scrollbar-thumb { - background: var(--bg-3); - border-radius: 3px; + background: var(--bg-3); + border-radius: 3px; } /* Responsive */ @media (max-width: 640px) { - #root { - padding: 1.5rem 1rem 3rem; - } + #root { + padding: 1.5rem 1rem 3rem; + } - .field-grid { - grid-template-columns: 1fr; - } + .field-grid { + grid-template-columns: 1fr; + } - .field--wide { - grid-column: 1; - } + .field--wide { + grid-column: 1; + } - .actions { - flex-wrap: wrap; - } + .actions { + flex-wrap: wrap; + } } .section-note { - margin-top: 0.75rem; - font-size: 0.75rem; - color: var(--text-3); - line-height: 1.5; + margin-top: 0.75rem; + font-size: 0.75rem; + color: var(--text-3); + line-height: 1.5; } .section-note code { - background: var(--bg-2); - padding: 0.1em 0.35em; - border-radius: 3px; - font-family: var(--font-mono); - font-size: 0.7rem; -} \ No newline at end of file + background: var(--bg-2); + padding: 0.1em 0.35em; + border-radius: 3px; + font-family: var(--font-mono); + font-size: 0.7rem; +} diff --git a/examples/example-convex/src/App.tsx b/examples/example-convex/src/App.tsx index abab21e3da..76b8243432 100644 --- a/examples/example-convex/src/App.tsx +++ b/examples/example-convex/src/App.tsx @@ -4,589 +4,645 @@ import { api } from '../convex/_generated/api' import { useState, useRef, useEffect, useCallback, type ReactNode } from 'react' function tryParseJson(str: string, addLog: (msg: string) => void, field: string): unknown | undefined { - const trimmed = str.trim() - if (!trimmed) return undefined - try { - return JSON.parse(trimmed) - } catch { - addLog(`Parse error in ${field}: invalid JSON`) - return undefined - } + const trimmed = str.trim() + if (!trimmed) return undefined + try { + return JSON.parse(trimmed) + } catch { + addLog(`Parse error in ${field}: invalid JSON`) + return undefined + } } function Section({ - num, - title, - accent, - children, - defaultOpen = true, + num, + title, + accent, + children, + defaultOpen = true, }: { - num: number - title: string - accent: string - children: ReactNode - defaultOpen?: boolean + num: number + title: string + accent: string + children: ReactNode + defaultOpen?: boolean }) { - const [open, setOpen] = useState(defaultOpen) - return ( -
- - {open &&
{children}
} -
- ) + const [open, setOpen] = useState(defaultOpen) + return ( +
+ + {open &&
{children}
} +
+ ) } function Field({ label, hint, children, wide }: { label: string; hint?: string; children: ReactNode; wide?: boolean }) { - return ( - - ) + return ( + + ) } function App() { - // Shared - const [distinctId, setDistinctId] = useState('user-123') - - // 1. Capture - const [captureEvent, setCaptureEvent] = useState('button_clicked') - const [captureProps, setCaptureProps] = useState('{"plan":"pro","amount":99}') - const [captureGroups, setCaptureGroups] = useState('{"company":"acme"}') - const [captureSendFlags, setCaptureSendFlags] = useState(false) - const [captureGeoip, setCaptureGeoip] = useState(false) - const [captureUuid, setCaptureUuid] = useState('') - const [captureTimestamp, setCaptureTimestamp] = useState('') - - // 2. Identify - const [identifyProps, setIdentifyProps] = useState('{"name":"Test User","email":"test@example.com","plan":"pro"}') - const [identifyGeoip, setIdentifyGeoip] = useState(false) - - // 3. Group Identify - const [groupType, setGroupType] = useState('company') - const [groupKey, setGroupKey] = useState('acme') - const [groupProps, setGroupProps] = useState('{"industry":"Technology","size":100}') - const [groupDistinctId, setGroupDistinctId] = useState('') - const [groupGeoip, setGroupGeoip] = useState(false) - - // 4. Alias - const [aliasValue, setAliasValue] = useState('anon-456') - const [aliasGeoip, setAliasGeoip] = useState(false) - - // 5. Capture Exception - const [errorMsg, setErrorMsg] = useState('Something went wrong') - const [errorType, setErrorType] = useState<'error' | 'string' | 'object'>('error') - const [exceptionProps, setExceptionProps] = useState('{"page":"/checkout"}') - const [exceptionDistinctId, setExceptionDistinctId] = useState('') - - // 7. AI Generation - const [aiLibrary, setAiLibrary] = useState<'agent' | 'ai-sdk'>('agent') - const [aiCapture, setAiCapture] = useState<'manual' | 'withTracing' | 'otel'>('manual') - const [aiPrompt, setAiPrompt] = useState('What is PostHog?') - - // 6. Feature Flags - const [flagKey, setFlagKey] = useState('test-flag') - const [ffGroups, setFfGroups] = useState('{"company":"acme"}') - const [ffPersonProps, setFfPersonProps] = useState('{"email":"test@example.com"}') - const [ffGroupProps, setFfGroupProps] = useState('{"company":{"industry":"tech"}}') - const [ffSendEvents, setFfSendEvents] = useState(false) - const [ffGeoip, setFfGeoip] = useState(false) - const [ffMatchValue, setFfMatchValue] = useState('') - const [ffFlagKeys, setFfFlagKeys] = useState('') - - // Log & button status - const [log, setLog] = useState([]) - const logRef = useRef(null) - const [btnStatus, setBtnStatus] = useState>({}) - const addLog = useCallback( - (msg: string) => setLog((prev) => [...prev, `${new Date().toLocaleTimeString()} ${msg}`]), - [] - ) - - useEffect(() => { - if (logRef.current) logRef.current.scrollTop = logRef.current.scrollHeight - }, [log]) - - // Convex hooks - const captureM = useMutation(api.example.testCapture) - const identifyM = useMutation(api.example.testIdentify) - const groupIdentifyM = useMutation(api.example.testGroupIdentify) - const aliasM = useMutation(api.example.testAlias) - const captureExceptionM = useMutation(api.example.testCaptureException) - const throwErrorM = useMutation(api.example.testThrowError) - - const agentManualA = useAction(api.convexAgent.manualCapture.generate) - const agentTracedA = useAction(api.convexAgent.withTracing.generate) - const agentOtelA = useAction(api.convexAgent.openTelemetry.generate) - const aiSdkManualA = useAction(api.aiSdk.manualCapture.generate) - const aiSdkTracedA = useAction(api.aiSdk.withTracing.generate) - const aiSdkOtelA = useAction(api.aiSdk.openTelemetry.generate) - - const getFeatureFlagA = useAction(api.example.testGetFeatureFlag) - const isFeatureEnabledA = useAction(api.example.testIsFeatureEnabled) - const getPayloadA = useAction(api.example.testGetFeatureFlagPayload) - const getResultA = useAction(api.example.testGetFeatureFlagResult) - const getAllFlagsA = useAction(api.example.testGetAllFlags) - const getAllPayloadsA = useAction(api.example.testGetAllFlagsAndPayloads) - - const run = async (label: string, fn: () => Promise) => { - setBtnStatus((s) => ({ ...s, [label]: 'loading' })) - addLog(`${label}...`) - let outcome: 'success' | 'error' = 'success' - try { - const result = await fn() - addLog(`${label} -> ${JSON.stringify(result)}`) - } catch (e) { - addLog(`${label} ERROR: ${e}`) - outcome = 'error' + // Shared + const [distinctId, setDistinctId] = useState('user-123') + + // 1. Capture + const [captureEvent, setCaptureEvent] = useState('button_clicked') + const [captureProps, setCaptureProps] = useState('{"plan":"pro","amount":99}') + const [captureGroups, setCaptureGroups] = useState('{"company":"acme"}') + const [captureSendFlags, setCaptureSendFlags] = useState(false) + const [captureGeoip, setCaptureGeoip] = useState(false) + const [captureUuid, setCaptureUuid] = useState('') + const [captureTimestamp, setCaptureTimestamp] = useState('') + + // 2. Identify + const [identifyProps, setIdentifyProps] = useState('{"name":"Test User","email":"test@example.com","plan":"pro"}') + const [identifyGeoip, setIdentifyGeoip] = useState(false) + + // 3. Group Identify + const [groupType, setGroupType] = useState('company') + const [groupKey, setGroupKey] = useState('acme') + const [groupProps, setGroupProps] = useState('{"industry":"Technology","size":100}') + const [groupDistinctId, setGroupDistinctId] = useState('') + const [groupGeoip, setGroupGeoip] = useState(false) + + // 4. Alias + const [aliasValue, setAliasValue] = useState('anon-456') + const [aliasGeoip, setAliasGeoip] = useState(false) + + // 5. Capture Exception + const [errorMsg, setErrorMsg] = useState('Something went wrong') + const [errorType, setErrorType] = useState<'error' | 'string' | 'object'>('error') + const [exceptionProps, setExceptionProps] = useState('{"page":"/checkout"}') + const [exceptionDistinctId, setExceptionDistinctId] = useState('') + + // 7. AI Generation + const [aiLibrary, setAiLibrary] = useState<'agent' | 'ai-sdk'>('agent') + const [aiCapture, setAiCapture] = useState<'manual' | 'withTracing' | 'otel'>('manual') + const [aiPrompt, setAiPrompt] = useState('What is PostHog?') + + // 6. Feature Flags + const [flagKey, setFlagKey] = useState('test-flag') + const [ffGroups, setFfGroups] = useState('{"company":"acme"}') + const [ffPersonProps, setFfPersonProps] = useState('{"email":"test@example.com"}') + const [ffGroupProps, setFfGroupProps] = useState('{"company":{"industry":"tech"}}') + const [ffSendEvents, setFfSendEvents] = useState(false) + const [ffGeoip, setFfGeoip] = useState(false) + const [ffMatchValue, setFfMatchValue] = useState('') + const [ffFlagKeys, setFfFlagKeys] = useState('') + + // Log & button status + const [log, setLog] = useState([]) + const logRef = useRef(null) + const [btnStatus, setBtnStatus] = useState>({}) + const addLog = useCallback( + (msg: string) => setLog((prev) => [...prev, `${new Date().toLocaleTimeString()} ${msg}`]), + [] + ) + + useEffect(() => { + if (logRef.current) logRef.current.scrollTop = logRef.current.scrollHeight + }, [log]) + + // Convex hooks + const captureM = useMutation(api.example.testCapture) + const identifyM = useMutation(api.example.testIdentify) + const groupIdentifyM = useMutation(api.example.testGroupIdentify) + const aliasM = useMutation(api.example.testAlias) + const captureExceptionM = useMutation(api.example.testCaptureException) + const throwErrorM = useMutation(api.example.testThrowError) + + const agentManualA = useAction(api.convexAgent.manualCapture.generate) + const agentTracedA = useAction(api.convexAgent.withTracing.generate) + const agentOtelA = useAction(api.convexAgent.openTelemetry.generate) + const aiSdkManualA = useAction(api.aiSdk.manualCapture.generate) + const aiSdkTracedA = useAction(api.aiSdk.withTracing.generate) + const aiSdkOtelA = useAction(api.aiSdk.openTelemetry.generate) + + const getFeatureFlagA = useAction(api.example.testGetFeatureFlag) + const isFeatureEnabledA = useAction(api.example.testIsFeatureEnabled) + const getPayloadA = useAction(api.example.testGetFeatureFlagPayload) + const getResultA = useAction(api.example.testGetFeatureFlagResult) + const getAllFlagsA = useAction(api.example.testGetAllFlags) + const getAllPayloadsA = useAction(api.example.testGetAllFlagsAndPayloads) + + const run = async (label: string, fn: () => Promise) => { + setBtnStatus((s) => ({ ...s, [label]: 'loading' })) + addLog(`${label}...`) + let outcome: 'success' | 'error' = 'success' + try { + const result = await fn() + addLog(`${label} -> ${JSON.stringify(result)}`) + } catch (e) { + addLog(`${label} ERROR: ${e}`) + outcome = 'error' + } + setBtnStatus((s) => ({ ...s, [label]: outcome })) + setTimeout(() => { + setBtnStatus((s) => { + const next = { ...s } + if (next[label] === outcome) delete next[label] + return next + }) + }, 2000) } - setBtnStatus((s) => ({ ...s, [label]: outcome })) - setTimeout(() => { - setBtnStatus((s) => { - const next = { ...s } - if (next[label] === outcome) delete next[label] - return next - }) - }, 2000) - } - - const btnProps = (label: string) => { - const status = btnStatus[label] - return { - className: `btn${status ? ` btn--${status}` : ''}`, - disabled: status === 'loading', + + const btnProps = (label: string) => { + const status = btnStatus[label] + return { + className: `btn${status ? ` btn--${status}` : ''}`, + disabled: status === 'loading', + } } - } - - const json = (str: string, field: string) => tryParseJson(str, addLog, field) - - const ffArgs = () => ({ - distinctId, - flagKey, - groups: json(ffGroups, 'FF groups') as Record | undefined, - personProperties: json(ffPersonProps, 'FF person props') as Record | undefined, - groupProperties: json(ffGroupProps, 'FF group props') as Record> | undefined, - sendFeatureFlagEvents: ffSendEvents || undefined, - disableGeoip: ffGeoip || undefined, - }) - - const ffAllArgs = () => { - const keys = ffFlagKeys.trim() - ? ffFlagKeys - .split(',') - .map((k) => k.trim()) - .filter(Boolean) - : undefined - return { - distinctId, - groups: json(ffGroups, 'FF groups') as Record | undefined, - personProperties: json(ffPersonProps, 'FF person props') as Record | undefined, - groupProperties: json(ffGroupProps, 'FF group props') as Record> | undefined, - disableGeoip: ffGeoip || undefined, - flagKeys: keys, + + const json = (str: string, field: string) => tryParseJson(str, addLog, field) + + const ffArgs = () => ({ + distinctId, + flagKey, + groups: json(ffGroups, 'FF groups') as Record | undefined, + personProperties: json(ffPersonProps, 'FF person props') as Record | undefined, + groupProperties: json(ffGroupProps, 'FF group props') as Record> | undefined, + sendFeatureFlagEvents: ffSendEvents || undefined, + disableGeoip: ffGeoip || undefined, + }) + + const ffAllArgs = () => { + const keys = ffFlagKeys.trim() + ? ffFlagKeys + .split(',') + .map((k) => k.trim()) + .filter(Boolean) + : undefined + return { + distinctId, + groups: json(ffGroups, 'FF groups') as Record | undefined, + personProperties: json(ffPersonProps, 'FF person props') as Record | undefined, + groupProperties: json(ffGroupProps, 'FF group props') as Record> | undefined, + disableGeoip: ffGeoip || undefined, + flagKeys: keys, + } } - } - - return ( -
-
-

- Post - Hog - × - Convex -

-

SDK Explorer

-
- -
- - setDistinctId(e.target.value)} - placeholder="user-123" - /> - -
- -
- {/* 1. Event Capture */} -
-
- - setCaptureEvent(e.target.value)} /> - - - setCaptureUuid(e.target.value)} - placeholder="auto-generated" - /> - - -