
Security News
OpenCode User Hit with Hostile Backlash After Reporting Free AI Loophole
An OpenCode user faced harassment after reporting a free AI usage loophole, drawing a bitter backlash in a GitHub thread that has since been deleted.
convex-langfuse
Advanced tools
Send LLM traces, generations, and scores from your Convex app to Langfuse over OpenTelemetry, with reactive local queries.
Send LLM traces, generations, and scores from your Convex app to Langfuse. Reactive local queries, OpenTelemetry-native ingestion.

const langfuse = new Langfuse(components.convexLangfuse, {
publicKey: process.env.LANGFUSE_PUBLIC_KEY!,
secretKey: process.env.LANGFUSE_SECRET_KEY!,
});
// Log a completed LLM call
const { traceId } = await langfuse.logGeneration(ctx, {
name: "chat-completion",
userId: "user_123",
model: "gpt-5",
input: prompt,
output: completion,
inputTokens: 120,
outputTokens: 340,
});
// Attach a user rating or eval score to it
await langfuse.recordScore(ctx, { traceId, name: "user-rating", value: 1 });
Most Langfuse integrations assume a long-lived Node process that can hold an OpenTelemetry span open for the duration of an LLM call. Convex actions are short, stateless, request/response functions — there's no "open a span, await a stream, close it later." This component is built for that shape: you call it once, with the finished input/output/timing/usage already in hand, and it does the rest.
Under the hood it exports a single completed OpenTelemetry span per call, using Langfuse's documented span attribute conventions (langfuse.observation.*, langfuse.trace.*) — no OpenTelemetry SDK dependency, just fetch. This matters more than it sounds: Langfuse's older /api/public/ingestion batch endpoint is deprecated and sunsets on Langfuse Cloud on November 16, 2026. Building against the OTLP endpoint from day one means this component doesn't need a rewrite in a few months.
Alongside the export, every trace, generation, and score is mirrored into Convex tables so you can query them reactively — build a live "recent generations" or "token usage" view without a separate call to Langfuse's API.
logGeneration() / logSpan() take a finished call and export it as a proper Langfuse trace + observationrecordScore() attaches an evaluation or user-rating score to any tracefetchnpm install convex-langfuse
Requirements: Convex v1.33.1 or later, Node.js 18+, a Langfuse project (Cloud or self-hosted, OTel-compatible)
Three steps to send traces from your Convex app to Langfuse.
In convex/convex.config.ts:
import { defineApp } from "convex/server";
import convexLangfuse from "convex-langfuse/convex.config";
const app = defineApp();
app.use(convexLangfuse);
export default app;
npx convex env set LANGFUSE_PUBLIC_KEY pk-lf-xxxxxxxxxxxx
npx convex env set LANGFUSE_SECRET_KEY sk-lf-xxxxxxxxxxxx
In convex/observability.ts:
import { components } from "./_generated/api";
import { Langfuse } from "convex-langfuse";
export const langfuse = new Langfuse(components.convexLangfuse, {
publicKey: process.env.LANGFUSE_PUBLIC_KEY!,
secretKey: process.env.LANGFUSE_SECRET_KEY!,
// baseUrl defaults to the EU Langfuse Cloud region — set it for US/JP/self-hosted
});
Import langfuse from this file in any Convex action that calls an LLM.
export const chat = action({
args: { userId: v.string(), prompt: v.string() },
handler: async (ctx, args) => {
const completion = await callYourModel(args.prompt);
const { traceId } = await langfuse.logGeneration(ctx, {
name: "chat-completion",
userId: args.userId,
model: "gpt-5",
input: args.prompt,
output: completion,
inputTokens: completion.usage.inputTokens,
outputTokens: completion.usage.outputTokens,
});
return { completion, traceId };
},
});
// Returns: { traceId, observationId }
await langfuse.logSpan(ctx, {
traceId,
name: "vector-search",
input: { query },
output: { matches },
});
export const rate = action({
args: { traceId: v.string(), value: v.number() },
handler: async (ctx, args) => {
await langfuse.recordScore(ctx, {
traceId: args.traceId,
name: "user-rating",
value: args.value, // e.g. 1 for thumbs up, 0 for thumbs down
});
return null;
},
});
export const getTrace = query({
args: { traceId: v.string() },
handler: async (ctx, args) => langfuse.getTrace(ctx, args),
});
export const getObservations = query({
args: { traceId: v.string() },
handler: async (ctx, args) => langfuse.listObservations(ctx, args),
});
Pass the traceId returned from the first call into subsequent logGeneration()/logSpan() calls to group multiple observations (a retrieval step, then a generation, then a tool call) under one trace in the Langfuse UI:
const { traceId } = await langfuse.logSpan(ctx, { name: "retrieve-context", ... });
await langfuse.logGeneration(ctx, { traceId, name: "generate-answer", ... });
| Method | Kind | Description |
|---|---|---|
logGeneration(ctx, args) | action | Exports a completed LLM call as a Langfuse generation |
logSpan(ctx, args) | action | Exports a completed non-LLM step as a Langfuse span |
recordScore(ctx, args) | action | Attaches an evaluation/rating score to a trace |
getTrace(ctx, args) | query | Fetch one trace by id |
listTracesByUser(ctx, args) | query | List a user's traces, newest first |
listTracesBySession(ctx, args) | query | List a session's traces, newest first |
listObservations(ctx, args) | query | List a trace's observations, oldest first |
listScores(ctx, args) | query | List a trace's scores |
listRecentTraces(ctx, args?) | query | Every trace, newest first, regardless of user/session — for a history view or dashboard |
getStats(ctx) | query | Aggregate trace/observation/score counts for a small dashboard |
type LogObservationArgs = {
traceId?: string; // omit to start a new trace
traceName?: string;
userId?: string;
sessionId?: string;
tags?: string[];
release?: string;
traceMetadata?: Record<string, unknown>;
name: string;
input?: unknown;
output?: unknown;
model?: string;
inputTokens?: number;
outputTokens?: number;
level?: "DEBUG" | "DEFAULT" | "WARNING" | "ERROR";
statusMessage?: string;
metadata?: Record<string, unknown>;
startTime?: number; // ms epoch, defaults to now
endTime?: number; // ms epoch, defaults to startTime
};
type Trace = {
traceId: string;
name: string;
userId?: string;
sessionId?: string;
tags?: string[];
metadata?: string;
release?: string;
};
type Observation = {
observationId: string;
traceId: string;
type: "span" | "generation" | "event" | "embedding" | "agent" | "tool"
| "chain" | "retriever" | "guardrail" | "evaluator";
name: string;
input?: string;
output?: string;
model?: string;
inputTokens?: number;
outputTokens?: number;
level: "DEBUG" | "DEFAULT" | "WARNING" | "ERROR";
statusMessage?: string;
metadata?: string;
startedAt: number;
endedAt: number;
};
type Score = {
scoreId: string;
traceId: string;
name: string;
value: number;
comment?: string;
};
traces: {
traceId, name, userId?, sessionId?, tags?, metadata?, release?,
createdAt, updatedAt,
}
observations: {
observationId, traceId, type, name, input?, output?, model?,
inputTokens?, outputTokens?, level, statusMessage?, metadata?,
startedAt, endedAt, createdAt,
}
scores: {
scoreId, traceId, name, value, comment?, createdAt,
}
listRecentTraces() and getStats() read across these tables directly with a full scan — fine for a history view or small dashboard, not intended as a high-volume production query path.
npm run test
Component logic is tested with convex-test in src/component/lib.test.ts. Import convex-langfuse/test in your own app to register this component's schema against your test instance.
example/ is a full Vite + React demo that exercises the entire component end to end against your own Langfuse account:
logSpan().recordScore() call, with an optional comment.Run it from the repo root:
npm install --legacy-peer-deps
npx convex env set LANGFUSE_PUBLIC_KEY pk-lf-xxxxxxxxxxxx
npx convex env set LANGFUSE_SECRET_KEY sk-lf-xxxxxxxxxxxx
npm run dev
npm run dev starts the Convex backend and the Vite frontend together — there's no need to cd example or run either one separately.
userId, sessionId, tags, metadata) are attached to every span you log under that trace, per Langfuse's own OTel guidance for filterable trace attributes — there's no separate "create trace" call.Nothing shows up in the Langfuse UI — confirm baseUrl matches your project's region (EU, US, JP, HIPAA, or your self-hosted URL) and that the OTLP endpoint isn't blocked by a firewall between Convex and Langfuse.
401 from Langfuse — publicKey/secretKey are combined into a Basic auth header; confirm both are set and belong to the same project.
Generation shows no cost/usage — cost is computed by Langfuse from inputTokens/outputTokens and the model's pricing table; confirm model matches a model name Langfuse recognizes for your provider.
See CONTRIBUTING.md.
See CHANGELOG.md.
FAQs
Send LLM traces, generations, and scores from your Convex app to Langfuse over OpenTelemetry, with reactive local queries.
The npm package convex-langfuse receives a total of 0 weekly downloads. As such, convex-langfuse popularity was classified as not popular.
We found that convex-langfuse demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.

Security News
An OpenCode user faced harassment after reporting a free AI usage loophole, drawing a bitter backlash in a GitHub thread that has since been deleted.

Threat Research
/Security News
319 browser extensions installed by 13.4 million users hijack searches, capture queries, and collect referral payments from Yahoo and Bing.

Threat Research
Malicious Firefox extensions mimic Phantom, Jupiter, and Bitget wallets to steal recovery phrases and private keys, with one fetching its payload remotely.