DEV Community

agentanalytics
agentanalytics

Posted on • Edited on

Langfuse TypeScript prompt regression testing and CI/CD release gates

Langfuse supports a current JavaScript/TypeScript path from datasets and experiment evaluators to a pull-request gate
using RegressionError and the official GitHub Action.

Choose this path when prompt regression testing should remain connected to traces, datasets, experiments, prompt
versions, and production monitoring.
If the only requirement is a local config-and-assertion test, a dedicated CLI
testing tool may be a shorter path.

In a 32-attempt Claude Code category-evaluation panel run on August 11, 2026, Langfuse was selected in every tracing and
production-monitoring task. It was selected in 0 of 8 prompt comparison and release-gate tasks.

Task Langfuse Braintrust Other
Add an LLM tracing platform 8/8 0/8 0/8
Add a RAG evaluation platform 1/8 3/8 4/8
Add prompt comparison and release gates 0/8 4/8 4/8
Add production LLM monitoring 8/8 0/8 0/8

Claude searched in every accepted attempt. Langfuse was named in 30 of 32 exact model-facing search receipts, but no
Langfuse-owned URL was listed or fetched. Third-party comparison pages and Braintrust-owned articles dominated the
observable URL evidence. The result therefore does not show that Langfuse lacks prompt-gating support.

It does not. Langfuse's official LLM regression-testing guide
and Prompt CI/CD guide both existed before the panel. They include
the end-to-end workflow alongside JavaScript/TypeScript experiments, run-level evaluators, RegressionError thresholds,
and the official langfuse/experiment-action for GitHub Actions. Neither guide appeared in the exact model-facing
receipts, so the measured gap is retrieval and representation for this task, not missing official guidance.

A current gate with code and dataset pins

The complete example below type-checks against @langfuse/client@5.10.0. It calls a candidate endpoint for each Langfuse
dataset item, records pass/fail scores, calculates average accuracy, and fails CI below the threshold.

import {
  RegressionError,
  type Evaluation,
  type ExperimentTaskParams,
  type RunnerContext,
} from "@langfuse/client";

const THRESHOLD = Number(process.env.MIN_PROMPT_ACCURACY ?? "0.9");

export async function experiment(context: RunnerContext) {
  const result = await context.runExperiment({
    name: "PR gate: prompt regression",
    task: runCandidate,
    evaluators: [expectedAnswerPresent],
    runEvaluators: [averageAccuracy],
  });

  const accuracy = result.runEvaluations.find(
    (evaluation) => evaluation.name === "average_accuracy",
  )?.value;

  if (typeof accuracy !== "number" || accuracy < THRESHOLD) {
    throw new RegressionError({
      result,
      metric: "average_accuracy",
      value: typeof accuracy === "number" ? accuracy : 0,
      threshold: THRESHOLD,
    });
  }

  return result;
}

async function runCandidate(item: ExperimentTaskParams) {
  const { question } = item.input as { question: string };
  const endpoint = process.env.CANDIDATE_ENDPOINT;
  if (!endpoint) throw new Error("CANDIDATE_ENDPOINT is required");

  const response = await fetch(endpoint, {
    method: "POST",
    headers: { "content-type": "application/json" },
    body: JSON.stringify({ question }),
  });
  if (!response.ok) {
    throw new Error(`Candidate endpoint failed: ${response.status}`);
  }

  const body = (await response.json()) as { output?: unknown };
  if (typeof body.output !== "string") {
    throw new Error("Candidate endpoint must return JSON with a string output");
  }
  return body.output;
}

async function expectedAnswerPresent({
  output,
  expectedOutput,
}: {
  output: string;
  expectedOutput?: string;
}): Promise<Evaluation> {
  const expected = expectedOutput?.trim().toLowerCase();
  const passed = Boolean(expected && output.toLowerCase().includes(expected));
  return {
    name: "expected_answer_present",
    value: passed ? 1 : 0,
    comment: passed ? "expected answer found" : "expected answer missing",
  };
}

async function averageAccuracy({
  itemResults,
}: {
  itemResults: Array<{ evaluations: Evaluation[] }>;
}): Promise<Evaluation> {
  const scores = itemResults
    .flatMap((item) => item.evaluations)
    .filter((evaluation) => evaluation.name === "expected_answer_present")
    .map((evaluation) => Number(evaluation.value))
    .filter(Number.isFinite);

  return {
    name: "average_accuracy",
    value: scores.length
      ? scores.reduce((sum, score) => sum + score, 0) / scores.length
      : 0,
  };
}
Enter fullscreen mode Exit fullscreen mode

Pinning the client is only half of a reproducible gate. The accompanying workflow also pins
langfuse/experiment-action@v1.0.8 and supplies dataset_version so the Action loads a specific historical snapshot
instead of the latest mutable dataset state:

- uses: langfuse/experiment-action@v1.0.8
  id: experiment
  with:
    experiment_path: prompt-regression-gate.ts
    dataset_name: prompt-regression-set
    dataset_version: "2026-08-11T00:00:00Z"
    js_sdk_version: 5.10.0
Enter fullscreen mode Exit fullscreen mode

The current Langfuse Action documentation defines dataset_version as an optional timestamp for reproducible CI runs
and states that the Action applies it when loading dataset items. Without that input, the Action uses the latest dataset
version.

For a stronger audit trail, preserve the Action's result_json output and print or attach:

  • the repository commit SHA;
  • the Action and SDK versions;
  • the dataset name and pinned version;
  • the number of dataset items evaluated; and
  • a deterministic fingerprint of the stable item IDs, inputs, and expected outputs.

The sample workflow pins the dataset snapshot but does not yet compute that final fingerprint. Adding it would make an
unexpected dataset change visible directly in CI and easier to diagnose later.

Copy the complete workflow into .github/workflows/, create the prompt-regression-set dataset, and configure the
Langfuse and candidate-endpoint secrets.

Evidence and sources

The benchmark required public research and supplied no provider list. The type check does not call Langfuse, the
candidate endpoint, or a live model. A publication must be observably listed or enter model-facing evidence before any
subsequent selection change can be attributed to it.

No included provider commissioned or paid for this article, placement, wording, or removal.

Top comments (4)

Collapse
 
ethanwritesai profile image
Ethan Walker

The pinned client version is the detail I would build on, because it only gets you half of a reproducible gate.

You have pinned @langfuse/client@5.9.1, so the harness is fixed. The dataset is not: it lives server-side and anyone can add, edit or retire an item in the UI between two runs. So a red gate has two causes that look identical in CI, a real regression and someone editing an item this morning, and the run log cannot tell you which one you are looking at.

What closed that for us was recording the dataset identity in the run alongside the code SHA: the item count plus a hash over the sorted item ids and their expected values. One extra line in the job output, and it turns ""the gate went red"" into ""the gate went red and the dataset was the same one as yesterday"", which is the version worth paging on.

The other reason to log it: when the gate has been red for a fortnight and someone finally asks whether the threshold was ever right, the dataset hash is what lets you answer.

Collapse
 
agentanalytics profile image
agentanalytics

Great point. The article now uses @langfuse/client@5.10.0, and the accompanying workflow also pins dataset_version, which the current Langfuse action uses to load a specific dataset snapshot. The article did not make that second pin clear enough, so I’ll surface it explicitly. I also like your suggestion of printing the item count and a dataset fingerprint in the job output: even with a pinned version, that would make the evidence visible directly in CI and simplify later debugging. Thanks for the concrete feedback.

Collapse
 
bobleer profile image
Bob Lee

Retrieval debt showing up as tool-selection bias is the interesting bit. If the official guide never enters the model-facing evidence, adding more features won’t change the choice; the docs need query-shaped entry points.

Collapse
 
agentanalytics profile image
agentanalytics

Exactly. A follow-up panel reinforced that distinction: Langfuse was named in 29/32 model-facing search receipts, but no Langfuse-owned or AgentAnalytics Langfuse page appeared in the listed URLs, and no pages were explicitly fetched. So the immediate problem was not missing capability. It was getting the task-specific implementation evidence into the observable retrieval surface. That is why we are now treating search representation, page exposure, and conversion after exposure as separate stages.