npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

convex-langfuse

v0.0.5

Published

Send LLM traces, generations, and scores from your Convex app to Langfuse over OpenTelemetry, with reactive local queries.

Downloads

536

Readme

convex-langfuse

Send LLM traces, generations, and scores from your Convex app to Langfuse. Reactive local queries, OpenTelemetry-native ingestion.

npm version Convex Component npm downloads License

convex-langfuse demo

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 });

What this does

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.

  • One-shot logginglogGeneration() / logSpan() take a finished call and export it as a proper Langfuse trace + observation
  • ScoringrecordScore() attaches an evaluation or user-rating score to any trace
  • Reactive local queries — traces, observations, and scores are all queryable live from Convex
  • No SDK dependency — talks to Langfuse's OTLP/HTTP and Scores REST endpoints directly over fetch

Table of Contents

Install

npm install convex-langfuse

Requirements: Convex v1.33.1 or later, Node.js 18+, a Langfuse project (Cloud or self-hosted, OTel-compatible)

Quick Start

Three steps to send traces from your Convex app to Langfuse.

1. Add the component

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;

2. Set environment variables

npx convex env set LANGFUSE_PUBLIC_KEY pk-lf-xxxxxxxxxxxx
npx convex env set LANGFUSE_SECRET_KEY sk-lf-xxxxxxxxxxxx

3. Initialize the client

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.

Usage

Log a generation

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 }

Log a non-LLM step

await langfuse.logSpan(ctx, {
  traceId,
  name: "vector-search",
  input: { query },
  output: { matches },
});

Record a score

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;
  },
});

Read a trace reactively

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),
});

Continuing a Trace

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", ... });

API Reference

| 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 Reference

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;
};

Database Schema

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.

Testing

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 App

example/ is a full Vite + React demo that exercises the entire component end to end against your own Langfuse account:

  • Log a generation — simulate a finished chat completion (model, prompt, completion, token counts, tags) and export it as a Langfuse generation.
  • Continue the trace — attach a non-LLM step (a retrieval, a tool call, a guardrail check) to the same trace via logSpan().
  • Rate the response — attach a 👍/👎 recordScore() call, with an optional comment.
  • Trace explorer — look up any trace by id, or browse the traces recorded for a given user/session, all reactive.
  • History — every trace ever recorded in the deployment, newest first, independent of the browser-local demo identity that resets on reload. Expand a trace to see its full generation/step/score detail, and hit Replay this to recreate the whole scenario as a brand-new trace live, without retyping anything — useful for anyone poking at the demo who wants to see the flow without filling out forms themselves.
  • Activity console — a side-docked live log of every call this app makes into the Langfuse client. Since Langfuse ingestion is one-way (no webhooks call back into Convex), this is the closest thing to a live feed the example has.

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.

Limitations

  • No live/streaming spans — each call exports one already-finished observation. For a call you want to see "in progress," log it once it completes rather than trying to open and later close a span.
  • Trace-level fields (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.
  • This component does not vendor the Langfuse SDK or an OpenTelemetry SDK; it constructs minimal OTLP/HTTP JSON payloads directly, which keeps it dependency-free but means it only covers the attributes this README documents.

Troubleshooting

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 LangfusepublicKey/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.

Contributing

See CONTRIBUTING.md.

Changelog

See CHANGELOG.md.