Compare commits

...
254 Commits
Author SHA1 Message Date
Nimar d14d2114ba chore: release v3.174.0 2026-05-13 13:30:53 +02:00
Tobias WochingerandGitHub a7704185bd ci: tighten GitHub Actions cache policy (#13613)
* ci: tighten workflow cache policy

* ci: disable fork pr cache restores

* ci: fix workflow node version references
2026-05-13 10:03:27 +00:00
Niklas SemmlerandGitHub 65311ce628 feat(blob-export): expose exportSource and exportFieldGroups in public REST API (#13598)
## Summary

[LFE-9456](https://linear.app/langfuse/issue/LFE-9456) — final part of the stack. Exposes `exportSource` and `exportFieldGroups` on the public REST API for blob storage integrations.

- **Request**: `CreateBlobStorageIntegrationRequest` now accepts `exportSource` (optional, defaults to `TRACES_OBSERVATIONS`) and `exportFieldGroups` (`nullable<list<ExportFieldGroup>>`, optional).
- **Response**: `BlobStorageIntegrationResponse` now returns `exportSource` (non-null) and `exportFieldGroups` (`nullable<list<…>>`).
- **Strict validation**: REST contract is intentionally stricter than tRPC:
  - `TRACES_OBSERVATIONS` + non-null `exportFieldGroups` → 400 ("not applicable"). Covers `[]`, partial arrays, full arrays alike.
  - `EVENTS` / `TRACES_OBSERVATIONS_EVENTS` + provided `exportFieldGroups` without `core` → 400 (delegates to existing shared `validateExportFieldGroups`).
- **Source-conditional handler**: `TRACES_OBSERVATIONS` writes `undefined` (Prisma preserves the column / applies default for new rows) and reads return `null` to hide any inert legacy value. `EVENTS` family defaults to all 11 groups when omitted/null.
- **Fern source updated** with new `ExportSource` / `ExportFieldGroup` enums, request and response field additions, and rule docstrings. OpenAPI spec regenerated.

### Why the REST/tRPC divergence is intentional

The UI's tRPC path still submits all 11 groups for `TRACES_OBSERVATIONS` because the form always carries them; the shared `validateExportFieldGroups` doesn't enforce `core` for that source. The worker ignores the column entirely for `TRACES_OBSERVATIONS` (uses fixed-column exports). The REST contract should not expose a knob that's inert at export time — so it rejects on write and hides on read.

### Drive-by

Includes `openapi.yml` regen sweep for #13126 (the `every_20_minutes` enum value was added to Fern source but never regenerated). Generated artifact only; no behavior change.

### Impacted packages

- `web` — Zod request/response types, GET list + PUT handlers, server tests, regenerated OpenAPI spec
- `fern` — Fern source definition

## Test plan

- [x] `pnpm --filter web run lint`
- [x] `pnpm --filter web run typecheck`
- [x] `dotenv -e .env -- pnpm --filter web exec vitest run blob-storage-integration-api blob-storage-integration-trpc` — 50/50 passed (38 new REST + 12 tRPC regression)
- [x] `npx fern-api generate --api server` — Python SDK, TypeScript SDK, OpenAPI spec all regenerated cleanly
- [ ] Manual API client smoke test against staging once merged (verify SDK round-trip on EVENTS payload)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

<!-- greptile_comment -->

<details open><summary><h3>Greptile Summary</h3></summary>

This PR exposes `exportSource` and `exportFieldGroups` on the public REST API for blob storage integrations, with strict validation rules (TRACES_OBSERVATIONS rejects non-null field groups; EVENTS/TRACES_OBSERVATIONS_EVENTS requires `core` when groups are provided) and source-conditional response masking.

- **PUT handler**: correctly defaults a null/omitted `exportFieldGroups` to all 11 groups for `EVENTS`/`TRACES_OBSERVATIONS_EVENTS`, and passes `undefined` for `TRACES_OBSERVATIONS` so Prisma preserves the existing DB column without overwriting it.
- **GET handler and PUT response block**: both cast the raw DB `exportFieldGroups` value without applying the same null-→-all-groups default that the write path uses, meaning legacy `EVENTS` rows with a null DB column return `null` in the response rather than the full group list.
- **Test schema**: local `BlobStorageIntegrationResponseSchema` marks `exportSource` as `.optional()`, which is weaker than the production contract; tests would not fail if the field were accidentally dropped from the response.
</details>

<details><summary><h3>Confidence Score: 3/5</h3></summary>

The write path is correct and well-tested, but the read path has an inconsistency: GET returns raw DB null for EVENTS integrations with an unset exportFieldGroups column, while PUT always writes the full default list. Any legacy EVENTS row would expose this gap to API consumers.

The write-side logic (defaulting, masking, validation) is solid and tests cover it well. The inconsistency lives in the GET handler and the PUT response builder, both of which skip the null-to-all-groups normalization that the write path applies. For organizations with legacy EVENTS integrations (created before exportFieldGroups was populated), the GET response would return null for a field that should carry the full 11-group list, which could mislead consumers and break SDK round-trips.

web/src/pages/api/public/integrations/blob-storage/index.ts — both response-building blocks (GET and PUT) need the same null-defaulting logic that the write path already has for EVENTS/TRACES_OBSERVATIONS_EVENTS sources.
</details>

<details><summary><h3>Flowchart</h3></summary>

```mermaid
%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[PUT /integrations/blob-storage] --> B{Zod validation}
    B -->|exportSource = TRACES_OBSERVATIONS\nexportFieldGroups != null| C[400 not applicable]
    B -->|exportSource = EVENTS/TOE\nexportFieldGroups provided without 'core'| D[400 core required]
    B -->|valid| E{exportSource?}
    E -->|TRACES_OBSERVATIONS| F[pass exportFieldGroups: undefined\nPrisma skips column on update]
    E -->|EVENTS / TRACES_OBSERVATIONS_EVENTS| G[pass exportFieldGroups ?? all 11 groups]
    F --> H[upsertBlobStorageIntegration]
    G --> H
    H --> I{Build response}
    I -->|TRACES_OBSERVATIONS| J[exportFieldGroups: null]
    I -->|EVENTS/TOE| K[exportFieldGroups: DB value as-is\nno null → all-groups default]

    L[GET /integrations/blob-storage] --> M[fetch all org integrations]
    M --> N{for each integration}
    N -->|TRACES_OBSERVATIONS| O[exportFieldGroups: null]
    N -->|EVENTS/TOE| P[exportFieldGroups: DB value as-is\nno null → all-groups default]
    O --> Q[200 response array]
    P --> Q
```
</details>

<details><summary>Prompt To Fix All With AI</summary>

`````markdown
Fix the following 2 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 2
web/src/pages/api/public/integrations/blob-storage/index.ts:94-98
**GET doesn't normalize null `exportFieldGroups` for EVENTS sources**

The PUT handler defaults a null/omitted `exportFieldGroups` to all 11 groups for `EVENTS` and `TRACES_OBSERVATIONS_EVENTS` sources (`validatedData.exportFieldGroups ?? [...BLOB_EXPORT_FIELD_GROUPS]`). The GET handler here simply passes the raw DB value through, so a legacy `EVENTS` row where the column was never populated returns `null` instead of the full group list. A consumer reading the GET response on such a row sees `null`—indistinguishable from `TRACES_OBSERVATIONS`'s "not applicable" null—while a subsequent PUT would return the full list. The same cast-without-defaulting pattern repeats on the PUT response block at lines 213–217.

### Issue 2 of 2
web/src/__tests__/server/blob-storage-integration-api.servertest.ts:31-38
**Test schema marks `exportSource` optional — weaker than production contract**

The production `BlobStorageIntegrationResponse` schema requires `exportSource` as non-optional (it's always present in the response). Marking it `.optional()` in the test schema means the tests would pass even if the field were accidentally dropped from the API response, making the test suite weaker than intended for this new field.

`````

</details>

<sub>Reviews (1): Last reviewed commit: ["feat(blob-export): expose exportSource a..."](https://github.com/langfuse/langfuse/commit/8601e56bd7e996def93d14761a4419b607b77923) | [Re-trigger Greptile](https://app.greptile.com/api/retrigger?id=31769478)</sub>

> Greptile also left **2 inline comments** on this PR.

<!-- /greptile_comment -->
2026-05-13 11:46:32 +02:00
NimarandGitHub 692aa60054 chore(deps): bump protobufjs to at least 7.5.6 (#13612) 2026-05-13 09:07:27 +00:00
NimarandGitHub 1b0039671e chore(deps): bump turbo to 2.9.12 (#13599)
* chore(deps): bump turbo to 2.9.12

* fix
2026-05-12 15:58:12 +00:00
marliessophieandGitHub 053185ed98 fix(annotation): introduce conditional read from events table for batch action (#13585)
* fix(batch-actions): ensure read from events/observations conditionally

* tests: add
2026-05-12 12:32:01 +00:00
Hassieb PakzadandGitHub 544f934758 fix(llm-api-keys): scope update by project id (#13595) 2026-05-12 14:21:08 +02:00
NimarandGitHub 42907f78d2 chore(deps): bump otel sdk to 0.217.0 / 2.7.1 (#13581)
* chore(deps): dedupe

* chore(deps): bump otel sdk to 0.217.0

* consistent otel

* also bump to 2.7.1
2026-05-12 11:04:00 +00:00
e7cf58d6d3 fix(worker): add rows_dropped metric for ClickhouseWriter exhaust (#13488)
Emit langfuse.queue.clickhouse_writer.rows_dropped with entity_type tag
when rows are discarded after max flush attempts, alongside existing
error increments and logs.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-12 09:34:12 +00:00
Steffen SchmitzandGitHub 5a0e189b91 fix(sso): fall back to preferred_username/upn for Azure AD when email domain mismatches (#13465)
Some Entra tenants emit an external or personal address in the `email`
claim while the tenant UPN sits in `preferred_username` / `upn`. When the
email domain doesn't match the configured SSO domain, fall back to either
of those if they're valid emails on the configured domain so the
reverse-domain check in `auth.ts` doesn't reject the user.
2026-05-12 08:34:28 +00:00
Ben BachemandGitHub fc3680f9b1 chore(web): Use new logo (#13576) 2026-05-12 07:29:43 +00:00
Niklas SemmlerandGitHub 2e6c11f07b feat(blob-export): add configurable field groups for events export (#13493)
## Summary

[LFE-9456](https://linear.app/langfuse/issue/LFE-9456) — stacked on top of the DB migration PR.

Adds configurable field groups for the events blob export, covering the query layer, worker wiring, and UI.

**Query layer (`packages/shared`)**
- Rewrites `getEventsForBlobStorageExport` to select only the requested field groups instead of hardcoding all fields
- Adds two new field sets to the query builder: `trace_context` (tags, release, traceName, usagePricingTierName) and `model_export` (providedModelName, modelId, modelParameters — uses `model_id` alias for blob consumers)
- Extends `OBSERVATION_FIELD_GROUPS` from 9 → 11 groups (adds `tools`, `trace_context`)

**Worker (`worker`)**
- Wires `exportFieldGroups` from the Prisma record through `processBlobStorageExport` into the query function and `enrichObservationStream`
- Gates pricing enrichment (input/output/total price) on the `usage` field group — skips model lookup entirely when `usage` is not selected
- Drops `provided_model_name` and `model_parameters` from enrichment output when `model` group is not selected
- Default = all 11 groups, so output is identical to the previous hardcoded path

**UI (`web`)**
- Adds a multi-checkbox field group selector to the blob storage settings form, visible when `exportSource` is `EVENTS` or `TRACES_OBSERVATIONS_EVENTS`
- Resets `exportFieldGroups` to all groups on source switch to prevent silent validation failures
- Surfaces tRPC mutation errors via toast

**Validation**
- `core` group is required and non-deselectable in the UI
- Schema enforces `core` must be present; `exportFieldGroups` must be non-empty when `exportSource` is `EVENTS`
- Extracts `validateExportFieldGroups` as a reusable validator shared between tRPC and schema
2026-05-12 09:33:01 +02:00
Tobias WochingerandGitHub eda3dc5c18 fix(web): use prompt model config for experiments (#13565)
* fix(web): use prompt model config for experiments

* fix(web): simplify prompt config model selection
2026-05-12 07:12:37 +00:00
Max DeichmannandGitHub 08615fe741 docs(agents): add Datadog query recipes skill (#13575) 2026-05-11 20:42:16 +00:00
033616dda3 fix(playground): make tools list scrollable when more than 4 are attached (#13439)
The tools popover capped visible cards at ~4 with no working scrollbar
because `<ScrollArea max-h-[...]>` lands the height constraint on the
Radix Root. The Viewport's `h-full` doesn't resolve against a parent
with only `max-height` (CSS resolves `height: 100%` against parent
`height`, not `max-height`), so the Viewport sized itself to content,
never reported overflow, and Radix never activated its scrollbar.
Meanwhile the Root's `overflow: hidden` clipped the rest.

Apply the height constraint to the Viewport via a Tailwind arbitrary
child variant so Radix sees the overflow and renders its scrollbar.

Closes #13433

Co-authored-by: Nimar <l.nimar.b@gmail.com>
2026-05-11 17:57:39 +00:00
NimarandGitHub 5bb223a22c chore(deps): dedupe (#13580) 2026-05-11 17:15:26 +00:00
Ben BachemandGitHub 550e8bd5e7 fix(tracing): Disable score columns by default in events table (#13578) 2026-05-11 16:14:20 +00:00
Ben BachemandGitHub 1073d408a5 refactor(web): Clean up nextjs pages filetree (#13569) 2026-05-11 16:11:41 +00:00
Ben BachemandGitHub cb4c77d6a1 chore(web): Up @codemirror/search + tests for NFKD matching (#13577) 2026-05-11 16:07:20 +00:00
Ben BachemandGitHub dc44c5f854 fix(filters): fallback to key input on empty key options (#13570) 2026-05-11 14:50:45 +00:00
Ben BachemandGitHub 84952c0eb5 fix(web): Table row spacing consistency (#13568) 2026-05-11 14:49:58 +00:00
Ben BachemandGitHub 6d927256ea refactor(web): Migrate more icons to Spinner (#13573)
refactor(web): Migrate more icons to
2026-05-11 14:40:10 +00:00
Valery MeleshkinandGitHub 23aa702b5d chore: document max scores limit behaviour on scores v2 API route (#13567) 2026-05-11 15:08:54 +02:00
Max DeichmannandGitHub 3ab29b171f ci: add semgrep PR security scan (#13566) 2026-05-11 12:30:24 +00:00
Max DeichmannandGitHub 2982875d4a ci: add Claude Code security review workflow (#13556)
* ci: add Claude Code security review workflow

* ci: align Claude security review workflow

* ci: pin Claude security review actions

* ci: rename security review workflow

* ci: address security review feedback

* ci: restrict security review to trusted PRs
2026-05-11 12:17:38 +00:00
35830bcf7e fix(shared): validate outbound fetch DNS at connection time (#13554)
Harden secure outbound fetches against DNS rebinding by validating
connection-time lookup results with the existing outbound URL blocklist and
whitelist policy.

Co-authored-by: Codex Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-11 12:00:28 +00:00
Max DeichmannandGitHub eb95e6b08c docs(agents): route cross-repo context to navigator (#13562) 2026-05-11 13:51:16 +02:00
Max DeichmannandGitHub 42c48900b2 docs(agents): add architecture principles (#13564) 2026-05-11 13:47:33 +02:00
Max DeichmannandGitHub 844d17dac8 docs(skills): require human approval before Linear handoff (#13563)
* docs(skills): require human approval before Linear handoff

* docs(skills): avoid duplicate Linear approval prompts
2026-05-11 13:44:59 +02:00
Niklas SemmlerandGitHub 58d190e97c feat(blob-export): add exportFieldGroups DB column and tRPC passthrough (#13483)
Part 2 of [LFE-9456](https://linear.app/langfuse/issue/LFE-9456) — stacked on top of the test-coverage PR.

- Adds `export_field_groups TEXT[] NOT NULL DEFAULT ARRAY[...]` column to `blob_storage_integrations`
- Adds `BLOB_EXPORT_FIELD_GROUPS` client-safe constant to `@langfuse/shared` (analytics-integrations module)
- Adds `exportFieldGroups` to the Zod form schema (`types.ts`) with `min(1)` validation and all-groups default
- Wires `exportFieldGroups` through `upsertBlobStorageIntegration` service and tRPC `update` mutation
- No behaviour change: default = all 11 groups = today's output

The 11-group default includes `tools` and `trace_context` which the query builder doesn't handle yet (PR 3). These values are stored but ignored by the worker until PR 3 is merged.
2026-05-11 13:17:02 +02:00
Max DeichmannandGitHub 5e5ed8a585 docs(agents): add cloud cost analysis skill (#13555) 2026-05-11 09:44:19 +00:00
Niklas SemmlerandGitHub b94a230c53 test: add column contract tests for blob export and API v2 field groups (#13481)
Part 1 of 4 for [LFE-9456](https://linear.app/langfuse/issue/LFE-9456) (export field group selection for blob storage integrations). No implementation changes — establishes regression baselines before the refactor.
2026-05-11 11:40:08 +02:00
Ben BachemandGitHub 4dab005c98 chore(web): Setup storybook (#13472) 2026-05-11 09:19:47 +00:00
marliessophieandGitHub 05f77cf53d refactor(trace): rename folder and remove code duplication (#13492)
* refactor(trace): rename folder from `trace2` to `trace`

* docs: rm `trace2` from code comments

* fixup: delete trace and observation preview files

* refactor(trace): update badge rendering logic in Observation and Trace detail views to conditionally display based on annotation mode

* chore: push

* fix: ensure observation id is selected
2026-05-11 09:13:48 +00:00
marliessophieandGitHub ea14d09dac fix(annotation-queues): fetch parent trace id from events table on cloud (#13510)
* fix(annotation-queues): fetch parent trace id from events table on cloud

* fix: assignment

* docs: doc string
2026-05-11 09:01:33 +00:00
NimarandGitHub c4f50a1e3f fix(tool-parsing): ai sdk handle stringified jsons as well (#13550)
* fix(tool-parsing): ai sdk handle stringified jsons as well

* fix parse

* ai sdk e2e test

* only count model called tool calls as calls

* comment
2026-05-11 08:29:52 +00:00
c1af23ac29 fix(scim): block removing last organization owner (#13530)
* fix(scim): block removing last organization owner

SCIM DELETE, PUT(active:false), and PATCH(active:false) deprovisioning paths
unconditionally removed the target user's organization membership, allowing
the last OWNER to be deleted and orphaning the organization.

Mirror the tRPC `deleteMembership` invariant: count remaining OWNERs and
reject with 403 when the request would remove the final OWNER. The error
body uses the SCIM error schema with the same message as the tRPC path.

Tests cover all three deprovisioning verbs against a sole owner plus a
positive control where a second OWNER exists.

Resolves INT-1223.

* fix(scim): wrap last-owner check + delete in serializable txn

Closes a TOCTOU race where two concurrent SCIM deprovision requests with
exactly two OWNERs could both pass the owner-count guard and both delete,
leaving the org with zero owners. The check and delete now run inside a
single Prisma transaction with Serializable isolation; on a serialization
failure (P2034) the endpoint returns 409 so the SCIM client retries.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 06:21:37 +00:00
Max DeichmannandGitHub bf6e490b92 fix(agents): harden shared setup docs (#13548) 2026-05-10 18:37:55 +02:00
Max DeichmannandGitHub 441c51953c docs(agents): add production regression triage skills (#13547) 2026-05-10 18:30:12 +02:00
Max DeichmannandGitHub 66a1e99335 fix(web): hide org API keys tab without key access (#13545)
* Hide org API keys tab without access

* test(web): cover org api key entitlement gate
2026-05-10 15:47:11 +00:00
Steffen SchmitzandGitHub 351a5aefad fix(rbac): restrict organization API key management to OWNER (#13539)
ADMIN previously held organization:CRUD_apiKeys, allowing any ADMIN
to mint organization-scoped API keys. Combined with SCIM PUT not
enforcing role hierarchy, this enabled ADMIN -> OWNER privilege
escalation (INT-1222). Removing the scope from ADMIN closes the
escalation primitive at the key-creation boundary; OWNER remains
the only role that can create, list, update, or delete org API keys.
2026-05-09 08:32:29 +00:00
NimarandGitHub 2466d4ce9b chore(deps): bump fast-uri to 3.1.2 (#13538) 2026-05-09 06:58:12 +00:00
Max DeichmannandGitHub 0634ddab71 fix: reuse webhook fetch for remote experiments (#13536)
* Harden remote experiment fetch against SSRF

* fix: address outbound fetch review comments

* fix: remove outbound fetch dispatcher hardening
2026-05-08 19:30:03 +00:00
Max DeichmannandGitHub af55949254 fix(blobstorage-integration): add audit logs for validate and runNow (#13535)
* Log blob storage integration validate and runNow actions

* fix(blobstorage-integration): guard success audit logs
2026-05-08 19:27:16 +00:00
NimarandGitHub c224589849 chore(deps): bump fast-uri to 3.1.1 (#13537) 2026-05-08 18:45:11 +00:00
Max DeichmannandGitHub 492f14481d fix(rbac): keep invite totalCount aligned with project filters (#13518)
Fix invite pagination totalCount mismatch
2026-05-08 18:19:21 +00:00
Max DeichmannandGitHub ce18027cf8 fix(datasets): validate remote experiment URLs before saving (#13520)
* Validate remote experiment URLs before saving

* fix(datasets): reuse webhook validation for remote experiments

* test(datasets): strengthen remote experiment upsert assertion
2026-05-08 18:18:52 +00:00
NimarandGitHub 9fdb53fa04 chore(deps): bump fast-xml-builder to 1.1.7 (#13534) 2026-05-08 17:49:54 +00:00
NimarandGitHub cb0aa34291 fix(tool-parsing): ai sdk parsing of tools (#13533)
* fix(tool-parsing): ai sdk parsing of tools

* fix parsing
2026-05-08 17:46:25 +00:00
Mark SalpeterandGitHub 9259f78c1a fix(sso): reduce multi-tenant SSO config cache TTL to 10 minutes (#13525) 2026-05-08 15:24:12 +00:00
f0d5e633a9 fix(security): rate limit org-admin REST endpoints (#13529)
Adds RateLimitService check (after auth + admin-api entitlement gates)
to the three org-scoped admin handlers so that compromised
organization-scoped API keys can no longer issue unbounded writes or
probe global user existence at full request rate.

Resolves INT-1270.

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-08 15:11:48 +00:00
Steffen SchmitzandGitHub 0bd8f740fe fix(scim): normalize userName casing in user POST flow (#13528)
fix(scim): normalize userName casing in user POST flow (INT-1320)

The SCIM POST flow checked for an existing user with the case-preserved
userName but performed the upsert with a lowercased email. With case-sensitive
uniqueness on User.email, a case-variant userName slipped past the duplicate
check and either linked to an unrelated existing user or hit a unique
constraint instead of returning 409.

Lowercase userName once and reuse it for both the existing-user lookup and
the upsert. Adds a server test covering case-variant duplicate detection.
2026-05-08 15:06:11 +00:00
e7d97dc833 feat(web): Add more default views to v4 traces view (#13452)
* feat(web): Add more default views to v4 traces view

* Improve merging of table view presets

* Resolve PR comments

* Update root observation preset

Co-authored-by: Nimar <l.nimar.b@gmail.com>

---------

Co-authored-by: Nimar <l.nimar.b@gmail.com>
2026-05-08 14:58:16 +00:00
Hassieb PakzadandGitHub 10c9758054 fix(llm-completions): handle Bedrock reasoning content (#13527) 2026-05-08 16:35:47 +02:00
Steffen SchmitzandGitHub 6aff5f6bdc fix(auth): enforce AUTH_DOMAINS_WITH_SSO_ENFORCEMENT on the email-OTP path (#13526)
The signIn callback consulted only the cloud-only multi-tenant SSO
provider check, which is a no-op on self-hosted instances. This left
the password-reset OTP path as an alternate authentication channel
for users on domains that AUTH_DOMAINS_WITH_SSO_ENFORCEMENT was
meant to lock down. Block the email provider for enforced domains
the same way the credentials authorize() and signup handler do.
2026-05-08 14:03:40 +00:00
Steffen SchmitzandGitHub cfb88be630 chore: add CH-insert based seeder documentation (#13504) 2026-05-08 12:29:49 +00:00
0ec50b3258 feat(auth): email verification on signup (LFE-8709) (#12427)
* feat(clickhouse): add analytics_events_core view for project-level analytics (LFE-8734)

Adds a ClickHouse VIEW on events_core with per-project, per-hour aggregations
including type/source/scope/SDK counts via sumMap, unique counts via uniqIf
and uniqArray, and has_* boolean flags for feature detection.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat(auth): add email verification on signup gated behind AUTH_EMAIL_VERIFICATION_REQUIRED (LFE-8709)

Add an optional OTP email verification step before user creation during
email/password signup. When AUTH_EMAIL_VERIFICATION_REQUIRED=true (and
SMTP is configured), the signup flow becomes: enter email+name → receive
OTP → verify code → set password. Self-hosters without SMTP or without
the flag keep the current direct signup behavior. SSO/social logins are
unaffected.

Key changes:
- New env var AUTH_EMAIL_VERIFICATION_REQUIRED
- New POST /api/auth/signup-verify endpoint (creates passwordless user)
- New /auth/setup-password page for initial password setup
- Merged set/reset password into one ResetPasswordPage component
- Context-aware email template (welcome vs reset wording)
- hasPassword added to session for mode detection
- Direct /api/auth/signup blocked when verification is required
- Parameterized email verification cutoff (default 10 minutes)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-08 12:25:14 +00:00
da0e339095 fix(dashboards): Use correct units for charts (#13338)
* fix(dashboards): Use correct units for charts

* Fix view version logic

* Support value formatter in `BigNumber` and `HistogramChart`

* Fix value formatter usage

* Resolve PR comments

* Improve formatting single digit millisecond values

* Introduce `formatMetric`

* Fix chart label in `LatencyChart`

* Remove unit form latency label in `score-analytics-utils.ts`

* fix rounding to m for 1000k

* more compact tests

* compact

---------

Co-authored-by: Nimar <l.nimar.b@gmail.com>
2026-05-08 11:30:46 +00:00
Mark SalpeterandGitHub 4c9fc1c241 fix(web): refresh sso configs after domain verification (#13522) 2026-05-08 09:35:28 +00:00
Max DeichmannandGitHub 9b7daaf704 fix(web): emit audit log for public blob storage deletion (#13517)
Add audit log for blob storage deletion
2026-05-08 09:05:31 +00:00
Max DeichmannandGitHub d676ee2c01 fix(annotation-queues): enforce read scope in typeById (#13519)
Fix annotation queue item access check
2026-05-08 08:44:22 +00:00
Nimar 19449c25f4 chore: release v3.173.0 2026-05-08 10:47:01 +02:00
8c66f62141 ci: harden prettier check file arguments (#13513)
* ci: harden prettier check file arguments

Prevent changed filenames from being interpreted as prettier options in CI.

Co-Authored-By: Codex Opus 4.6 (1M context) <noreply@anthropic.com>

* ci: ignore deleted files in prettier check

Exclude deleted paths so delete-only changes do not pass missing files to Prettier.

Co-Authored-By: Codex Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Codex Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-08 07:47:01 +00:00
NimarandGitHub f1a61cfe1d chore(deps): bump nextjs to 16.2.6 (#13516)
* chore(deps): bump nextjs to 16.2.6

* add exculsion
2026-05-08 07:42:05 +00:00
849ef156e8 fix(shared): reject DNS-failing hostnames in outbound URL validation (#13512)
* fix(shared): reject DNS-failing hostnames in outbound URL validation

The LLM base URL validator silently bypassed the IP blocklist whenever
DNS resolution failed (NXDOMAIN/SERVFAIL/timeouts/split-horizon), which
enabled DNS-rebinding SSRF against cloud metadata and internal services.
Treat DNS failure as a hard error and drop the per-caller opt-out flag;
self-hosters must use LANGFUSE_LLM_CONNECTION_WHITELISTED_HOST for
gateways the validator cannot resolve. Closes INT-1226.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(web): use resolvable placeholders in llm-api-key servertest

The new strict DNS validation rejects custom.openai.com / new-custom.openai.com
/ new-endpoint.example.com because they NXDOMAIN. Swap to IANA-reserved
example.com / example.org / example.net which always resolve to public IPs
and pass the IP blocklist.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 19:25:12 +00:00
871a8d5a07 feat(sso): self-service SSO config with DNS-verified domains (#13507)
* feat(sso): add DNS-based verified domains

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(sso): add ssoConfig tRPC router

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(sso): require https:// on user-supplied OIDC issuer urls

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(sso): show zone-relative host and query fresh DNS for domain verification

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(sso): add per-domain self-service SSO config UI

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(sso): pre-flight OIDC discovery on ssoConfig.save and the legacy support endpoint

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(sso): enforce verified-domain invariant on SsoConfig lifecycle

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(sso): include name field on custom OIDC provider form and payload

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(sso): translate P2002 race on verifiedDomain.create to CONFLICT

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(sso): allow pending verified-domain claims to coexist across orgs

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(sso): require non-empty Azure AD tenantId on save

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(sso): restore URL grammar validation on OIDC issuer and GH Enterprise baseUrl

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(sso): refuse redirects on OIDC discovery fetch (SSRF defense)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(sso): surface schema errors for github-enterprise baseUrl in the form

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(sso): gate verifiedDomain mutations on the SSO entitlement

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(sso): preserve advanced authConfig fields when re-saving the same provider

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(sso): skip orphan-prevention check on pending verified-domain deletes

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(sso): accept Azure AD multi-tenant {tenantid} placeholder in OIDC discovery

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(sso): require non-empty name on custom OIDC provider

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(sso): clarify verified-domain delete dialog copy when SSO config exists

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(sso): satisfy codespell and clean up validation messages

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 16:30:04 +00:00
marliessophieandGitHub a5a197e1d1 fix(web): remove Request Chart button from home screen (#13509)
* fix(web): remove request chart button from home screen

* chore(web): remove unused feedback button wrapper
2026-05-07 16:09:11 +00:00
NimarandGitHub 8e4c2cc622 chore(deps): bump ip-addresses to 10.2.0 (#13505) 2026-05-07 12:13:09 +00:00
Ben BachemandGitHub 8ca61cf846 chore(web): Setup in-source testing with Vitest (#13484) 2026-05-07 12:04:27 +00:00
c99012235c fix(projects): persist parsed metadata on project create/update (#13497)
* fix(scim): write audit log on user creation via SCIM POST

POST /api/public/scim/Users now emits an auditLog entry for the
created organizationMembership, matching the tRPC members.create
behavior. Without this, an ADMIN using SCIM to add users with
arbitrary roles left no audit trail (INT-1250).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(projects): persist parsed metadata on project create/update

handleCreateProject and handleUpdateProject parsed string metadata
via JSON.parse for validation but discarded the parsed value and
wrote the raw string into Prisma. As a result, metadata sent as a
JSON string was stored as a string-typed JSON value instead of the
intended object (INT-1338).

Capture the parsed value and persist it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(projects): reject non-object project metadata

After parsing the metadata JSON string the value can still be null,
a primitive, or an array. Persisting those in the Json? column
violated the API contract, and JS null in particular wrote SQL NULL
to Prisma — silently wiping any existing metadata on update.

Add a shape check after JSON.parse on both create and update paths
to reject non-object metadata with 400. Adds regression tests for
"null", arrays, numbers, strings, and a direct JS null.

Addresses review feedback on PR #13497.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(projects): explicit coverage for omitted metadata

Document the contract that omitting metadata is valid:
- create without metadata returns {} and stores NULL
- update without metadata preserves the existing object

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 10:33:16 +00:00
Tobias WochingerandGitHub 290fcf2c84 fix(web): validate image URL redirects (#13501)
* fix(web): validate image URL redirects

Validate validateImgUrl redirect targets with shared outbound URL checks to prevent SSRF via automatic redirect following.

* fix(web): avoid duplicate image URL validation
2026-05-07 10:00:11 +00:00
Tobias WochingerandGitHub dafbc986e3 fix(worker): preserve encrypted webhook headers on disable (#13503)
Patch only lastFailingExecutionId when disabling failed automations so decrypted execution config cannot overwrite encrypted webhook headers at rest.
2026-05-07 09:50:22 +00:00
Ben BachemandGitHub d74059f54c fix(traces): Create synthetic traces from events consistently (#13450)
fix(traces): Create virtual traces from events consistently
2026-05-07 09:44:36 +00:00
210c5088ea fix(public-api): rate-limit project apiKeys admin and prompt POST (#13498)
Three legacy Next.js handlers authenticate directly via ApiAuthService
without going through createAuthedProjectAPIRoute and were not invoking
RateLimitService:

- projects/[projectId]/apiKeys (GET/POST) — backdoor-credential minting
  via caller-chosen publicKey/secretKey was unbounded with a leaked
  org-scoped key (INT-1271)
- projects/[projectId]/apiKeys/[apiKeyId] (DELETE) — unbounded enumeration
  / churn of project API keys (INT-1265)
- prompts POST — unlimited prompt-version writes; GET already used the
  "prompts" rate-limit bucket but POST silently bypassed it (INT-1260)

Add RateLimitService.rateLimitRequest with isRateLimited() short-circuit
after auth/entitlement checks. Uses "public-api" for the apiKeys admin
endpoints and "prompts" for the prompt POST, matching the bucket the GET
branch already consumes.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 07:48:42 +00:00
d6b549be14 fix(scim): write audit log on user creation via SCIM POST (#13496)
POST /api/public/scim/Users now emits an auditLog entry for the
created organizationMembership, matching the tRPC members.create
behavior. Without this, an ADMIN using SCIM to add users with
arbitrary roles left no audit trail (INT-1250).

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 07:21:15 +00:00
Ben BachemandGitHub 63fc5959fc fix(web): Saved views UX improvements (#13454)
* fix(web): Expand sidebar filters when selecting a saved view

* fix(web): Always consider saved view filter values regardless of backend options
2026-05-07 06:10:29 +00:00
96dc4ef208 fix(shared): harden outbound URL validation against SSRF bypasses (#13485)
* fix(shared): parse webhook URLs before validation

Validate webhook hostnames from the original WHATWG-parsed URL and reject embedded URL credentials to prevent parser mismatch SSRF bypasses.

* refactor(shared): dedupe outbound URL host validation

Share the parse-original and host/IP validation path between webhook and LLM base URL validation, and cover the LLM parser mismatch regression.

* fix(shared): block IPv6 transition SSRF ranges

Block NAT64 and 6to4 IPv6 ranges that can embed IPv4 destinations.

* fix(shared): strip credentials on cross-origin redirects

Co-Authored-By: Codex Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(shared): cover local DNS SSRF gaps

Co-Authored-By: Codex Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(worker): strip secret webhook headers on redirects

Co-Authored-By: Codex Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Codex Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-06 15:42:15 +00:00
25a0ec469a fix(trace-ui): prevent image flicker on validateImgUrl false (#13440)
Co-authored-by: Nimar <l.nimar.b@gmail.com>
2026-05-06 15:21:07 +00:00
63f81db838 feat(worker): add secondary otel ingestion queue (#13490)
* feat(worker): add secondary otel ingestion queue

Mirrors the existing secondary ingestion queue pattern for the OTel pipeline
so high-throughput projects can be redirected to a dedicated processing pool
via LANGFUSE_SECONDARY_OTEL_INGESTION_QUEUE_ENABLED_PROJECT_IDS.

Refs LFE-6579.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore: remove unused values

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 14:10:31 +00:00
Ben BachemandGitHub f3a901c005 chore: Create eslint plugin package (#13444)
* chore: Create eslint plugin package

* Resolve PR comments

* Use correct version of @types/node in eslint-plugin
2026-05-06 14:00:09 +00:00
NimarandGitHub 8fa7572987 chore(deps): bump posthog 5.32 / 1.372 (#13487)
* chore(deps): bump posthog 5.32 / 1.372

* fix @ungap/structured-clone to 1.3.1
2026-05-06 11:58:33 +00:00
2caa429eaf chore(deps): web - build migrate binary with Go 1.26 (#13486)
Co-authored-by: Nimar <l.nimar.b@gmail.com>
2026-05-06 11:38:56 +00:00
6cd1101484 refactor(web): Create new design system dir & extract Spinner (#13428)
* Initial readme

* Updated readme

* Create `Spinner` component

* Split out `size` prop

* Use semantic size names

* Update readme

* Split out `display` prop

* Rename variants

* Cleanup component

* Fix readme

* Migrate remaining `Loader2` icons

* Use size instead of h- and w- classes

---------

Co-authored-by: Nimar <l.nimar.b@gmail.com>
2026-05-06 11:26:04 +00:00
Max DeichmannandGitHub 25365ba034 fix(events): stringify batchIO metadata for tRPC (#13457)
* fix(events): stringify batchIO metadata for tRPC

* test(events): avoid ClickHouse dependency in batchIO regression

* refactor(events): reuse shared metadata parser

* refactor(events): remove redundant metadata parsing

* fix(events): stringify batchIO input and output

* fix(events): avoid unexported adapter test type

* refactor(events): clarify stringified batchIO fallback

* fix(events): stringify byTraceId observation metadata

* fix(events): sanitize byTraceId observation payload

* docs(events): update stringified metadata example

* fix(events): remove recursive payload sanitizer

* fix(events): return batch IO strings from repository

* test: remove redundant metadata helper test

* test: remove events trpc server test

* refactor(events): simplify batch io output types

* refactor(events): simplify parsed observation type
2026-05-05 19:49:23 +00:00
Łukasz JernaśandGitHub bb7928a1ba fix(web): Prisma also returns "Unique constraint failed", so check lowercase string (#13477)
Prisma also returns "Unique constraint failed", so check lowercase string
2026-05-05 20:47:44 +02:00
Max DeichmannandGitHub d0642a6d7c chore: add migration hints for legacy public ClickHouse APIs (#13475)
* Add migration hints for legacy public ClickHouse APIs

* fix(public-api): simplify ClickHouse migration hints

* fix(public-api): configure ClickHouse advice via middleware options

* fix(public-api): refine ClickHouse migration hint wording

* fix(public-api): clarify cloud-only migration hints

* style(public-api): format migration hint routes
2026-05-05 16:17:36 +00:00
marliessophieandGitHub c0ee8d29ea fix(evals): add evaluator filter validation and handling (#13474)
* fix(evals): add evaluator filter validation and handling

* refactor(evals): rename normalizedFilter to validatedFilters and update related logic

* test: adjust
2026-05-05 14:40:30 +00:00
Tobias WochingerandGitHub 5d0171aece feat(experiments): show metadata in overview (#13456)
* feat(experiments): show metadata in overview

* fix(experiments): polish experiment overview layout

* fix(experiments): wrap overview metadata links

* fix(experiments): address overview metadata review

* fix(experiments): clean up metadata overview review fixes
2026-05-05 13:10:54 +00:00
92be83f30b fix(docker): remove corepack cache from runtime-base stage (#13470)
The node:24-alpine base image pre-populates /root/.cache/node/corepack/
when corepack is enabled during the build. Removing the module directory
alone left this cache intact in the final image, giving Snyk something
to scan. Add it to the rm -rf in both web and worker runtime-base stages.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-05 13:07:29 +00:00
Max DeichmannandGitHub 0b9c464c73 fix(worker): keep spend alert billing skips healthy (#13467) 2026-05-05 11:53:29 +00:00
8dd5d0a088 fix(web): include today in Prompts table observation count window (#13415)
The "Number of Observations (7d)" column on the Prompts table was passing
toTimestamp = end of yesterday, which excluded every observation with a
start_time during the current day. New prompt calls therefore never
appeared to increment the counter, and prompts only used today showed 0.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Nimar <l.nimar.b@gmail.com>
2026-05-05 09:19:21 +00:00
c57b68e492 fix(widgets): render latency metrics in scaled units in custom dashboard widgets (#13242)
* feat(widgets): use latency formatter for millisecond measure units

Adds getMeasureUnit helper to dataModel and wires latencyFormatter into
both DashboardWidget and WidgetForm preview when the selected measure
unit is "millisecond".

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(widgets): add day unit to latency formatter

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(widgets): apply latency formatter to pivot table values

Threads the existing valueFormatter prop from Chart through to PivotTable
so latency metrics render in auto-scaled units (ms/s/min/hr/day) instead
of raw milliseconds, matching the other chart widget types.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(widgets): memoize valueFormatter with unit switch

Replaces the inline measureUnit ternary with a memoized switch keyed on
measureUnit, making it trivial to add more unit-to-formatter mappings
(usd, tokens, etc.) as they arrive.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(widgets): apply latency formatter to histogram bin labels

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(widgets): apply value formatter to vertical bar y-axis ticks

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(widgets): apply value formatter to pie center label and pad tooltip rows

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(widgets): trim latency formatter

Drop unused latencyFormatterParts export and stop padding latency labels
with trailing zeros (1.00s -> 1s).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(widgets): pick formatter from agg-aware result unit and add USD

Add getResultUnit alongside getMeasureUnit so count/uniq aggregations
resolve to "integer" instead of inheriting the source measure's unit
(e.g. count(latency) no longer renders as milliseconds). Both
DashboardWidget and WidgetForm now derive the formatter from
getResultUnit and switch on the result, with a new USD branch routing
to usdFormatter alongside the existing millisecond -> latencyFormatter.
WidgetForm's inline ternary becomes a useMemo to mirror DashboardWidget.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(widgets): per-column pivot formatting via units overlay

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(widgets): drive chart formatting from chartConfig.unit

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* improvement(widgets): cleaned up some dead code

* improvement(formatting): reduced allocations of time duration formatters

* perf(widgets): memoize chart valueFormatter

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(widgets): route sub-millesimal values through compactSmallNumberFormatter

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(widgets): merge sanitized defaultSort back into pivot chartConfig

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(widgets): scale negative latencies in latencyFormatter

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(widgets): preserve precision for sub-unit magnitudes

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-04 16:26:33 +00:00
6ad92e17a0 chore(deps): remove redundant @types/uuid devDependency (#13448)
uuid v7+ ships bundled TypeScript declarations ("types": "./dist/index.d.ts"),
so @types/uuid is redundant after the v9→v14 upgrade and risks the stale
v9-era DefinitelyTyped types shadowing the bundled ones.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-04 13:57:56 +00:00
Ben BachemandGitHub c8668952a1 fix(organizations): add margin to separator when no project is selected (#13445) 2026-05-04 09:49:42 +00:00
755ac76ba6 fix(web): Improve toast title for ClickHouseResourceError errors (#13373)
Co-authored-by: Nimar <l.nimar.b@gmail.com>
2026-05-04 09:31:15 +00:00
Ben BachemandGitHub 8d826cf8fb chore(web): Remove unused unified & remark dependencies (#13409) 2026-05-04 09:31:02 +00:00
Ben BachemandGitHub 9cff62e3c7 chore(web): Remove unused graphql dependency (#13410) 2026-05-04 09:30:56 +00:00
Max DeichmannandGitHub b8bd27c14f refactor(model-match): remove redis parse span (#13182) 2026-05-04 09:14:16 +00:00
Max DeichmannandGitHub 645ad5b343 chore: Increase admin access webhook dedupe window to 24 hours (#13414)
Increase admin access webhook dedupe window to 24 hours
2026-05-04 09:13:52 +00:00
8cf3f51f90 chore(deps): upgrade uuid v9 → v14 (#13443)
chore(deps): upgrade uuid from v9 to v14 to resolve Snyk alert

Snyk flagged uuid@9 for improper index validation. The package is only
used for v4() random ID generation with no untrusted input, so not
exploitable, but upgrading clears the alert cleanly.

uuid v14 requires Node 20+ (we run Node 24) and keeps the same
import API (import { v4 } from "uuid").

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-04 09:10:47 +00:00
Max DeichmannandGitHub 5ddad9b13d chore: upgrade bullmq to 5.76.3 (#13442)
Upgrade bullmq to 5.76.3
2026-05-04 08:40:36 +00:00
marliessophieandGitHub c9e355928f fix(batch-actions): compute count subject to searchQuery and searchType (#13441) 2026-05-04 08:11:32 +00:00
Hassieb PakzadandGitHub 0256db0067 fix(evals): validate evaluator mapping target server-side (#13430) 2026-05-02 02:40:23 +09:00
Hassieb PakzadandGitHub d17485b920 fix(evals): do not drop langfuseObject on config on template upgrades (#13429) 2026-05-02 01:00:10 +09:00
Nimar 7fb4a68923 chore: release v3.172.1 2026-05-01 17:01:30 +09:00
Tobias WochingerandGitHub 3c38dba48f fix(traces): refresh scores in trace detail (#13427) 2026-05-01 06:59:23 +00:00
Tobias WochingerandGitHub 81e1ba3120 test: deflake DNS-dependent CI tests (#13412) 2026-04-30 06:04:49 +00:00
Tobias WochingerandGitHub e748cc102f chore(ci): disable test sharding + speed up test suite (#13383)
* ci: disable web test sharding

* test: isolate comment fixtures

* test: make score update fixture deterministic

* test: report slowest vitest tests

* ci: install only chromium for e2e tests

* test: speed up slow server tests

* test: show slowest vitest tests only in ci

* ci: re-enable web test sharding

* test: reduce ingestion and rate limit test latency

* test: report top 50 slowest vitest tests

* test: parallelize prompt name validation cases

* ci: limit vitest server workers

* test: stabilize score comparison sampling test

* test: show slowest worker tests only in ci

* ci: disable web test sharding

* test: reduce slow trace and annotation fixtures

* test: report slowest vitest files

* test: reduce slow trace and score fixtures

* test: reuse score and prompt list fixtures

* test: streamline slow server fixtures

* test: use valid dataset list limit

* test: stabilize and speed up worker tests

* test: isolate dataset item backfill assertions

* test: speed up API key fixture creation

* test: keep legacy api auth coverage explicit

* ci: skip duplicate next typecheck in test builds

* ci: build dependencies before typecheck

* ci: install playwright headless shell

* test: retry flaky vitest tests in ci

* ci: run prisma generate without turbo cache

* test: isolate dataset schema fixtures for retries
2026-04-30 02:03:35 +00:00
2719f8baff chore(eslint): Disallow use of overflow-scroll classes (#13403)
ref(web): Disallow use of `overflow-scroll` classes

Co-authored-by: Nimar <l.nimar.b@gmail.com>
2026-04-30 01:00:27 +00:00
Nimar 03be9523d1 chore: release v3.172.0 2026-04-29 14:47:36 +09:00
Ben BachemandGitHub b92e60618c chore(web): Track error notification clicks (#13386) 2026-04-29 03:52:38 +00:00
Ben BachemandGitHub 1ac51eb648 chore(web): Remove @mui/material dependency (#13399) 2026-04-29 03:39:44 +00:00
f665b40eb2 fix(traces): Show correct title prefix in trace detail peek view (#13283)
* refactor(web): Simplify `TablePeekView` props

* fix(traces): Show trace id in trace peek view title

* fix(web): Align observation peek title with trace detail view title

* fix(web): Align trace peek title with trace detail view title

* fix(web): Apply same fix as 2f235d831 to trace peek detail view

---------

Co-authored-by: Nimar <l.nimar.b@gmail.com>
2026-04-29 03:39:23 +00:00
ed8207058a fix(clickhouse): use alter_sync/mutations_sync on multi-ALTER clustered migrations (#13398)
* fix(clickhouse): set alter_sync/mutations_sync on multi-ALTER clustered migrations

Back-to-back ALTERs on the same table in a single migration file race the
replicated metadata-version update on ReplicatedMergeTree / SharedMergeTree
(ClickHouse Cloud), where alter_sync defaults to 0 and the second statement
can land on a replica whose metadata version still lags Keeper, producing
CANNOT_ASSIGN_ALTER (517) during initial bootstrap.

Add the per-statement settings to every clustered migration that issues
multiple ALTERs on the same table:
- alter_sync = 2 on metadata ALTERs (ADD/DROP/MODIFY COLUMN, ADD/DROP INDEX)
- mutations_sync = 2 on mutation-creating ALTERs (MATERIALIZE INDEX) so the
  index is fully built on all replicas before the migration returns

Covers 0005, 0006, 0008, 0025, 0026, 0031. The unclustered/ mirror runs on
plain MergeTree where these settings are no-ops, so it is left untouched.

Document the rule and the metadata-vs-mutation distinction in the
clickhouse-best-practices skill so future migrations follow the convention.
Validated end-to-end by bootstrapping migrations 1-34 against a fresh
ClickHouse Cloud instance with no 517 errors.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(clickhouse): use alter_sync on ADD INDEX in 0013/0015/0016/0018

These four pre-existing migrations applied SETTINGS mutations_sync = 2 to
both ALTERs, but mutations_sync is a no-op on metadata ALTERs like
ADD INDEX, so the metadata-version race that this PR is fixing in
0005/0006/0026 still applied here. Switch the ADD INDEX line in each to
SETTINGS alter_sync = 2 (the MATERIALIZE INDEX line stays on
mutations_sync = 2). Caught by review on PR #13398.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* cleanup

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 03:36:37 +00:00
NimarandGitHub 1c03673a9d chore(deps): bump next to 16.2.4 (#13402) 2026-04-29 03:34:11 +00:00
NimarandGitHub 1f9bce893b chore(skills): suggest to run pnpm dedupe (#13401) 2026-04-29 12:19:58 +09:00
NimarandGitHub 3e314974e4 chore(deps): bump prettier to 3.8.3 (#13387) 2026-04-28 09:05:45 +00:00
7db8063c2e fix(dashboards): Resolve filter options consistently (#13335)
Co-authored-by: Nimar <l.nimar.b@gmail.com>
2026-04-28 04:50:11 +00:00
2bd367a7d8 fix(events): use release field for trace release in events table adapter (#13274)
* fix(events): use release field for trace release in events table adapter

eventsToTraceAdapter was using earliest.version for both version and
release fields when synthesizing trace data from the events table
(SDK v5+ direct-write path). This caused langfuse.release span
attribute values to be overwritten with the langfuse.version value.

Also adds 'release' to base/baseWithoutTools/byIdBase field sets in
EventsQueryBuilder so it is actually selected from ClickHouse, and
adds release to EventsObservationSchema so the TypeScript type includes it.

Fixes #13273

* test(events): verify release field is returned from events table queries

Adds two tests to event-repository.servertest.ts:
- getObservationsWithModelDataFromEventsTable returns release separately from version
- getObservationByIdFromEventsTable returns release separately from version

These confirm the fix in event-query-builder.ts (adding 'release' to
base/byIdBase field sets) and EventsObservationSchema (adding the release
field) are wired up end-to-end.

* fix(events): include release field in EventsObservationRecordReadType and converter

Add `release` to `eventsObservationRecordReadSchema` so the field is
typed and preserved through ClickHouse deserialization, and map it in
`convertEventsObservation` so it reaches the domain object.

Previously the field was selected by the query builder but silently
dropped because neither the Zod schema nor the converter passed it
through.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(events): add release field to APIObservation schema

The release field is now returned in GET /api/public/observations
responses (via EventsObservation ...rest spread), but APIObservation
was .strict() and did not declare release, causing makeZodVerifiedAPICall
to fail with "Unrecognized key: release" in all useEventsTable=true tests.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Steffen Schmitz <steffen@langfuse.com>
2026-04-28 04:34:02 +00:00
d9fb3982f1 fix(web): Pre-fill email when switching regions (#13370)
Co-authored-by: Nimar <l.nimar.b@gmail.com>
2026-04-28 01:57:52 +00:00
2f035f856d fix(traces): Limit number of categorical score names and values (#13308)
* fix(traces): Limit number of categorical score names and values

* Fix tests

---------

Co-authored-by: Nimar <l.nimar.b@gmail.com>
2026-04-28 01:25:44 +00:00
0f7ccc80a5 feat(traces): Add none filter mode for tags in the sidebar (#13339)
Co-authored-by: Nimar <l.nimar.b@gmail.com>
2026-04-28 01:25:17 +00:00
Tobias WochingerandGitHub 982f745c2f chore(eslint): enable no-deprecated (#13374)
* test: re-enable vitest parallelism

* test(worker): wait for trace visibility in batch action test

* test(worker): wait for isolated eval queue jobs

* test(web): isolate rate limit redis keys

* chore(eslint): enable no-deprecated experiment

* chore(eslint): handle remaining deprecations

* chore(eslint): avoid stale deprecation suppressions

* chore(eslint): address review feedback

* chore(eslint): remove unrelated test changes

* chore(eslint): keep queue scheduling unchanged

* chore(eslint): remove stale react-icons exceptions
2026-04-27 14:22:35 +00:00
Tobias WochingerandGitHub b7723758f2 test: re-enable Vitest parallelism (#13366)
* test: re-enable vitest parallelism

* test(worker): wait for trace visibility in batch action test

* test(worker): wait for isolated eval queue jobs

* test(web): isolate rate limit redis keys

* test(worker): isolate batch action eval queues

* test(worker): stop flushing redis in ingestion tests

* test(web): isolate observations v2 api project

* test(web): isolate ingestion api project

* test(web): address parallel test review comments
2026-04-27 13:58:14 +00:00
8e275fbdc5 chore(agent-dx): add debug-issue-with-datadog skill (#13375)
Adds a shared agent skill for triaging Linear/GitHub issues and incident
reports against Datadog APM, logs, and metrics, with a repo-debug map and
output template that produces a structured root-cause analysis.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 09:10:50 +00:00
Tobias WochingerandGitHub 205d896a31 docs(agents): improve Agents.md (#13372)
* docs(agents): reference package manifests for stack versions

* docs(agents): slim backend guide entrypoint

* docs(agents): focus backend testing guidance

* docs(agents): replace stale repository examples

* docs(agents): slim clickhouse guide entrypoint

* docs(agents): clarify environment setup scripts

* docs(agents): document cva component variants

* docs(agents): prefer existing constant owners

* docs(agents): add review-derived guidance

* docs(agents): fix clickhouse skill heading flow
2026-04-27 08:25:36 +00:00
Hassieb PakzadandGitHub 2330c39817 fix(internal-tracing): remove output parser spans (#13371)
* fix(internal-tracing): remove output parser spans

* push
2026-04-27 07:19:47 +00:00
marliessophieandGitHub 48511aad9f chore(env): add LANGFUSE_ENABLE_EVENTS_TABLE_UI flag for UI events table support (#13346)
* chore(env): add LANGFUSE_ENABLE_EVENTS_TABLE_UI flag for UI events table support

* refactor: rm env variable from wrong usage

* tests: remove env flag
2026-04-27 05:50:29 +00:00
Valery MeleshkinandGitHub 02121f4181 perf(clickhouse): emit has(metadata_names) conjunct for events filters (#13369)
The ClickHouse index analyzer cannot extract has(names, k) semantics
from
values[indexOf(names, k)] OP v (cross-array arrayElement form), so a
bloom_filter skipping index on metadata_names cannot prune granules for
this shape. Wrap every operator branch in StringObjectFilter.apply() for
events_core / events_full / events_proto with an explicit
has(names, k) AND (...) conjunct.

Also corrects a latent semantic bug: today arr[indexOf(names, missing)]
resolves to the empty string (Array(String) default), making
"does not contain V" match rows where the key was never written, and
"OP empty-value" match similarly. The has(...) conjunct fixes this for
all five operators uniformly.

Add bloom_filter on events_core.metadata_names. events_core is the
table that takes filters in the V2 split-query pattern; events_full
reads by ID after the base CTE narrows things down, so the symmetric
index is intentionally omitted for now.
2026-04-27 05:40:12 +00:00
Hassieb PakzadandGitHub 20b81b3b7f fix(server): ingest flattened experiment metadata (#13368)
* fix: ingest flattened experiment metadata

* refactor: use params object for otel metadata helpers
2026-04-27 05:14:32 +00:00
Nimar e22e79d598 chore: release v3.171.0 2026-04-27 13:33:02 +09:00
NimarandGitHub ce72b55b3c chore(deps): axios 1.15.2 and dedupe (#13365)
* chore(deps): axios 1.15.2 and dedupe

* clean
2026-04-27 03:06:46 +00:00
fbef5ab2f0 perf(clickhouse): pre-filter traces in CTE for analytics integration joins (#13364)
Pre-filters the trace JOIN in a CTE so the trace timestamp window prunes
partitions directly instead of living alongside the LEFT JOIN where the
planner cannot push it down. Applied to the score and generation analytics
queries that drive the PostHog and Mixpanel exports.

Switches `grace_hash` from unconditional to retry-gated: first attempt uses
ClickHouse's `auto` algorithm, retries fall back to `grace_hash` so an OOM
recovers without manual intervention while healthy syncs stay fast.

Note: the generation analytics query previously used `LEFT JOIN ... WHERE
t.project_id = {projectId}` which silently dropped generations whose trace
was missing or outside the 7-day window. With the CTE-based LEFT JOIN those
generations now ship with NULL trace fields instead.

Refs: LFE-9475

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 02:28:37 +00:00
marliessophieandGitHub c9c841508e chore: hide eval preview if user has not ingested with otel in the past 7 days (#13343)
* chore: hide eval preview if user has not ingested with otel in the past 7 days

* chore: push
2026-04-27 01:00:32 +00:00
Steffen SchmitzandGitHub 39510c9d46 chore: add langfuse JP to region picker (#13361)
* chore: add langfuse JP to region picker

* chore: add JP
2026-04-27 00:21:55 +00:00
Hassieb PakzadandGitHub 9cfaa7571a feat(model-prices): add gpt-5.5) (#13354) 2026-04-25 10:34:19 +00:00
marliessophieandGitHub 1694bc4d52 fix: add breadcrumb to dataset items page (#13344)
chore(web): remove dataset breadcrumb utility test
2026-04-24 12:51:50 +00:00
Valery MeleshkinandGitHub 76fdf528f1 perf(clickhouse): scores query in events.all scans all partitions (#13336) 2026-04-24 09:44:33 +00:00
NimarandGitHub f2f0de7207 fix(dev): fix seed command (#13337) 2026-04-24 07:51:22 +00:00
31adc47680 fix(shared): cap analytics observations CTE upper bound (LFE-9475) (#13329)
The observations_agg CTE in getTracesForAnalyticsIntegrations only had a
lower bound on o.start_time, so ClickHouse scanned observations from
minTimestamp - 1h to the end of the table on every run. For long-lived
projects or repeated retries this is an unbounded scan.

Cap the CTE at maxTimestamp + OBSERVATIONS_TO_TRACE_INTERVAL (2 days),
matching the existing observation-to-trace join convention elsewhere in
the file. Traces outside [minTimestamp, maxTimestamp) are already
filtered in the outer SELECT, and in steady state the 30-min now-buffer
ensures a trace's observations have settled well before the window
boundary advances, so the cap does not truncate data that would
otherwise be emitted.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 17:00:48 +00:00
604c0be87f fix(worker): cap PostHog export window at next UTC day boundary (LFE-9475) (#13326)
* fix(worker): cap PostHog export window at next UTC day boundary (LFE-9475)

When a PostHog integration failed repeatedly, lastSyncAt never advanced
and each hourly retry re-scanned an ever-growing window against
ClickHouse. Cap maxTimestamp at the next UTC day boundary after
lastSyncAt so per-run work is bounded and aligned with the toDate(...)
partition/ordering keys. Healthy integrations are unaffected because
now - 30min wins whenever the sync is within a day of present. Initial
backfills (no lastSyncAt) skip the cap to avoid pathological
day-by-day stepping from 2000-01-01.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(worker): use project createdAt as PostHog sync floor on first run

Replace the 2000-01-01 fallback for minTimestamp with the project's
createdAt. No trace data can precede it, so this is a tight lower bound
that lets the day-cap also apply to initial backfills. Old projects
still catch up incrementally (one UTC day per hourly run) instead of
re-scanning all of history in one shot.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore: update comments

* fix(shared): use half-open upper bound for analytics integration queries

Switch the primary event-timestamp filter from `<=` to `<` in the four
getXForAnalyticsIntegrations queries (traces, generations, scores,
events). Since the PostHog and Mixpanel schedulers advance
`lastSyncAt` to the previous run's `maxTimestamp`, a row whose
timestamp falls exactly on a window boundary was previously emitted
once per run on both sides. Half-open semantics ensure each row is
emitted exactly once across consecutive runs. Secondary trace-join
upper bounds (7-day lookback for metadata) stay `<=` because they are
range optimizations, not emission bounds.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 16:36:38 +00:00
443e7d443a feat(scores-api): allow source=ANNOTATION on POST /api/public/scores (#13286)
* feat(scores-api): allow source=ANNOTATION on POST /api/public/scores

Expose the `source` field on the create-score request so callers can
post scores as `ANNOTATION` (or `EVAL`). This unblocks LLM-prefilled
scores that show up in an annotation queue for a human reviewer.

When `source` is `ANNOTATION`, `configId` is required unless
`dataType` is `CORRECTION` (matches the existing tRPC annotation
contract).

Ref: LFE-9330

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(scores-api): narrow source to API/ANNOTATION and enforce rule on ingestion path

Addresses review feedback:

- Drop EVAL from the public create-score surface. EVAL remains a valid
  stored/query value but is reserved for internal evaluator outputs; external
  callers should use API (default) or ANNOTATION. Narrowed via a new
  `CreateScoreSource` enum in Fern and inline zod enum at `PostScoresBody`.
- Move the "ANNOTATION requires configId unless CORRECTION" constraint into
  `validateAndInflateScore` so it applies to both the REST `POST /scores` path
  and the ingestion/SDK path (`POST /ingestion` → score-create event), closing
  the gap flagged on the PR. The zod refine on `PostScoresBody` is kept so REST
  callers still get a synchronous 400 instead of an async drop.
- Fix the stale path in the `PostScoresBody` comment that pointed to a
  non-existent file.
- Add a servertest covering the ingestion path: HTTP returns 207, worker drops
  the ANNOTATION-without-configId event, sentinel score confirms the batch was
  processed.

Ref: LFE-9330

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(scores-api): replace magic strings with ScoreSourceEnum and a named subset schema

Follow-up to review feedback on stringly-typed source checks.

- Add PublicApiCreateScoreSourceArray / Domain / Type in domain/scores.ts
  with `satisfies readonly ScoreSourceType[]` so the public-API subset stays
  provably ⊂ ScoreSourceArray at compile time. Dropping a value from
  ScoreSourceArray would now break this declaration first.
- Use PublicApiCreateScoreSourceDomain on PostScoresBody instead of the
  inline z.enum(["API", "ANNOTATION"]).
- Reference ScoreSourceEnum.ANNOTATION / ScoreSourceEnum.API and
  ScoreDataTypeEnum.CORRECTION instead of raw string literals in the
  PostScoresBody refine and in validateAndInflateScore.

No behavior change; all source-field tests continue to pass.

Ref: LFE-9330

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(scores-api): clarify which entry points trigger the annotation/configId check

The "REST create-score path vs ingestion/SDK path" phrasing was misleading:
both are REST, just different HTTP endpoints (/scores vs /ingestion). Spell
that out and note that both funnel through this function in the worker.

Ref: LFE-9330

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(scores-api): consolidate annotation/configId rule into a shared predicate

- Drop unused PublicApiCreateScoreSourceArray/Type exports; inline the
  subset tuple into the Domain declaration.
- Add `isAnnotationScoreMissingConfigId` + ANNOTATION_SCORE_REQUIRES_CONFIG_ID_MESSAGE
  in domain/scores.ts. Both the zod refine on PostScoresBody and the throw in
  validateAndInflateScore now call the same predicate with the same message,
  removing the copy-pasted rule and its comments.
- Trim redundant prose comments that duplicated the predicate's intent.

Net -18 lines. No behavior change; all 6 source-field tests still pass.

Ref: LFE-9330

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(scores-api): unit-test validateAndInflateScore and expose source on client Fern

Addresses PR review feedback:

- Add unit tests for validateAndInflateScore covering the configId tenancy
  check (the reviewer's specific ask) plus the ANNOTATION/configId rule.
  Direct function calls — no HTTP, no queue, no sentinel waiting; each test
  runs in <400ms.
- Drop the flaky ingestion-endpoint sentinel test that asserted the same
  ANNOTATION drop behavior; coverage is now more precise at the function
  level where the rule actually lives.
- Add `source` + `CreateScoreSource` to the client Fern definition so the
  public client spec matches the server Fern surface. Regenerated the
  corresponding OpenAPI.

Ref: LFE-9330

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 16:01:17 +00:00
steffen911 6c32b797b3 chore: release v3.170.0 2026-04-23 17:06:12 +02:00
76b019cdfe chore(worker): harden cloud free tier usage threshold job (#13322)
Wrap each day of the usage aggregation loop in its own span with
langfuse.free_tier.dayStart/dayEnd/dayDate/daysAgo attributes so daily
work is visible individually in traces. Extend the ClickHouse
request_timeout to 120s on the three per-day count queries, and retry
each query up to two additional times on failure — a single re-run is
cheap compared to a full job retry.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 13:44:54 +00:00
cd3d02b75f fix(web): prevent crash on invalid JSONPath in dataset mapping editor (#13253)
* fix(web): prevent crash on invalid JSONPath in dataset mapping editor

CustomMappingEditor crashed the dialog when a user entered a malformed
JSONPath (e.g. `$..[?(@.x=)]`), because MappingPreviewPanel called
applyFieldMappingConfig in a useMemo and jsonpath-plus threw during
render. applyFieldMappingConfig now wraps evaluateJsonPath in a safe
helper, reports failures via a new onJsonPathError callback, and returns
undefined for that entry/field instead of throwing. applyFullMapping
propagates the callback so json_path_error entries carry the actual
source field and mapping key.

On the frontend, MappingPreviewPanel surfaces the error as a destructive
banner and invalidates validation when a schema is present. Notice boxes
are deduplicated into a small IssueList/IssueItem helper.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(web): surface invalid JSON path in evaluator prompt preview

renderPromptPreviewFromObservation used to discard the error returned by
extractValueFromObject, so an invalid jsonSelector silently rendered the
raw column value. Surface it inline as `<invalid JSON path "X": …>` so
the user sees which mapping is broken instead of getting a misleading
preview (or a generic "Unexpected Error" toast via useExtractVariables).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(web): show real error message when evaluator variable extraction fails

useExtractVariables was forwarding plain Errors (e.g. invalid JSON path
thrown by jsonpath-plus) to trpcErrorToast, whose non-TRPC fallback
renders a generic "Unexpected Error" toast and drops the actual message.
Use showErrorToast directly so the user sees "Invalid JSON path: …"
instead of a useless generic error.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(web): label evaluator extraction error as JSON path issue

"Failed to extract variable" read like a semantic failure. The only
throw path in extractValueFromObject is the jsonpath-plus call, so any
error surfaced here is a JSON path syntax problem — reflect that in the
toast title so users know where to look.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(web): include JSON path in evaluator extraction error toast

Match the phrasing of the dataset mapping banner and preview inline
message so all three surfaces show both the offending path and the
underlying parser message.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(web,shared): standardize on "JSONPath" in user-facing strings

"JSON path" / "JSON Path" / "json path" were used interchangeably in
dataset mapping, evaluator preview, and their shared helpers. Use the
canonical "JSONPath" everywhere these strings surface to the user so the
three error surfaces read consistently.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Revert "fix(web): surface invalid JSON path in evaluator prompt preview"

This reverts commit f943fd87f. Scope creep relative to LFE-9336 — the
inline <invalid JSONPath …> marker inside a rendered prompt is noisy,
and the batch-actions preview is a separate concern that deserves its
own pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(web): distinguish JSONPath syntax errors from misses in final preview

FinalPreviewStep collapsed both json_path_error and json_path_miss into
a single amber "did not match" banner, so a syntax error reaching this
step (possible when the field has no schema or for metadata mappings)
was rendered as a soft warning with misleading wording. Split the
errors by type and render syntax errors with destructive styling and
"invalid syntax" wording, while keeping misses as amber warnings. When
a card has both, prefer the destructive treatment and combine the two
counts into one line. Extract the banner chrome into a small IssueBanner
helper to share variant styling between the top banner and per-card
footer.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(shared): return errors/misses from applyFieldMappingConfig

Replace the onJsonPathMiss/onJsonPathError callback parameters on
applyFieldMappingConfig with a direct FieldMappingResult return shape
of { value, misses, errors }. Callers no longer mutate external arrays
via closures — applyFullMapping consumes result.misses / result.errors
directly and MappingPreviewPanel destructures them out of the return.

Also tightens per-field fault isolation semantics in applyFullMapping
and cleans up a couple of low-value comments.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(web): extract shared IssueBanner from mapping preview panels

MappingPreviewPanel and FinalPreviewStep were each carrying their own
variant lookups, notice-box markup, and icon pickers for the error /
warning JSONPath chrome. Consolidate into a single
AddObservationsToDatasetDialog/components/IssueBanner module that exports:

- IssueBanner, IssueList, IssueItem components
- issueChromeVariants (border + bg + text for banner / list / card footer)
- issueCardVariants (outer card border with an explicit "none" variant)
- issueTextVariants (text color for children that override CSS inheritance)
- issueIcons (variant -> lucide icon)

Both callers now compose cva output with their layout classes via cn().
No visual change; colors and spacing are identical.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(web): address PR review nits on JSONPath extraction + preview copy

useExtractVariables was labelling every error that reached the toast
effect as "Invalid JSONPath in variable mapping", including unrelated
runtime errors that hit the outer Promise.all().catch() branch. Replace
the plain Error state with a discriminated ExtractionError so only
errors surfaced by extractValueFromObject use the JSONPath title;
unexpected failures fall back to a generic "Failed to extract variable".

FinalPreviewStep's per-card footer rendered "1 path have invalid
syntax" for a single error; move the verb into the ternary so the
singular reads "1 path has invalid syntax".

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 12:15:27 +00:00
10d6637ea0 feat(experiments): add enabled toggle for remote dataset run trigger (#13221) (#13289)
* feat(experiments): add enabled toggle for remote dataset run trigger (#13221)

* feat(experiments): add enabled toggle for remote dataset run trigger

Allows users to temporarily disable the remote trigger without losing
the configured URL and default payload.

- Add `remoteExperimentEnabled` boolean column (default true) to the
  `datasets` table
- Add an "Enable trigger" switch to the upsert form
- Hide the "Run" button in the experiment dialog when disabled
- Server-side early return in `triggerRemoteExperiment` when disabled
  (returns `{ success: true, skipped: true }`)

Motivation: once a URL is configured, every Run click fetches the URL
and shows a "Failed to trigger remote experiment" error toast if the
endpoint is unreachable. The only way to silence it today is to delete
the URL, which is lossy. This adds a simple toggle to pause the trigger
while keeping the config.

Backward compatible: default is `true` at both column and form levels,
so existing datasets behave identically.

* fix(public-api): include remoteExperimentEnabled in Dataset v2 response schema

Without this, POST/GET/PUT /api/public/v2/datasets responses fail strict
Zod validation with "Unrecognized key: remoteExperimentEnabled" since the
tRPC/API layer now returns this field for the new toggle.

* fix(experiments): address review feedback on enabled toggle

- deleteRemoteExperiment: reset remoteExperimentEnabled back to true so
  a later upsert without the optional enabled flag does not silently
  inherit the previously disabled state
- RemoteExperimentTriggerModal.onSuccess: distinguish the
  { success: true, skipped: true } response and show a "Remote trigger
  is disabled" toast instead of the misleading success toast
- allDatasets: add remoteExperimentEnabled to the Omit exclusion list
  so the $queryRaw return type matches the actual SQL SELECT (the
  field is not fetched, so the type annotation would otherwise lie)

* fix(public-api): select remoteExperimentEnabled in list dataset handlers

GET /api/public/datasets (v1) and GET /api/public/v2/datasets (v2) both
use an explicit Prisma select that narrows the return type. The APIDataset
response schema now requires remoteExperimentEnabled, so the select needs
to include it or the build fails with "Property 'remoteExperimentEnabled'
is missing".

The single-dataset GETs (v1 /datasets/[name] and v2 /datasets/[datasetName])
use the default all-fields findFirst, so they're already fine.

---------

Co-authored-by: Nimar <l.nimar.b@gmail.com>

* chore: fix migration order

* refactor: remove remoteExperimentEnabled field from API dataset types and endpoints

* refactor: wording

* test: ensure remote experiment fields are not exposed in public API responses

* chore: wording nits

* fix: invalidate remote experiment cache on dataset removal in RemoteExperimentUpsertForm

---------

Co-authored-by: Yuto Toya <97585904+toyayuto@users.noreply.github.com>
Co-authored-by: Nimar <l.nimar.b@gmail.com>
2026-04-23 11:15:18 +00:00
7d9a396bcf chore(observability): upgrade opentelemetry and datadog SDKs (#12737)
* chore(observability): upgrade opentelemetry and datadog SDKs

* fix(ci): disable tracing in tests-web startup

* fix(ci): remove local codex files from pr

* test(worker): retry bedrock llm connection smoke tests

* chore; revert pipeline changes

* revert(test): remove bedrock retry logic from llm connection tests

Reverts the retry/backoff additions from cdaacaf45 — these should be
handled in a separate PR since they are unrelated to the OTel/DD upgrade.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore; disable DD tracing for web tests

* chore: downgrade dd-trace to 5.82.0

* chore: downgrade prisma instrumentation

* chore: bump dd-trace-js

* chore: release v3.170.0-0

* revert release push

---------

Co-authored-by: steffen911 <steffen@langfuse.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Steffen Schmitz <steffenschmitz@hotmail.de>
2026-04-23 08:55:37 +00:00
marliessophieandGitHub fb3dcf8d60 feat(ui): add floating multi-select action bar (#12851)
* fix(ui): simplify floating action bar selection count

* chore: address feedback

* style: opacity and button order

* chore: fix

* chore: format number

* chore: push

* style: action bar

* style: ring style with backdrop

* chore: push

* fix: disabled prop on button

* chore: push

* feat(table): add highlightAllRows prop to ExperimentCompareTable, ExperimentGridView, and ExperimentItemsTable components

* chore: push

* fix: row selection state
2026-04-23 08:03:28 +00:00
52fa068d90 feat: add 5-minute and 20-minute blob storage export frequency options (#13126)
* feat: add 5-minute and 20-minute blob storage export frequency options

Add sub-hourly export frequency options (every 5 minutes, every 20 minutes)
to the blob storage integration, in addition to the existing hourly/daily/weekly.
The scheduler cron is updated from hourly to every 5 minutes to support the
new intervals.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: remove 5-minute export option, reduce lag buffer from 30 to 20 minutes

Per reviewer feedback, 5-minute export frequency is too granular given
internal data paths that may take longer in the worst case. Keeps only
the 20-minute option as the new minimum frequency, adjusts the lag
buffer to 20 minutes, and updates the queue schedule accordingly.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: address PR review feedback for blob storage export frequency changes

- Add removeRepeatable for old hourly cron pattern to prevent duplicate schedules
- Extract lag buffer duration into BLOB_STORAGE_LAG_BUFFER_MS constant
- Add every_20_minutes to Fern BlobStorageExportFrequency enum
- Update lag buffer docs from 30 to 20 minutes
- Fix test comment referencing old 30-min lag buffer

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: update test timing assertion from 30-min to 20-min lag buffer

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: export BLOB_STORAGE_LAG_BUFFER_MS and use it in tests

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: prettier formatting for exportFrequency enum in types.ts

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-22 15:49:38 +00:00
e51b3ebcd0 feat(ui): decode unicode escapes in PrettyJsonView for trace detail (#13223)
* feat(ui): decode unicode escapes in PrettyJsonView for trace detail

Apply decodeUnicodeEscapesOnly() recursively to parsed JSON in the trace
detail PrettyJsonView so that \uXXXX sequences (produced by Python SDK's
json.dumps with ensure_ascii=True) are decoded to their original
characters when viewing traces in the UI.

This is a follow-up to PR #12882 which applied the same decoder to the
batch export pipeline, and to the earlier IOTableCell change (PR #9686).
Together these ensure non-ASCII content (e.g. Japanese, Chinese, Korean)
renders correctly everywhere a user encounters it in Langfuse.

Uses greedy mode to cover both \uXXXX (single-escaped) and \\uXXXX
(double-escaped) ingest paths. Only \uXXXX escapes are decoded; other
escapes (\n, \t, \", etc.) are left untouched.

Refs #10972

* test(ui): add unit tests for decodeUnicodeInJson in PrettyJsonView

Cover primitive passthrough, string decoding (including double-escaped
greedy mode), recursive decoding of arrays / nested objects, surrogate
pairs, and mixed already-decoded input. Same style as unicode.clienttest
from PR #12882.

* refactor(ui): make decodeUnicodeInJson iterative and bounded

Address review feedback from @nimarb on PR #13223: very large or deeply
nested JSON payloads could blow the call stack or freeze the browser tab.

- Replace recursion with an explicit-stack iterative walk, so stack depth
  is no longer bounded by JS engine limits.
- Add two guards:
  - DECODE_UNICODE_MAX_NODES (50,000): stop decoding once the total number
    of visited entries exceeds the budget; remaining values are kept as-is.
  - DECODE_UNICODE_MAX_DEPTH (200): do not descend into subtrees past this
    depth; the subtree is returned undecoded.
- Export the caps so tests can assert behavior at the boundary.

Tests: two new cases in PrettyJsonView.clienttest.ts -- one for chains
~10x deeper than MAX_DEPTH (must not throw), one for arrays larger than
MAX_NODES (first entries decoded, tail preserved verbatim).

* fix(ui): clone parsedJson for JSONView and decode escaped object keys

Address follow-up review feedback on PR #13223.

1. JSONView was receiving `parsedJson` directly, but JSONView internally
   calls `deepParseJson` which mutates nested string fields in place. Since
   baseTableData[].rawChildData holds references back into `parsedJson`,
   sharing the same reference corrupted the table's lazy-loaded children
   (parsed sub-objects replaced the original maxDepth:2 strings). Pass a
   `structuredClone` of `parsedJson` to JSONView via a `useMemo` so the two
   views stay independent without cloning on every render.

2. `decodeUnicodeInJson` was decoding values but leaving object keys as-is.
   Payloads like `{"\\u4f60\\u597d": "value"}` ended up with escaped keys
   alongside decoded values. Apply `decodeUnicodeEscapesOnly` to keys too.

Two new tests cover key decoding (flat + nested); existing tests cover the
value path.

---------

Co-authored-by: Nimar <l.nimar.b@gmail.com>
2026-04-22 13:39:19 +00:00
NimarandGitHub eaeaef0c3a fix(widgets): handle pivot table legacy sorting (#13306) 2026-04-22 13:22:16 +00:00
Valery MeleshkinandGitHub 4a5fe63c34 chore: add ingestion_size_stats table and MVs to dev-tables (#13307)
Track event size distributions across projects via a MergeTree table
fed by two materialized views (one from traces, one from observations).
Every insert including updates gets its own row for full ingestion
visibility.
2026-04-22 13:16:09 +00:00
4e8f0221fc refactor(web): Do not reuse table name for observations and events table (#13204)
* refactor(web): Do not reuse table name for observations and events table

* Resolve PR comments

---------

Co-authored-by: Nimar <l.nimar.b@gmail.com>
2026-04-22 12:14:08 +00:00
bc9ef188b4 fix(evals): fix score filtering on evaluator runs page (#13225)
* fix(evals): remove broken score value filter from evaluator runs page

The score value filter on the evaluator runs page never worked because
it LEFT JOINed the PostgreSQL scores table, but scores live in
ClickHouse. Remove the filter from the UI and strip it in the backend
for backward compatibility with bookmarked URLs.

Closes LFE-9279

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(evals): remove session ID filter and column from evaluator runs page

The session ID filter/column depended on the Postgres `traces` table via
LEFT JOIN, but traces now live in ClickHouse so the join never resolved.
Drop the UI filter facet, table column, and the unused traces JOIN.

Also generalize the bookmarked-URL stripping into a DEPRECATED_FILTER_COLUMNS
constant (currently scoreValue + sessionId).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-22 11:57:10 +00:00
NimarandGitHub e3c741d3b5 feat(widgets): distinguish between available and called tools in filter (#13281)
* feat(widgets): distinguish between available and called tools in filter

* add file

* refactor

* add tests

* fix build

* up

* fix build

* fix scores

* fix observations release
2026-04-22 11:52:12 +00:00
Ben BachemandGitHub 97342f5830 feat(prompts): Add time window filtering to prompt metrics (#13282)
* feat(prompts): Add time window filtering to prompt metrics

* Normalize time window for prompt metrics to start of day / end of day

* Add explanatory tooltip to "last used" and "first used" columns
2026-04-22 11:07:55 +00:00
Ben BachemandGitHub 6508550cc7 refactor(web): Always allow toggling V4 in dev mode (#13284) 2026-04-22 09:45:34 +00:00
Hassieb PakzadandGitHub 2012bd14ee fix(web): migrate unstable eval unit tests to vitest (#13300) 2026-04-22 11:11:54 +02:00
Hassieb PakzadandGitHub ecb0d443ad feat(api): add unstable evals public endpoints (#12829) 2026-04-22 10:51:46 +02:00
585b50ae76 refactor(web): migrate test framework from Jest to Vitest (#13191)
* refactor(web): migrate test framework from Jest to Vitest

Replace Jest with Vitest in the web package for faster test execution
and native ESM/TypeScript support without requiring next/jest SWC transforms.

- Replace jest/jest-environment-jsdom/@types/jest with vitest/@vitejs/plugin-react/vite-tsconfig-paths
- Add vitest.config.mts with 3 projects (client/server/e2e-server) matching the original Jest config
- Convert all jest.* API calls to vi.* equivalents across ~28 test files
- Convert jest.requireActual() to async vi.importActual() with async factory functions
- Remove @jest-environment docblock pragmas (environment set in vitest config)
- Add resolveWorkspaceDeps vite plugin for pnpm strict mode compatibility
- Set testTimeout to 30s to match previous Jest/next-jest default

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(web): fix type error in jumptoplayground test mock

Cast vi.importActual("zod") to any to satisfy both the TypeScript
compiler and the consistent-type-imports lint rule.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: remove jest from shared tsconfig types

The nextjs.json shared tsconfig included "jest" in the types array,
which required @types/jest to be installed. Since we migrated to vitest,
vitest globals are provided via web/vitest-env.d.ts instead.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: update CI pipeline to use vitest instead of jest

Replace jest CLI calls with vitest equivalents in the GitHub Actions
pipeline. Also regenerate pnpm-lock.yaml to remove stale jest entries.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(web): include client project in test:watch

The old Jest test:watch ran all projects. The Vitest migration narrowed
it to only --project server, silently excluding *.clienttest.{ts,tsx}
files from watch mode.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* refactor(web): simplify vitest shared aliases and add web to root workspace

- Auto-generate @langfuse/shared resolve aliases from its package.json
  exports instead of hardcoding each subpath
- Add web to root vitest.workspace.ts alongside worker

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(web): address PR review feedback

- Update AGENTS.md quick commands to use vitest positional patterns
  instead of Jest --testPathPatterns flag
- Move admin-access-webhook.servertest.ts from src/__tests__/async/ to
  src/__tests__/server/async/ so it matches the server project include
  pattern (was silently orphaned under both Jest and Vitest)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(web): fix admin-access-webhook test for 5-minute dedupe window

The dedupe window was increased from 60s to 5 minutes in bbd209194 but
the test was never updated because it was orphaned (not picked up by any
test project). Now that it runs, fix the test to advance the clock past
the actual 5-minute window.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* refactor(web): simplify vitest config using deps.inline

Replace the custom resolve aliases, resolveWorkspaceDeps plugin, and
esbuild.tsconfigRaw workaround with a single deps.inline config — the
same approach used by the worker package. This tells Vitest to process
@langfuse/* packages through its transform pipeline using normal Node
resolution (following pnpm symlinks) instead of Vite's strict exports
resolver.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore(web): remove unnecessary vitest-env.d.ts

Test files are excluded from the build tsconfig, and vitest injects
global types at runtime when globals: true is set. The declaration
file is not needed.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(web): add vitest/globals to tsconfig types and update docs

- Add "vitest/globals" to web/tsconfig.json types so Next.js build
  can resolve describe/it/expect in test files (fixes CI build error)
- Move @testing-library/jest-dom/vitest to client project setupFiles
  instead of per-file imports
- Update CONTRIBUTING.md, AGENTS.md, and skill reference docs to
  replace Jest references with Vitest

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(web): add @testing-library/jest-dom to tsconfig types

Without this, IDE shows type errors on jest-dom matchers like
toBeInTheDocument() in client test files. The setupFiles config
registers the matchers at runtime but doesn't provide the types.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: correct file location paths in testing-guide.md

Update three embedded file location annotations from async/ to server/
to match the actual directory structure.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(web): enable parallel server tests, clean up CONTRIBUTING.md diff, revert turborepo skill

- Remove fileParallelism: false from server/e2e-server projects to
  enable parallel test execution (faster CI)
- Rewrite CONTRIBUTING.md changes preserving original CRLF line endings
  to minimize diff noise
- Revert turborepo dependencies.md change (generic skill, not repo-specific)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(web): restore sequential server tests, clean up docs

- Add maxWorkers: 1 to server/e2e-server projects to match Jest's
  --runInBand (shared DB requires sequential execution)
- Clean CONTRIBUTING.md diff (preserve CRLF line endings)
- Revert turborepo skill change (generic skill, not repo-specific)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: align CONTRIBUTING.md test command description with example

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(web): convert remaining jest.* calls in controller.clienttest.ts

New test code merged from main still used jest.mock/jest.fn/jest.mocked
instead of vi.* equivalents.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Nimar <l.nimar.b@gmail.com>
2026-04-22 08:25:56 +00:00
439e9dbbbd chore(ci): pin action version comments to immutable patch tags (#13291)
Tightens `# v1`/`# v2`/`# v6` floating-major comments on SHA-pinned
actions to their exact patch-level tags (`v1.8.4`, `v2.1.0`, `v2.6.1`,
`v6.1.1`). Same SHAs, no behavior change — just removes ambiguity about
which release the pin corresponds to, and keeps the comment truthful if
the upstream major ever moves to a new SHA (which is exactly what bit
us on actions/setup-node in langfuse-js).

Left alone:
- winterjung/split — only publishes a `v2` floating tag; no patch-level
  tag exists to tighten to.
- orange-buffalo/dependabot-auto-rebase — pins a `v1` branch head (not
  a tag). Switching off a mutable ref is a separate decision.
- .github/workflows/ci.yml.template — not an active workflow.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 17:21:11 +00:00
marliessophieandGitHub 9e59c2c2a7 fix(web): prevent DataTable onRowClick when clicking interactive elements (#12881) 2026-04-21 16:00:52 +00:00
82a4ac4926 feat(api): add DELETE endpoint for LLM connections (#13247)
* feat(api): add DELETE endpoint for LLM connections

Adds `DELETE /api/public/llm-connections/{id}` so API clients (e.g. the
Terraform provider) can fully manage the lifecycle of LLM connections.
Mirrors the tRPC delete behavior by pausing dependent evaluator configs
when the connection is removed.

Refs: langfuse/terraform-provider-langfuse#19,
langfuse/terraform-provider-langfuse#20

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore: remove admin api key auth

* chore: revert

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 15:05:23 +00:00
d205b39edf fix(onboarding): add PH button capture for 'try demo project' (#13251)
* add capturing for demo project button

* delete duplicate entry 'delete_form_open' in posthog client capture

---------

Co-authored-by: Nimar <l.nimar.b@gmail.com>
2026-04-21 12:33:43 +00:00
Ben BachemandGitHub c735ea914a feat(web): Add region selector to user menu (#13270)
* feat(web): Add region selector to user menu

* Only show region switcher in cloud region

* Create `isProductionRegion` function

* Use same regions for auth and user navigation
2026-04-21 12:17:57 +00:00
marliessophieandGitHub 4c2be93bd1 feat(experiments): add support to trigger evals (#13240)
* feat(experiments): add support to trigger evals

* feat: add experiment comparison and batch evaluation actions to tables

* refactor: add experimentRootObservationsOnly config to batch action and event stream

* fix: types & build error

* feat: enhance evaluator selection with dynamic scope labels and improve evaluation dialog messaging

* chore: fix

* feat: add experimentItemExpectedOutput field to event query builder

* feat: add 'Is Experiment Item Root Span' column to events table and update related mappings

* fix: experiment cost preview

* chore: gate experiment dialog

* chore: build
2026-04-21 12:13:23 +00:00
marliessophieandGitHub ac393ee675 chore(experiments): remove outdated peek view code (#13009)
* chore(experiments): remove outdated peek view code

* chore:push
2026-04-21 12:03:20 +00:00
020c344211 feat: detect SDK version from langfuse events table (#13203)
* feat: detect SDK version from langfuse events table

* chore: push

* feat(events): set preferred Clickhouse service for SDK metadata retrieval

* refactor: rename SDK metadata functions for clarity and update documentation

* refactor(events): update metadata selection to use selectMetadataExpanded for full values

* tests: ai sdk test case

* refactor: enhance SDK metadata extraction to include telemetrySdkName for improved identification

---------

Co-authored-by: Nimar <l.nimar.b@gmail.com>
2026-04-21 11:59:05 +00:00
e5074f2ead fix(storage): disable default S3 checksums for GCS multipart uploads (#13280)
AWS SDK v3 >= 3.729 sends a composite CRC32 header on
CompleteMultipartUpload by default, which GCS's S3-compat layer rejects
with a 412 PreconditionFailed. Set requestChecksumCalculation and
responseChecksumValidation to WHEN_REQUIRED so buffered multipart and
lib-storage Upload both work against GCS HMAC endpoints.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 11:42:24 +00:00
Steffen SchmitzandGitHub 8226dd89dc fix(audit-logs): capture after-state and normalise resource ids for member role changes (#13278)
- `updateOrgMembership` now passes the updated record as `after` so org-level
  role changes log both before and after in the audit trail.
- `updateProjectRole` passes `updatedProjectMembership` as `after`, uses
  `action: "create"` when the upsert creates a new row, and standardises the
  `resourceId` to `projectId--userId` across create/update/delete so one
  membership can be tracked end-to-end.

Fixes LFE-9056.
2026-04-21 11:08:05 +00:00
96fed782c6 fix(batch-actions): allow dialog close on status step and fix Go to Dataset 404 (#13277)
fix(batch-actions): allow dialog dismissal on status step and fix Go to Dataset 404

Previously the add-observations-to-dataset dialog blocked ESC / outside-click
on the status step, and the "Go to Dataset" link 404'd for datasets whose ids
contain slashes (e.g. seed datasets named `folder/simple-dataset`). Allow
ambient dismissal except while the batch action is in flight, and navigate
via `router.push` with an encoded dataset id so Next.js doesn't decode `%2F`
back to `/` on render.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 11:06:27 +00:00
77ef07bc7f fix(dashboards): exclude TEXT and CORRECTION scores from scores-numeric view (#13276)
Replace the scores-numeric segment's "does not contain CATEGORICAL"
exclusion with a positive allow-list of data_type IN ('NUMERIC', 'BOOLEAN').
Previously, free-text (TEXT) and correction scores leaked into the
scores-numeric view because only CATEGORICAL was excluded; their null
numeric values also distorted avg/min/max aggregations.

The positive allow-list is safer by default — any future data_type value
added to the enum is excluded unless explicitly opted in.

Refs https://linear.app/langfuse/issue/LFE-9399

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 09:46:56 +00:00
Ben BachemandGitHub 00784f72c5 refactor(web): Enable react/no-unused-prop-types eslint rule (#13209)
* chore(web): Enable `react/no-unused-prop-types` eslint rule

* refactor(web): Fix react/no-unused-prop-types eslint warnings

* Avoid false positives around react-table cells using satisfies

* Update pivot-table.clienttest.tsx to remove `accessibilityLayer` prop
2026-04-21 09:05:57 +00:00
Ben BachemandGitHub fdc3d2f1f8 fix(web): Use overflow-auto instead of overflow-scroll for main content (#13267) 2026-04-21 08:59:22 +00:00
Ben BachemandGitHub 253a4f8cd0 chore(web): Remove @headlessui packages (#13275) 2026-04-21 08:13:04 +00:00
Ben BachemandGitHub c3438490e7 fix(web): Stale search highlights (#13237)
* fix(web): Stale search highlights

* refactor(web): Do not compute search match ranges twice

* Fix PR review issue about active match not being restored

* Resolve PR comment

* Split `useSyncMessageSearchMessages` useEffects
2026-04-21 07:40:38 +00:00
Ben BachemandGitHub f993eb1efc fix(web): Improve skeleton loading state for tables (#13235)
* fix(web): Improve skeleton loading state for tables

* Resolve PR comments

* Add missing loadingCell arguments

* Resolve more PR comments

* Add loading cell for metadata column in scores table

* Resolve PR comments
2026-04-21 07:30:01 +00:00
NimarandGitHub 210b6bfe2a chore(deps): bump dompurify to 3.4.0 (#13272) 2026-04-20 23:50:05 +02:00
e488990476 chore(worker): include fileKey in OTEL ingestion failure logs (#13271)
Emit the S3 fileKey on every OTEL ingestion failure path so operators can
scan their log platform for dropped or errored batches and feed the list
into worker/src/scripts/replayIngestionEventsV2. Covers: masking
fail-closed drops, observation parse failures, per-event record
creation/eval/write failures, and the job-level ForbiddenError/catch
fallthrough. The masking drop log additionally carries orgId and the
propagated callback headers to support zero-trust audit trails.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 16:31:29 +00:00
a7ecf440f7 chore(tests): use gemini-2.5-flash-lite for GoogleAIStudio tests (#13265)
Avoids rate limit conflicts when VertexAI and GoogleAIStudio tests
run concurrently against the same model.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-20 16:22:56 +00:00
Valery MeleshkinandGitHub 9d62604937 chore: bump bullmq to 5.73.5 (#13256) 2026-04-20 18:25:23 +02:00
8834a3af42 fix(datasets): fix errors during json schema generation (#13193)
* fix(datasets): downgrade schema example error to warn to avoid Sentry noise

json-schema-faker can crash on certain user-provided schemas (e.g. arrays
without items). The function already gracefully returns "" and the UI hides the
example section, but console.error was captured by Sentry's capture_console
integration. Downgrade to console.warn so this expected edge case no longer
triggers alerts.

Fixes LANGFUSE-4S5

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat(datasets): upgrade json-schema-faker to 0.6.1 and migrate to async API

- Upgrade json-schema-faker from 0.5.9 to 0.6.1 (ESM-only, zero deps,
  TypeScript rewrite) which fixes the array-without-items crash
- Migrate from deprecated synchronous default-export API to the new named
  async `generateJson` export with per-call options
- Update callers (DatasetSchemaHoverCard, NewDatasetItemForm) to handle
  async generation with proper cancellation cleanup
- Add Jest moduleNameMapper + transformIgnorePatterns for ESM-only package
- Refactor jest.config.mjs to pre-resolve configs and avoid duplicate calls

Fixes LANGFUSE-4S5

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* refactor(datasets): revert jest.config.mjs changes, use virtual mock in test

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore(datasets): remove generateSchemaExample test file

ESM-only json-schema-faker@0.6.1 can't be resolved by Jest's CJS
resolver without jest.config changes. The wrapper is trivial — drop the
test rather than adding workarounds.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* test(datasets): add generateSchemaExample tests with Jest ESM resolution

Add moduleNameMapper for json-schema-faker (ESM-only package) so Jest
can resolve it, and restore the client tests.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(datasets): add type assertion for JsonSchema compatibility

Prisma.JsonValue narrows to JsonObject | JsonArray which isn't
assignable to json-schema-faker's JsonSchema type. Cast explicitly
since we already guard for non-objects.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* refactor(datasets): remove unnecessary cancellation in schema example effects

Schema generation is near-instant — cancellation cleanup adds
complexity for no practical benefit.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(datasets): merge moduleNameMapper with Next.js defaults in jest config

Spread sharedOverrides was overwriting Next.js's built-in
moduleNameMapper (CSS, assets, server-only). Use a helper that merges
our ESM mapper with the resolved config's existing mappings instead.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(datasets): add type annotation to withEsmMapper parameter

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(datasets): add cancellation guard to NewDatasetItemForm schema effect

Rapidly switching datasets could let a stale promise overwrite the
correct placeholder. Add cancelled flag for the multi-dependency effect.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(datasets): prevent cascading re-runs and user input overwrite in schema effect

- Remove inputValue/expectedOutputValue from the effect dependency array
  to prevent form.setValue triggering cascading effect re-runs
- Use form.getValues() at both dispatch and resolution time to avoid
  overwriting content the user typed while generation was in-flight

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(datasets): add cancellation guard to DatasetSchemaHoverCard effect

For consistency with NewDatasetItemForm's async pattern.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-20 15:36:51 +00:00
Ben BachemandGitHub 533d0f0029 refactor(traces): Remove unused updateTags mutation from router (#13269) 2026-04-20 15:36:42 +00:00
cd9ae6d0c3 feat(auth): allow configuration of ID Token signed response alg (#12333)
* feat(auth): allow configuration of ID Token signed response alg

* fix(auth.ts): linter compliance

---------

Co-authored-by: Steffen Schmitz <steffen@langfuse.com>
Co-authored-by: Tobias Wochinger <tobias.wochinger@clickhouse.com>
2026-04-20 14:20:50 +00:00
Ben BachemandGitHub 88313c1d82 refactor(web): Make useSidebarFilterState state location more explicit (#13150)
* refactor(web): Make `useSidebarFilterState` state location more explicit

* Use discriminated union for state location & further cleanup

* Remove incorrect if condition

* Update peek readme

* Remove unneccessary usePeekTableState hooks
2026-04-20 14:20:09 +00:00
Ben BachemandGitHub b5bada7b21 refactor(web): Remove unused components and hooks (#13148) 2026-04-20 13:29:24 +00:00
b3e2e2dfd6 ci: pin useblacksmith/setup-docker-builder comment to v1.6.0 (#13266)
The SHA 5241b2e9 resolves to v1.6.0, not the floating v1 tag. Fixes
zizmor ref-version-mismatch alerts #500 and #501.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 13:28:20 +00:00
5a7420ec2a ci: run zizmor on fork PRs by failing on findings (#13263)
Fork PRs don't receive security-events: write on GITHUB_TOKEN, so the
SARIF upload path is unavailable. Previously the whole job was skipped
on fork PRs, meaning contributor changes to workflows bypassed zizmor.
Add a fork-PR step without advanced-security that fails the job on
findings so contributors see the error directly; keep the SARIF upload
step for trusted events so the code-scanning ruleset still blocks merges.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 15:16:09 +02:00
Hassieb PakzadandGitHub 5abbf2c5a3 fix(model-prices): add match on claude-haiku-4-5 (#13254) 2026-04-20 13:21:42 +02:00
Valery MeleshkinandGitHub 06ee77746a feat: emit .rate and .time metrics with shard tags for DataDog aggregation (#13249)
feat: emit .rate and .time metrics with shard tags for DataDog
aggregation
2026-04-20 09:54:43 +00:00
d8bab6b29f feat(web): warn about unencoded special characters in DATABASE_URL on migration failure (#13186)
* feat(web): warn about unencoded special characters in DATABASE_URL on migration failure

When Prisma migrations fail, the error message now detects whether the
DATABASE_URL credentials contain special characters that need percent-encoding
and prints an actionable hint with an example and documentation link.

Closes #3923

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat(web): add ClickHouse password special character detection on migration failure

Extends the migration failure diagnostics to also check CLICKHOUSE_PASSWORD
for characters (&, =, #, ?, %, +, @) that would break the query-string
interpolation in the ClickHouse migration script.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: feedback

* chore: patch

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-20 07:25:22 +00:00
Steffen SchmitzandGitHub f3ad6ae999 chore: prefix batch export logs (#13246) 2026-04-17 19:15:14 +00:00
011ce23d7f chore(scim): add [SCIM] log prefix and operation confirmations (#13245)
Prefix every SCIM API log with [SCIM] so they can be filtered out of
aggregate logs. Add user-id-based confirmation logs after PUT/PATCH
provisioning and deprovisioning and after DELETE, mirroring the
existing POST assignment log, so operations can be traced without
emitting userName/email. Drop the email from the 409 already-exists
log in POST /Users and log the existing membership userId instead.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 18:53:44 +00:00
b8b5c6012a fix(security): use constant-time comparison for admin API key auth (#13208)
* fix(security): use constant-time comparison for admin API key auth

Replace direct string comparison (!==) with crypto.timingSafeEqual for
ADMIN_API_KEY verification to prevent timing side-channel attacks
(CWE-208). This aligns with the secure pattern already used for project
API key authentication in createAuthedProjectAPIRoute.ts.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(security): handle timingSafeEqual byte-length mismatch in admin auth

Align AdminApiAuthService with the project-scoped admin-key check in
createAuthedProjectAPIRoute.ts: drop the JS-string-length pre-check (which
misreads UTF-8 byte length and can crash on multibyte tokens) and wrap
timingSafeEqual in try/catch. Remove the dead !env.ADMIN_API_KEY guard
already handled by the early-return. Replace the duplicated inline admin
key comparison in createNewSsoConfigHandler with AdminApiAuthService so
both admin paths share one timing-safe implementation, and add
cross-reference comments between the two remaining call sites.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-17 16:59:00 +00:00
53a0e9d074 fix(security): sanitize score config names to prevent CSS injection (#13206)
* fix(security): sanitize score config names to prevent CSS injection

Score config names were interpolated unsanitized into a <style
dangerouslySetInnerHTML> block in ChartStyle, allowing stored CSS
injection via crafted config names (e.g. `x}*{background:red}/*`).

- Sanitize ChartConfig keys at the render site in chart.tsx (defense in
  depth for existing data)
- Add ScoreConfigNameSchema in shared domain with regex validation
  (^[\w\s.()-]+$) for use at input boundaries
- Apply name validation to tRPC create/update and public API POST/PUT
  score-config endpoints

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: score config docs

* chore: udnerscores

* chore: adjust chart.tsx schema

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-17 15:25:23 +00:00
Valery MeleshkinandGitHub 9dacef8c25 fix: make new queue depth metrics usable in cloud watch (#13241) 2026-04-17 15:24:15 +00:00
Valery MeleshkinandGitHub 29026c361f chore: deterministic CloudUsageMeteringQueue init (#13239) 2026-04-17 14:22:08 +00:00
Steffen SchmitzandGitHub 3e1b5992d4 fix: coerce AUTH_SSO_TIMEOUT to number (#13199)
* fix: coerce AUTH_SSO_TIMEOUT to number

* Update AUTH_SSO_TIMEOUT to be positive integer
2026-04-17 13:27:34 +00:00
Nimar 2c4a486edd chore: release v3.169.0 2026-04-17 14:43:16 +02:00
Ben BachemandGitHub ade2c3cff2 fix(prompts): Prevent text overlap when adding prompt reference (#13236) 2026-04-17 12:35:30 +00:00
Valery MeleshkinandGitHub 3089778d7d feat: QueueMetricsRunner collects queue metrics on a fixed schedule and aggegates sharded queue metrics (#13231) 2026-04-17 12:23:51 +00:00
Ben BachemandGitHub 7221ba1018 chore: Update pull request template to clarify chore and refactor types (#13166) 2026-04-17 12:17:33 +00:00
NimarandGitHub a851d49c5e chore(skill): pnpm upgrade skill doesnt change lock file manually (#13234) 2026-04-17 11:59:20 +00:00
NimarandGitHub bcf993a74d chore(deps): bump follow-redirects 1160 (#13233) 2026-04-17 11:51:24 +00:00
NimarandGitHub 7b1ddc6182 chore(deps): bump protobufjs to 7.5.5 (#13232)
* chore(deps): bump protobufjs to 7.5.5

* dedupe
2026-04-17 11:22:54 +00:00
628156a39e fix(ci): use exact release tags in action version comments (#13229)
The zizmor ref-version-mismatch rule flags hash-pinned actions whose
version comment references a moving major tag (e.g. `# v2`) instead of
the exact release tag the hash belongs to. Update the three flagged
actions:

- aws-actions/amazon-ecr-login: `# v2` → `# v2.1.2`
- github/codeql-action/upload-sarif (snyk-worker): `# v4` → `# v4.35.1`
- github/codeql-action/upload-sarif (snyk-web): `# v4` → `# v4.35.1`

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-17 09:49:05 +00:00
marliessophieandGitHub 59d0e620f0 fix: update managed ragas-faithfulness-evaluator with proper scoring and prompt details (#13226)
* fix: update managed ragas-faithfulness-evaluator with proper scoring and prompt details

* chore: push
2026-04-17 09:41:29 +00:00
Ben Bachem 23f1a51b2e chore: release v3.168.0 2026-04-17 10:36:00 +02:00
Ben BachemandGitHub 285f980b56 fix(web): Hide irrelevant filters in subtables (#13136) 2026-04-17 08:26:56 +00:00
df4d782182 fix(evals): return full JSONPath slice result and deduplicate eval JSONPath logic (#13200)
* fix(evals): return full JSONPath slice result and deduplicate eval JSONPath logic

JSONPath slice expressions (e.g. $[1:]) returned only the first matched
element due to an unconditional result[0] in parseJsonDefault. Now
multi-match results return the full array while single-match results
remain unwrapped for backward compatibility.

Also consolidates three separate JSONPath evaluation paths (UI preview,
trace eval, observation eval) into the shared extractValueFromObject,
removing duplicated logic from the worker. The snakeToCamel column ID
fallback and parseUnknownToString remain in the worker since they are
database-specific concerns.

Closes LFE-8416

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(evals): address PR review — scope parseMultiEncodedJson, fix error logging and test expectations

- Only call parseMultiEncodedJson when a jsonSelector is present to avoid
  mutating formatting on the no-selector passthrough path.
- Preserve the raw original value (not the parsed one) in the error
  fallback.
- Add error logging in extractObservationVariables (was already done in
  parseDatabaseRowToString but missed here).
- Update three pre-existing test expectations to match the new unwrap
  semantics: single-match results are unwrapped, non-matching paths
  return empty string instead of "[]".

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(evals): update evalService test expectations for single-match unwrap

Four more test assertions in evalService.test.ts still expected
array-wrapped JSONPath results (e.g. '["Hello world"]'). Updated to
match the new unwrap semantics for single-match queries.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* test(evals): remove duplicate slice/single-element tests from extractObservationVariables

These cases are already covered by extractValueFromObject.test.ts.
The pre-existing tests ($.prompt, $.response, non-matching path) remain
as integration tests for the observation eval path.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* style(evals): use @langfuse/shared alias instead of relative path in test import

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-17 07:49:55 +00:00
Hassieb PakzadandGitHub 571005d2f3 feat(model-prices): add claude-opus-4-7 (#13214)
* feat(model-prices): add claude-opus-4-7

* push

* push
2026-04-16 16:53:44 +00:00
Ben BachemandGitHub 6fc9ea6bad fix: Missing trace tags in v4 table and detail view (#13165)
* fix: Missing trace tags in v4 table and detail view

* Resolve review comments

* Add comment

* Add missing `isRoot` condition
2026-04-16 15:47:13 +00:00
marliessophieandGitHub d4aa05a4d2 chore(trpc): handling of errors with body parse issues (#13211) 2026-04-16 15:09:44 +00:00
Steffen SchmitzandGitHub aecb6ef2be fix: prevent ip validation bypass for image URL validation (#13207)
fix: prevent ip validation bypass for URL validation
2026-04-16 13:47:23 +00:00
824758349d chore(ci): remove GitHub Actions that rely on Node 20 (#13194)
* chore(ci): replace fkirc/skip-duplicate-actions with inline gh script

Remove third-party action dependency and replicate the tree-hash
deduplication logic using gh api. Compares the current commit's git
tree SHA against recent successful workflow runs to skip redundant CI.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore(ci): replace ravsamhq/notify-slack-action with slackapi/slack-github-action

Switch to the official Slack GitHub Action (v3.0.1) for failure
notifications. Uses Block Kit payload for richer messages with
branch/tag, actor, and a direct link to the workflow run.

Also removes the now-unnecessary SLACK_WEBHOOK_URL entry from the
zizmor secrets-outside-env allowlist since the webhook is now passed
via action input rather than env var.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 13:10:35 +00:00
2839f659bb fix(otel): prevent prototype pollution in OTel attribute key parsing (#13201)
Crafted OTel attribute keys like `gen_ai.prompt.__proto__.POLLUTED` could
pollute Object.prototype via the nested-object construction in
convertKeyPathToNestedObject. Guard against dangerous keys (__proto__,
constructor, prototype) and use Object.create(null) for result objects.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 12:45:12 +00:00
marliessophieandGitHub 8dd7b23a3e fix(annotation): render session-level screens subject to fast preview mode on/off (#13178)
* fix(annotation): properly handle session-level-annotation subject to v4

* fix: hide counter

* chore: push

* refactor: simplify

* chore: push

* chore: push
2026-04-16 08:14:58 +00:00
3e61ecc84f feat(tracing): tracing setup page with prompt (#13045)
* draft v1 for skill based onbording

* small ui change

* feat(web): redesign traces onboarding for agent-first setup

* formatting fix

* reuse env component

* add new events to posthog list

* adjsut padding

* feat(web): redesign traces onboarding and clean up setup helpers

* formatting fix

* fix(web): simplify base URL fallback for onboarding env setup

* fix(web): normalize SSR base URL fallback on Vercel

* rename to TracesSetupOnboardingCard

* remove duplicate code for baseurl calling

* catch error on copy button

* add 'copy pormpt' text to button

* fix(web): move copy button above prompt text to avoid overlap

* revert(web): restore HostNameProject and useLangfuseEnvCode from main

Made-with: Cursor

* ui: align layout left

* ui/smaller

* refine video positioning

* refine ui

* ui refinement

* ui refinement

* handle API key access

* edit copy button

* align api key access with existing components

* rmv text

* remove classname

* rmv classname form splashscreen

* rmv cn from splashscreen

* rmv comments

* Update web/src/features/setup/components/TracesSetupOnboardingCard.tsx

Co-authored-by: Nimar <l.nimar.b@gmail.com>

* Update web/src/features/setup/components/TracesSetupOnboardingCard.tsx

Co-authored-by: Nimar <l.nimar.b@gmail.com>

* standardize padding + add spacing from before

---------

Co-authored-by: Nimar <l.nimar.b@gmail.com>
2026-04-15 09:58:25 +00:00
marliessophieandGitHub 692789d0f5 fix(worker): sync managed evaluator vars on template updates (#13164)
* fix(worker): sync managed evaluator vars on template updates

* chore: timestamp

* chore: filter based on project id

* Revert "chore: filter based on project id"

This reverts commit 132ac226ad68c3dec25194433e99f95261974294.

* fix: update log message for managed evaluators upsert completion
2026-04-15 08:35:05 +00:00
864055f593 perf(dual-write): clamp min start time to past day and optimize trace sorting (#13172)
* perf(dual-write): clamp min start time to past day and optimize trace sorting

* chore: exclude project_id 'cmbktgdyf0059ad07yexqm2gp' from dual write

* chore: introducing LANGFUSE_EVENT_PROPAGATION_EXCLUDE_PROJECT_IDS

---------

Co-authored-by: Valery Meleshkin <valeriy@langfuse.com>
2026-04-15 08:31:45 +00:00
Valery MeleshkinandGitHub f741f84e97 chore: add redis.full_command to redis traces (#13169) 2026-04-14 16:42:31 +00:00
marliessophieandGitHub c2ff2c8b7d fix(experiments): keep referenced prompts single-row in compact density (#13167) 2026-04-14 15:09:30 +00:00
NimarandGitHub 02abecaaeb chore(security): fix snyk code scanning (#13158)
* chore(security): fix snyk code scanning

* cleanup

* guard
2026-04-14 13:30:38 +00:00
5d99c5a4a9 chore(v4): default new orgs to v4 (#13105)
* chore(v4): default new orgs to v4

* add rollout file

* simplify

* fix toggle shown on sign up

* persist in db

* exclude demo org

* fix order

* rename

* larger rename

* simpify cloud handling

* no test

* fix

* fix ondismiss

* max transactional

* feat: default new users to observation-level evals (#13151)

* feat: default new users to experiments beta (#13154)

* fix time

---------

Co-authored-by: marliessophie <74332854+marliessophie@users.noreply.github.com>
2026-04-14 12:55:22 +00:00
aa76b348e8 fix(ci): handle invalid security-severity in Snyk SARIF output (#13163)
fix(ci): handle null/undefined security-severity in Snyk SARIF output

Snyk emits invalid security-severity values (null, "undefined", "null")
that cause codeql-action/upload-sarif to reject the file. Replace the
sed-based fix with jq to handle all non-numeric values.

See: https://github.com/github/codeql-action/issues/2187

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 12:11:32 +00:00
56c27d4a63 fix(slack): remove redundant timestamp footer from Slack notifications (#13152)
Slack natively timestamps every message. Our custom footer showed the
worker's server timezone which confused users in different timezones.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 11:51:29 +00:00
7c150e7f77 ci: reapply GH Actions hardening with deploy secret fix (#13161)
* Revert "revert: ci: harden + monitor GH actions with zizmor (#13048) (#13155)"

This reverts commit 4ff398eda0.

* ci: pass deploy secrets explicitly to reusable workflow

Environment secrets don't auto-resolve in reusable workflows.
See: https://github.com/actions/runner/issues/3206

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 11:43:48 +00:00
Tobias WochingerandGitHub 4ff398eda0 revert: ci: harden + monitor GH actions with zizmor (#13048) (#13155)
Revert "ci: harden + monitor GH actions with zizmor (#13048)"

This reverts commit 818fd3b16e.
2026-04-14 10:24:56 +00:00
Ben BachemandGitHub 471e150a87 fix(traces): Use single-line skeletons for table with small row height (#13138) 2026-04-14 09:36:33 +00:00
Ben BachemandGitHub 01df1ede47 fix(web): Preserve whitespace in message search controller (#13096)
* fix(web): Preserve whitespace in message search controller

* Fix tests

* Clear search on blur if whitespace only
2026-04-14 09:35:53 +00:00
Valery MeleshkinandGitHub 0debc7274e fix: rename migration from a cleaned up name to avoid repeated reapplication (#13153)
fix: rename migration from a cleaned up name to avoid repeated
reapplication
2026-04-14 09:26:08 +00:00
e91a046d40 feat(slack): show change author in Slack prompt notification (#13149)
* feat(slack): show change author in Slack prompt notification

Display the user who made the change in the Slack notification message
for prompt version events. Falls back to email when name is unavailable,
and shows "API User" for API key-initiated changes.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(slack): escape mrkdwn in change author and use || for empty string fallback

Escape &, <, > in user name/email to prevent Slack mrkdwn injection
(e.g. <!channel> triggering mass notifications). Use || instead of ??
so empty string names fall back to email correctly.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(slack): escape all user-controlled mrkdwn fields in prompt notification

Apply escapeSlackMrkdwn to prompt.name, prompt.tags, and
prompt.commitMessage to prevent injection via those fields too.
Labels are safe (validated by PROMPT_LABEL_REGEX).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 08:44:04 +00:00
818fd3b16e ci: harden + monitor GH actions with zizmor (#13048)
* Add zizmor workflow and skip forked Claude review PRs

* ci: test if workflow breaks

* ci: add dependabot cooldown

* ci: zizmor auto-fixes

* ci: harden GitHub Actions workflows

* ci: refine zizmor workflow configuration

* ci: scope sdk and snyk secrets to environments

* ci: address zizmor workflow review feedback

* ci: fix license check

* ci: align sdk workflow secret handling

* ci: enable snyk checks on pull requests

* ci: remove temporary snyk pull request trigger

* ci: bump back to the zizmor minimum of 7 days

* style: move to nicer config syntax for secrets-outside-of-env

* ci: fix template-injection warnings in pipeline digest step

Move step outputs and matrix values from ${{ }} interpolation in run
blocks to env variables, preventing potential shell code injection.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(ci): fix broken heredoc expansion in Docker publish steps

The publish-manifest steps used <<'EOF' (single-quoted heredoc) which
suppresses bash variable expansion, and the env vars were never defined
in those steps. This meant every tag-triggered release would fail with
literal ${VAR} strings passed as Docker tags.

- Change <<'EOF' to <<EOF to enable variable expansion
- Add env: blocks defining STEPS_META_*_OUTPUTS_TAGS from step outputs
- Move remaining ${{ matrix.* }} interpolations to env vars

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* ci: rename reserved GITHUB_ env var prefix to INPUT_

Rename GITHUB_EVENT_INPUTS_CONFIRM to INPUT_CONFIRM. GitHub reserves
the GITHUB_ prefix for built-in runner variables.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* ci: use environment secret instead of inherit

* ci: switch to latest action

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 08:23:19 +00:00
Max DeichmannandGitHub d65ee4f6cb chore: add sampling for sharded queues (#13143)
* chore: add sampling for sharded queues

* chore: add sampling for sharded queues

* chore: add sampling for sharded queues

* fix(worker): constrain queue metrics sample rate

* Delete worker/src/__tests__/env.test.ts
2026-04-13 18:54:45 +00:00
Hassieb PakzadandGitHub 7d15ee9ed4 feat(cache): add local l1 cache for model match (#12977) 2026-04-13 20:13:42 +02:00
NimarandGitHub 29faeb31e5 chore(deps): run pnpm dedupe (#13142) 2026-04-13 17:30:55 +00:00
Valery MeleshkinandGitHub e7892863c4 chore: add bloom_filter index on experiment_id to events_core (#13141) 2026-04-13 16:32:27 +00:00
Jannik MaierhöferandGitHub c8faf987a7 feat(ui): remove tier from pylon issue field and change warning message (#13140)
feat(ui): remove tier from pylon issue field and change warnong message
2026-04-13 16:11:32 +00:00
Valery MeleshkinandGitHub de75917221 fix: count histogram UI switching during widget editing (#13128) 2026-04-13 15:04:36 +00:00
marliessophieandGitHub 9440dc24c1 chore(experiments): release public beta on cloud (#13131)
* chore(experiments): release public beta on cloud

* chore: push
2026-04-13 14:50:47 +00:00
Hassieb PakzadandGitHub ce7297a42f fix(email): add project name to evaluator pause notifications (#13135)
* fix(email): add project name to evaluator pause notifications

* push
2026-04-13 16:29:05 +02:00
Hassieb PakzadandGitHub 35e837700e fix(otel): normalize gen ai usage details (#13110) 2026-04-13 16:21:45 +02:00
Valery MeleshkinandGitHub 41c529ddc0 chore: Initialize local databases during cloud setup and maintenance scripts (#13106)
* revert(codex): remove setup_cloud AGENTS entry

* fix(codex): install golang-migrate in cloud services

* fix(codex): verify migrate binary integrity
2026-04-13 14:06:37 +00:00
c091da7c3d fix(evals): make evaluation prompt read-only in view-only template mode (#13047) (#13137)
Fix(evals): make evaluation prompt read-only in view-only template mode

Co-authored-by: Pratima Patel <pratimapatel2008@gmail.com>
2026-04-13 11:59:35 +00:00
Hassieb PakzadandGitHub f72184cc01 fix(llm-schemas): allow CUD access for project members (#13134) 2026-04-13 13:24:57 +02:00
ee7aca767e fix(shared): treat end-of-life model errors as non-retryable (#13129)
* fix(shared): treat end-of-life model errors as non-retryable

Extract non-retryable error patterns into a shared constant and add
"reached the end of its life" to the list so that Bedrock end-of-life
model errors surface immediately instead of being retried.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* refactor(shared): keep isNonRetryableLLMErrorMessage private

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(shared): extract status code from AWS SDK $metadata.httpStatusCode

The AWS SDK puts the HTTP status on `$metadata.httpStatusCode`, not on
`.status` or `.response.status`. Without this, the fallback defaulted to
500, making 4xx errors appear retryable.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* test: use real ResourceNotFoundException from AWS SDK

Instead of hardcoding the error shape, resolve and instantiate the real
`ResourceNotFoundException` from `@aws-sdk/client-bedrock-runtime` via
`@langchain/aws`'s dependency tree.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 09:41:27 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Tobias WochingerClaude Opus 4.6
073cd30cc1 ci(deps): bump the github-actions group across 1 directory with 16 updates (#13114)
* ci(deps): bump the github-actions group across 1 directory with 16 updates

Bumps the github-actions group with 16 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [actions/checkout](https://github.com/actions/checkout) | `2.7.0` | `6.0.2` |
| [aws-actions/configure-aws-credentials](https://github.com/aws-actions/configure-aws-credentials) | `4.3.1` | `6.1.0` |
| [aws-actions/amazon-ecr-login](https://github.com/aws-actions/amazon-ecr-login) | `2.1.1` | `2.1.2` |
| [actions/github-script](https://github.com/actions/github-script) | `7.1.0` | `9.0.0` |
| [github/codeql-action](https://github.com/github/codeql-action) | `3.35.1` | `4.35.1` |
| [codespell-project/actions-codespell](https://github.com/codespell-project/actions-codespell) | `2.1` | `2.2` |
| [actions/setup-node](https://github.com/actions/setup-node) | `4.4.0` | `6.3.0` |
| [pilosus/action-pip-license-checker](https://github.com/pilosus/action-pip-license-checker) | `2.0.0` | `3.1.0` |
| [dorny/paths-filter](https://github.com/dorny/paths-filter) | `3.0.2` | `4.0.1` |
| [pnpm/action-setup](https://github.com/pnpm/action-setup) | `2.4.1` | `5.0.0` |
| [actions/cache](https://github.com/actions/cache) | `4.3.0` | `5.0.4` |
| [docker/login-action](https://github.com/docker/login-action) | `2.2.0` | `4.1.0` |
| [actions/upload-artifact](https://github.com/actions/upload-artifact) | `4.6.2` | `7.0.1` |
| [docker/metadata-action](https://github.com/docker/metadata-action) | `4.6.0` | `6.0.0` |
| [actions/download-artifact](https://github.com/actions/download-artifact) | `4.1.8` | `8.0.1` |
| [actions/stale](https://github.com/actions/stale) | `9.1.0` | `10.2.0` |



Updates `actions/checkout` from 2.7.0 to 6.0.2
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v2.7.0...de0fac2e4500dabe0009e67214ff5f5447ce83dd)

Updates `aws-actions/configure-aws-credentials` from 4.3.1 to 6.1.0
- [Release notes](https://github.com/aws-actions/configure-aws-credentials/releases)
- [Changelog](https://github.com/aws-actions/configure-aws-credentials/blob/main/CHANGELOG.md)
- [Commits](https://github.com/aws-actions/configure-aws-credentials/compare/7474bc4690e29a8392af63c5b98e7449536d5c3a...ec61189d14ec14c8efccab744f656cffd0e33f37)

Updates `aws-actions/amazon-ecr-login` from 2.1.1 to 2.1.2
- [Release notes](https://github.com/aws-actions/amazon-ecr-login/releases)
- [Changelog](https://github.com/aws-actions/amazon-ecr-login/blob/main/CHANGELOG.md)
- [Commits](https://github.com/aws-actions/amazon-ecr-login/compare/183a1442edf41672e66566b7fc560e297a290896...f2e9fc6c2b355c1890b65e6f6f0e2ac3e6e22f78)

Updates `actions/github-script` from 7.1.0 to 9.0.0
- [Release notes](https://github.com/actions/github-script/releases)
- [Commits](https://github.com/actions/github-script/compare/f28e40c7f34bde8b3046d885e986cb6290c5673b...3a2844b7e9c422d3c10d287c895573f7108da1b3)

Updates `github/codeql-action` from 3.35.1 to 4.35.1
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/v3.35.1...c10b8064de6f491fea524254123dbe5e09572f13)

Updates `codespell-project/actions-codespell` from 2.1 to 2.2
- [Release notes](https://github.com/codespell-project/actions-codespell/releases)
- [Commits](https://github.com/codespell-project/actions-codespell/compare/406322ec52dd7b488e48c1c4b82e2a8b3a1bf630...8f01853be192eb0f849a5c7d721450e7a467c579)

Updates `actions/setup-node` from 4.4.0 to 6.3.0
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/49933ea5288caeca8642d1e84afbd3f7d6820020...53b83947a5a98c8d113130e565377fae1a50d02f)

Updates `pilosus/action-pip-license-checker` from 2.0.0 to 3.1.0
- [Release notes](https://github.com/pilosus/action-pip-license-checker/releases)
- [Changelog](https://github.com/pilosus/action-pip-license-checker/blob/main/CHANGELOG.md)
- [Commits](https://github.com/pilosus/action-pip-license-checker/compare/cc7a461bfa27b44ad187b8578c881ef5138c13fd...e909b0226ff49d3235c99c4585bc617f49fff16a)

Updates `dorny/paths-filter` from 3.0.2 to 4.0.1
- [Release notes](https://github.com/dorny/paths-filter/releases)
- [Changelog](https://github.com/dorny/paths-filter/blob/master/CHANGELOG.md)
- [Commits](https://github.com/dorny/paths-filter/compare/de90cc6fb38fc0963ad72b210f1f284cd68cea36...fbd0ab8f3e69293af611ebaee6363fc25e6d187d)

Updates `pnpm/action-setup` from 2.4.1 to 5.0.0
- [Release notes](https://github.com/pnpm/action-setup/releases)
- [Commits](https://github.com/pnpm/action-setup/compare/v2.4.1...fc06bc1257f339d1d5d8b3a19a8cae5388b55320)

Updates `actions/cache` from 4.3.0 to 5.0.4
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](https://github.com/actions/cache/compare/0057852bfaa89a56745cba8c7296529d2fc39830...668228422ae6a00e4ad889ee87cd7109ec5666a7)

Updates `docker/login-action` from 2.2.0 to 4.1.0
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](https://github.com/docker/login-action/compare/v2.2.0...4907a6ddec9925e35a0a9e82d7399ccc52663121)

Updates `actions/upload-artifact` from 4.6.2 to 7.0.1
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/ea165f8d65b6e75b540449e92b4886f43607fa02...043fb46d1a93c77aae656e7c1c64a875d1fc6a0a)

Updates `docker/metadata-action` from 4.6.0 to 6.0.0
- [Release notes](https://github.com/docker/metadata-action/releases)
- [Commits](https://github.com/docker/metadata-action/compare/818d4b7b91585d195f67373fd9cb0332e31a7175...030e881283bb7a6894de51c315a6bfe6a94e05cf)

Updates `actions/download-artifact` from 4.1.8 to 8.0.1
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](https://github.com/actions/download-artifact/compare/fa0a91b85d4f404e444e00e005971372dc801d16...3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c)

Updates `actions/stale` from 9.1.0 to 10.2.0
- [Release notes](https://github.com/actions/stale/releases)
- [Changelog](https://github.com/actions/stale/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/stale/compare/5bef64f19d7facfb25b37b414482c7164d639639...b5d41d4e1d5dceea10e7104786b73624c18a190f)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: 6.0.2
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
- dependency-name: aws-actions/configure-aws-credentials
  dependency-version: 6.1.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
- dependency-name: aws-actions/amazon-ecr-login
  dependency-version: 2.1.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: github-actions
- dependency-name: actions/github-script
  dependency-version: 9.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
- dependency-name: github/codeql-action
  dependency-version: 4.35.1
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
- dependency-name: codespell-project/actions-codespell
  dependency-version: '2.2'
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions
- dependency-name: actions/setup-node
  dependency-version: 6.3.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
- dependency-name: pilosus/action-pip-license-checker
  dependency-version: 3.1.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
- dependency-name: dorny/paths-filter
  dependency-version: 4.0.1
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
- dependency-name: pnpm/action-setup
  dependency-version: 5.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
- dependency-name: actions/cache
  dependency-version: 5.0.4
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
- dependency-name: docker/login-action
  dependency-version: 4.1.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
- dependency-name: actions/upload-artifact
  dependency-version: 7.0.1
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
- dependency-name: docker/metadata-action
  dependency-version: 6.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
- dependency-name: actions/download-artifact
  dependency-version: 8.0.1
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
- dependency-name: actions/stale
  dependency-version: 10.2.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>

* fix(ci): correct version comments on pinned GitHub Action hashes

pnpm/action-setup hash corresponds to v5.0.0 (not v3/v2),
astral-sh/setup-uv hash corresponds to v8.0.0 (not v8).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* style: fix typo

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Tobias Wochinger <tobias.wochinger@clickhouse.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 09:37:48 +00:00
fe76bd280f feat: add OCI Object Storage Native SDK integration with IAM auth options (#12379)
* OCI Object Storage Native SDK client Integration for StorageService with identity access Management options.
Updated tests accordingly.
Updated docker file with env variables and build options to build from code.
Updated package json file with OCI libraries used for Object Storage.
Added a sample .env file with instructions on how to use workload_identity' | 'instance_principal' | 'resource_principal' | 'oci_profile' | 'session_token.
Added !.env.dev-oci.example to gitignore to commit the file.

* Update StorageService.ts

Fixed Lint errors

* Added the pnpm lock file

* Add uploadFileBuffered per upstream PR requirements; rename env vars to langfuse_ prefix

Implemented uploadFileBuffered to satisfy requirements introduced by an upstream pull request.
Updated environment variable names to use the langfuse_ prefix for consistency/alignment

* Remove prisma-extension-kysely dependency

* Remove prisma-extension-kysely from pnpm-lock.yaml

Removed prisma-extension-kysely dependency and related entries.

---------

Co-authored-by: Steffen Schmitz <steffen@langfuse.com>
2026-04-13 11:27:44 +02:00
eb7ee42b8b feat(experiments): direct-write prompt experiment root events (#13044)
* feat(experiments): direct-write prompt experiment root events

* feat(tracing): centralize internal direct event writes

* push

* chore: move asRecord function to utils and update references in experiment service

* chore: move type coercion functions to utils for better organization and reuse

* chore(tracing): refactor internal tracing to use new events writer interface

* fix(experimentService): fix dataset item version conversion

* fix: remove invalid dependency

* fixup(tracing): ensure ordering key parity with experiment backfill

* fix: do not write to events table for self-hosters

* test: fix

* test: fix

* fix: do not write to events-table for self-hosters

* fix: rebase

* chore: type

* fix: skip remapping of IDs for self-hosters

* fix: test

* chore: push

* chore: move away from de-duplication approach

* chore: push

---------

Co-authored-by: Marlies Mayerhofer <74332854+marliessophie@users.noreply.github.com>
2026-04-13 07:47:04 +00:00
Ben BachemandGitHub d3d16272ed fix(web): Create new TableCellWithCopyButton for ApiKeyList (#13057)
* fix(web): Create new `TableCellWithCopyButton` for `ApiKeyList`

* Fix react re-rendering issue after copying text

* Handle rejections when copying to clipboard

* Simplify useCopyToClipboard tests
2026-04-13 07:43:56 +00:00
Ben BachemandGitHub 42f7361090 fix(web): Prevent toast error when toggling v4 with selected saved view (#13077)
* fix(web): Prevent toast error when toggling v4 with selected saved view

* Prevent table being rendered until flag is initialized

* Add mistakenly removed "as const" to StringParam
2026-04-13 07:43:45 +00:00
marliessophieandGitHub dd083cc867 chore(experiments): rewrite metrics aggregation for total cost and latency to skip trace-level aggregation (#13104)
* chore(experiments): rewrite metrics aggregation for total cost and latency to skip trace-level aggregation

* fix(experiments): handle null values in latency and total cost cells in ExperimentsTable

* fix: typo
2026-04-13 07:22:44 +00:00
9c6e749c97 feat(web): add support for AWS Bedrock API Keys (Bearer Tokens) (#13098)
* feat(web): add support for AWS Bedrock API Keys (Bearer Tokens)

Add Bedrock API key authentication as an alternative to AWS access keys
(SigV4) for Amazon Bedrock LLM connections. Users can now choose between
AWS access keys and Bedrock API keys via a tab-based selector in the UI.

- Add BedrockApiKeySchema and BedrockAccessKeysSchema as a discriminated
  union in shared credential schemas
- Add resolveBedrockAuth() to route between bearer token and SigV4 auth
- Add server-side validation of Bedrock credentials on create and update
- Derive and expose a safe authMethod enum (api-key, access-keys,
  default-credentials) in the tRPC list response without leaking secrets
- Add auth method tab selector to the create/update LLM API key form
- Add Bedrock credential validation to the public API PUT endpoint
- Add comprehensive unit, integration, and e2e tests

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* ci: add secret

* fix(web): fix Bedrock DefaultCredentials test for cloud environment

The test assumed updating to BEDROCK_USE_DEFAULT_CREDENTIALS would
succeed, but the test env sets NEXT_PUBLIC_LANGFUSE_CLOUD_REGION="DEV"
which makes the server reject default credentials. Updated the test to
assert the expected rejection on cloud deployments.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(web): add self-hosted happy path test for DefaultCredentials update

The previous fix only asserted cloud rejection. Add back the original
happy-path test that temporarily sets NEXT_PUBLIC_LANGFUSE_CLOUD_REGION
to undefined (simulating self-hosted) so the update-to-DefaultCredentials
path is actually exercised.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(web): add .min(1) to BedrockAccessKeysSchema, guard default creds in public API

- Add .min(1) to accessKeyId and secretAccessKey in BedrockAccessKeysSchema
  to reject empty-string credentials at validation time
- Add cloud guard in PUT /api/public/llm-connections rejecting the
  BEDROCK_USE_DEFAULT_CREDENTIALS sentinel on Langfuse Cloud
- Fix DefaultCredentials tests: use per-test env override with try/finally
  to simulate self-hosted deployments
- Add public API tests for sentinel rejection and invalid credential JSON

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 07:14:44 +00:00
Valery MeleshkinandGitHub ff97a3b413 fix: github oauth should check issuer (#13115) 2026-04-10 21:18:55 +02:00
996 changed files with 51371 additions and 24534 deletions
+47 -25
View File
@@ -11,7 +11,7 @@ evaluating, and debugging AI applications.
- `AGENTS.md` is a living document.
- Keep this file concise and router-like. Push narrow or conditional workflows
into package-local `AGENTS.md` files or shared skills under `skills/`.
into package-local `AGENTS.md` files or shared skills under `.agents/skills/`.
- Update this file in the same PR when monorepo-level architecture, workflows,
dependency boundaries, mandatory verification commands, or release/security
processes materially change.
@@ -23,36 +23,56 @@ evaluating, and debugging AI applications.
## Start Here By Task
- Architecture principles for high-scale observability and wide event data:
`.agents/ARCHITECTURE_PRINCIPLES.md`
- Langfuse org navigation, cross-repo context, or when context/skills from
other Langfuse codebases may be required:
use the `langfuse-codebase-navigator` skill.
- Repo-wide agent setup, `.agents/**`, provider shims, or MCP/bootstrap config:
[`README.md`](README.md),
[`skills/agent-setup-maintenance/SKILL.md`](skills/agent-setup-maintenance/SKILL.md)
`.agents/README.md`,
`.agents/skills/agent-setup-maintenance/SKILL.md`
- Langfuse Cloud cost structure, infra spend, AWS/ClickHouse cost splits, or
Metabase cost marts:
`.agents/skills/analyze-cloud-costs/SKILL.md`
- Production telemetry research, Datadog query recipes, tenant/public API usage
audits, or queue consumer telemetry:
`.agents/skills/datadog-query-recipes/SKILL.md`
- Backend/API work in `web/src/server/**`, `web/src/pages/api/public/**`,
`worker/src/**`, or `packages/shared/src/**`:
[`skills/backend-dev-guidelines/SKILL.md`](skills/backend-dev-guidelines/SKILL.md)
`.agents/skills/backend-dev-guidelines/SKILL.md`
- Model pricing work in `worker/src/constants/default-model-prices.json`,
`packages/shared/src/server/llm/types.ts`, or related pricing files:
[`skills/add-model-price/SKILL.md`](skills/add-model-price/SKILL.md)
`.agents/skills/add-model-price/SKILL.md`
- Code review tasks:
[`skills/code-review/SKILL.md`](skills/code-review/SKILL.md)
`.agents/skills/code-review/SKILL.md`
- Debugging a Linear issue, GitHub issue, or incident report using Datadog
(APM, logs, metrics) to establish a root cause:
`.agents/skills/debug-issue-with-datadog/SKILL.md`
- Measured bug or regression evidence that needs Linear deduplication, evidence
comments, or Triage bug issue creation:
`.agents/skills/linear-bug-triage/SKILL.md`
- Proactive production regression sweeps across Datadog errors, logs, spans, and
API latency with Linear handoff:
`.agents/skills/detect-prod-regressions/SKILL.md`
- Changelog drafting for completed feature branches:
[`skills/changelog-writing/SKILL.md`](skills/changelog-writing/SKILL.md)
`.agents/skills/changelog-writing/SKILL.md`
- ClickHouse schema/query review:
[`skills/clickhouse-best-practices/SKILL.md`](skills/clickhouse-best-practices/SKILL.md)
`.agents/skills/clickhouse-best-practices/SKILL.md`
- Monorepo/Turbo task graph changes:
[`skills/turborepo/SKILL.md`](skills/turborepo/SKILL.md)
`.agents/skills/turborepo/SKILL.md`
- pnpm dependency upgrades, package-version bumps, or `minimumReleaseAgeExclude`
decisions in `pnpm-workspace.yaml`:
[`skills/pnpm-upgrade-package/SKILL.md`](skills/pnpm-upgrade-package/SKILL.md)
`.agents/skills/pnpm-upgrade-package/SKILL.md`
- User-visible frontend changes, Playwright review, or browser signoff:
[`skills/frontend-browser-review/SKILL.md`](skills/frontend-browser-review/SKILL.md)
`.agents/skills/frontend-browser-review/SKILL.md`
- Web UI and frontend entry points:
`../web/AGENTS.md`
`web/AGENTS.md`
- Worker queues and processors:
`../worker/AGENTS.md`
`worker/AGENTS.md`
- Shared contracts, exports, schema, and migrations:
`../packages/shared/AGENTS.md`
`packages/shared/AGENTS.md`
- EE-only work:
`../ee/AGENTS.md`
`ee/AGENTS.md`
Read the minimal set required for the task. More-specific package guides and
shared skills take precedence over this root file for their scoped areas.
@@ -84,7 +104,7 @@ langfuse/
`packages/shared/clickhouse/migrations/{clustered,unclustered}/*.sql`
- Architecture handbook:
[langfuse.com/handbook/product-engineering/architecture](https://langfuse.com/handbook/product-engineering/architecture)
with source markdown in
with source markdown in the sibling docs checkout at
`../langfuse-docs/content/handbook/product-engineering/architecture.mdx`
## Core Commands
@@ -98,8 +118,8 @@ langfuse/
- Build check: `pnpm run build:check`
- Full build: `pnpm run build`
- Full reset/bootstrap (destructive): `pnpm run dx`
- Codex environment bootstrap: `bash scripts/codex/setup.sh`
- Codex environment maintenance: `bash scripts/codex/maintenance.sh`
- Environment/worktree bootstrap: `bash scripts/codex/setup.sh`
- Environment/worktree maintenance: `bash scripts/codex/maintenance.sh`
- Install Playwright Chromium for agent browser review: `pnpm run playwright:install`
Minimum verification matrix:
@@ -124,13 +144,15 @@ Minimum verification matrix:
- `*/dist/*`
- `packages/shared/prisma/generated/*`
- Public API contract changes must update Fern sources in `fern/apis/**` and
regenerated outputs; never hand-edit `generated/**`.
regenerated outputs. Never hand-edit `generated/**`.
- Before adding constants, value lists, or display mappings, search for an
existing owner and reuse or extend that source of truth.
- Keep tests independent and parallel-safe.
- For bug fixes, write the failing test first, confirm it fails, then fix the
bug.
- For user-visible frontend changes in `web/**`, review the affected flow in a
real browser with the Playwright MCP server before signoff. Use
`skills/frontend-browser-review/SKILL.md` and `../web/AGENTS.md` for the
`.agents/skills/frontend-browser-review/SKILL.md` and `web/AGENTS.md` for the
browser-review loop.
- Never commit secrets or credentials. Keep `.env*.example` files in sync with
required env vars.
@@ -140,19 +162,19 @@ Minimum verification matrix:
- `.agents/AGENTS.md` is the canonical root guide.
- Root `AGENTS.md` is a symlink to `.agents/AGENTS.md`.
- Root `CLAUDE.md` is a compatibility symlink to `AGENTS.md`.
- Shared agent/tool config lives in `config.json` and shared skills live in
`skills/`.
- Shared agent/tool config lives in `.agents/config.json` and shared skills
live in `.agents/skills/`.
- Project-scoped provider discovery files are generated local artifacts. Edit
the canonical files under `.agents/` instead of editing generated tool
directories by hand.
- If you change `.agents/config.json`, `skills/**`, or the shim-generation
workflow, run:
- If you change `.agents/config.json`, `.agents/skills/**`, or the
shim-generation workflow, run:
- `pnpm run agents:sync`
- `pnpm run agents:check`
- Do not commit generated provider config or shim outputs under `.claude/`,
`.cursor/`, `.codex/`, `.vscode/`, or `.mcp.json`.
- Durable cross-tool guidance belongs in root/package `AGENTS.md` files or
`skills/**`, not only in tool-specific config directories.
`.agents/skills/**`, not only in tool-specific config directories.
## Commit, PR, and Release Rules
+49
View File
@@ -0,0 +1,49 @@
# Underlying Architecture Principles
Langfuse architecture should optimize for high-scale, exploratory observability
on wide, structured event data. These principles are grounded in current
production scale and the reference material below.
## Reference Posts
- [Simplifying Langfuse for Scale](https://langfuse.com/blog/2026-03-10-simplify-langfuse-for-scale)
- [Charity Majors on Observability 2.0](https://charity.wtf/tag/observability-2-0/)
- [All you need is Wide Events, not "Metrics, Logs and Traces"](https://isburmistrov.substack.com/p/all-you-need-is-wide-events-not-metrics)
## Principles
- Model observations as the primary analytical unit. A trace is a correlation
handle that links related observations, not the only useful entry point.
- Prefer wide, richly attributed events over fragmented metrics, logs, and trace
records that require later reconstruction.
- Preserve high-cardinality context so users can slice, group, filter, and debug
unknown unknowns without predefining every future question.
- Favor immutable or append-oriented event records for high-volume telemetry.
Updates that force read-time deduplication create hidden query costs at scale.
- Denormalize carefully when it removes hot-path joins and makes common filters
into direct column predicates.
- Design storage and query paths around columnar access patterns: narrow field
selection, time-bounded scans, useful ordering keys, and data pruning.
- Keep list, dashboard, and aggregate views on compact query-optimized
representations. Fetch large raw payloads only for focused detail views.
- Make API contracts scale-aware: require time windows where needed, expose field
selection, use token pagination, and avoid defaults that can scan all history.
- Treat cost and operational simplicity as architectural constraints. Extra
databases, queues, materialized views, and migrations must earn their long-term
operational burden.
- Preserve real-time or near-real-time debugging workflows. Batch processing can
help, but it should not make fresh production behavior invisible.
## Practical Defaults For Agents
- Before adding a metric, ask whether the same question is better answered from
wide event data.
- Before adding a join, ask whether the attribute should be propagated or
denormalized onto the observation path.
- Before reading large fields, ask whether the view needs them or can defer them
until a single-record fetch.
- Before adding an update-heavy design, ask whether immutable events plus
derived representations would be simpler at production scale.
- Before documenting public behavior, separate stable public contracts from
private production topology, account details, secret names, and incident
runbooks.
+33 -4
View File
@@ -10,6 +10,8 @@ or `.vscode/`.
## Layout
- `AGENTS.md`: canonical shared root instructions
- `ARCHITECTURE_PRINCIPLES.md`: architecture principles for high-scale
observability
- `config.json`: shared bootstrap and MCP configuration used to generate
tool-specific shims
- `skills/`: shared, tool-neutral implementation guidance for recurring
@@ -38,15 +40,42 @@ Current shape:
"playwright": {
"transport": "stdio",
"command": "npx",
"args": ["-y", "@playwright/mcp@latest"]
"args": [
"-y",
"@playwright/mcp@latest",
"--isolated",
"--save-trace",
"--output-dir",
".playwright-mcp",
"--test-id-attribute",
"data-testid"
]
},
"datadog": {
"langfuse-docs": {
"transport": "http",
"url": "https://mcp.datadoghq.com/api/unstable/mcp-server/mcp"
"url": "https://langfuse.com/api/mcp"
},
"linear": {
"transport": "http",
"url": "https://mcp.linear.app/mcp"
}
},
"claude": {
"settings": {}
"settings": {
"permissions": {
"allow": [
"Bash(find:*)",
"Bash(rg:*)",
"Bash(grep:*)",
"Bash(ls:*)",
"Bash(cat:*)",
"Bash(head:*)",
"Bash(tail:*)"
],
"deny": []
},
"enableAllProjectMcpServers": true
}
},
"codex": {
"environment": {
+67
View File
@@ -33,6 +33,28 @@ Use for:
Open: [agent-setup-maintenance/SKILL.md](agent-setup-maintenance/SKILL.md)
### analyze-cloud-costs
Use for:
- Langfuse Cloud infrastructure cost structure
- AWS versus ClickHouse cost splits and cost drivers
- Metabase infra cost dashboard and cost marts
- daily cost per tracing event and cost regression analysis
Open: [analyze-cloud-costs/SKILL.md](analyze-cloud-costs/SKILL.md)
### datadog-query-recipes
Use for:
- production telemetry research across `prod-us`, `prod-eu`, `prod-hipaa`,
and `prod-jp`
- Datadog query recipes for spans, logs, metrics, tenants, public API usage,
and queue consumers
- ad hoc measured questions where the task is not yet an incident root-cause
analysis
Open: [datadog-query-recipes/SKILL.md](datadog-query-recipes/SKILL.md)
### frontend-browser-review
Use for:
@@ -72,6 +94,24 @@ Use for:
Open: [code-review/SKILL.md](code-review/SKILL.md)
### detect-prod-regressions
Use for:
- proactive Datadog sweeps across `prod-us`, `prod-eu`, `prod-hipaa`, and `prod-jp`
- comparing recent production errors, logs, spans, and API latency to baselines
- handing measured regressions to `linear-bug-triage` for Linear issues or comments
Open: [detect-prod-regressions/SKILL.md](detect-prod-regressions/SKILL.md)
### linear-bug-triage
Use for:
- deduplicating measured bug or regression evidence against Linear
- creating new Linear bug issues in `Triage` with `bug` and related labels
- adding concise evidence comments to related existing Linear issues
Open: [linear-bug-triage/SKILL.md](linear-bug-triage/SKILL.md)
### changelog-writing
Use for:
@@ -81,6 +121,24 @@ Use for:
Open: [changelog-writing/SKILL.md](changelog-writing/SKILL.md)
### clickhouse-best-practices
Use for:
- ClickHouse schema, query, or configuration review
- ClickHouse migrations under `packages/shared/clickhouse/**`
- applying the repo-specific ClickHouse rules layered on top of upstream best practices
Open: [clickhouse-best-practices/SKILL.md](clickhouse-best-practices/SKILL.md)
### debug-issue-with-datadog
Use for:
- root-causing Linear, GitHub, or incident reports with Datadog evidence
- production debugging across APM spans, logs, metrics, and monitors
- mapping observed error clusters back to Langfuse code paths
Open: [debug-issue-with-datadog/SKILL.md](debug-issue-with-datadog/SKILL.md)
### pnpm-upgrade-package
Use for:
@@ -92,6 +150,15 @@ Use for:
Open: [pnpm-upgrade-package/SKILL.md](pnpm-upgrade-package/SKILL.md)
### turborepo
Use for:
- `turbo.json` task graph, caching, filtering, or affected-run changes
- root/package script organization for Turborepo workflows
- monorepo package boundaries and shared-code layout decisions
Open: [turborepo/SKILL.md](turborepo/SKILL.md)
## Adding a New Shared Skill
1. Codex may create or refine shared skills under `.agents/skills/` when a
@@ -0,0 +1,62 @@
---
name: analyze-cloud-costs
description: |
Analyze Langfuse Cloud infrastructure cost structure using Metabase cost
marts. Use when asked about cloud spend, AWS versus ClickHouse cost splits,
cost drivers by provider/service/usage type/account, daily cost per tracing
event, infra cost dashboards, or cost regressions visible in Metabase.
---
# Analyze Cloud Costs
## Overview
Use this skill for evidence-backed Langfuse Cloud cost analysis. The primary
source is the Metabase infra cost dashboard and its production cost marts; the
deliverable should name the time window, query grain, top drivers, and caveats.
## Workflow
1. Clarify the question and choose the grain:
- Headline daily totals: total, AWS, ClickHouse, tracing events, and cost per
100k events.
- Cost structure: provider, service, usage type, operation, account, and day.
- Driver or regression analysis: compare a recent complete-day window against
a prior baseline.
2. Load [`references/cost-marts.md`](references/cost-marts.md) for table IDs,
field IDs, query examples, and caveats.
3. Use the Metabase MCP. If the Metabase tools are not visible, discover them
with tool search before falling back to manual interpretation.
4. Prefer complete UTC days. Avoid treating current-day AWS cost as final
because AWS CUR rows can arrive late.
5. Start broad, then drill down:
- Provider split.
- Service split within the dominant provider.
- Usage type, operation, and account split for the top services.
- Daily trend when explaining change over time.
6. Report only what the queried data supports. If a requested slice is absent,
say that no rows were found for that slice instead of inventing a driver.
## Query Rules
- Use `mcp__metabase__.query` for quick reads. Use
`construct_query` plus `execute_query` when you need to inspect or reuse the
opaque query.
- Pass `filters`, `aggregations`, `group_by`, and `fields` as JSON arrays. Some
tool schemas may display these as strings; if that happens, serialize the same
arrays without changing their shape.
- Keep limits explicit and small enough for analysis. Use pagination only when
the continuation token is needed.
- Include the Metabase dashboard link or query result context in the final
answer when useful.
## Output Expectations
Summarize:
- Time window and whether it uses complete UTC days.
- Total cost and provider split when relevant.
- Top cost drivers by service, usage type, operation, or account.
- Trend or baseline comparison when the user asks "why did this change?"
- Caveats, especially incomplete current-day AWS data and ClickHouse credit
labeling in the unified mart.
@@ -0,0 +1,4 @@
interface:
display_name: "Analyze Cloud Costs"
short_description: "Analyze Langfuse Cloud cost structure"
default_prompt: "Use $analyze-cloud-costs to explain recent Langfuse Cloud cost drivers from Metabase."
@@ -0,0 +1,137 @@
# Langfuse Cloud Cost Marts
Use this reference when querying or explaining Langfuse Cloud cost structure.
Dashboard:
- https://langfuse.metabaseapp.com/dashboard/22-infra-cost?account=&date=past90days&tab=20-tab-1
## Primary Tables
| Purpose | Table | ID |
| --- | --- | --- |
| Unified AWS and ClickHouse cost rows by provider, service, usage type, account, and day | `langfuse_prod.mart_daily_cost_chart` | `739` |
| Daily headline totals plus tracing event counts and cost per 100k events | `langfuse_prod.mart_daily_cost_with_events` | `784` |
| Detailed AWS CUR summary by product, operation, account, and usage type | `langfuse_prod.mart_aws_cost_daily_by_service` | `610` |
| Detailed ClickHouse costs by entity and metric | `langfuse_prod.mart_clickhouse_daily_cost` | `689` |
Prefer table `739` for structural breakdowns. Prefer table `784` for daily
headline totals.
## Field IDs
### `mart_daily_cost_chart` (`739`)
| Field ID | Field |
| --- | --- |
| `t739-0` | `usage_date` |
| `t739-1` | `service_provider` |
| `t739-2` | `service_name` |
| `t739-3` | `operation` |
| `t739-4` | `usage_type` |
| `t739-5` | `account_name` |
| `t739-6` | `cost_usd` |
### `mart_daily_cost_with_events` (`784`)
| Field ID | Field |
| --- | --- |
| `t784-0` | `usage_date` |
| `t784-1` | `total_cost_usd` |
| `t784-2` | `clickhouse_cost_usd` |
| `t784-3` | `aws_cost_usd` |
| `t784-5` | `s3_api_operations_cost_usd` |
| `t784-6` | `total_tracing_events` |
| `t784-7` | `total_cost_per_100k_events` |
## Metabase MCP Patterns
The Metabase MCP supports `query` for direct reads and
`construct_query` plus `execute_query` for reusable opaque queries. In practice,
pass `filters`, `aggregations`, `group_by`, and `fields` as JSON arrays:
```json
{
"table_id": 739,
"filters": [
{
"field_id": "t739-0",
"operation": "greater-than-or-equal",
"value": "2026-05-09"
}
],
"aggregations": [
{
"function": "sum",
"field_id": "t739-6"
}
],
"group_by": [
{ "field_id": "t739-1" },
{ "field_id": "t739-2" }
],
"limit": "200"
}
```
If a tool surface insists on strings for those parameters, serialize the same
arrays as JSON strings.
## Common Breakdowns
Provider split:
- Table `739`
- Aggregate `sum(t739-6)`
- Group by `t739-1`
Service split:
- Table `739`
- Aggregate `sum(t739-6)`
- Group by `t739-1`, `t739-2`
Usage type split:
- Table `739`
- Aggregate `sum(t739-6)`
- Group by `t739-1`, `t739-4`
Environment/account split:
- Table `739`
- Aggregate `sum(t739-6)`
- Group by `t739-1`, `t739-5`
Daily headline totals:
- Table `784`
- Filter `t784-0` by date.
- Read `total_cost_usd`, `clickhouse_cost_usd`, `aws_cost_usd`,
`total_tracing_events`, and `total_cost_per_100k_events`.
Daily trend by provider:
- Table `739`
- Aggregate `sum(t739-6)`
- Group by `t739-0`, `t739-1`
Drilldown sequence for a cost spike:
1. Compare total daily cost in table `784`.
2. Split the same days by provider in table `739`.
3. Split the dominant provider by service.
4. Split the dominant service by usage type, operation, and account.
## Caveats
- Current-day AWS cost can be incomplete because AWS CUR data may not have
landed yet.
- For stable recent analysis, prefer the last complete UTC days rather than
including today.
- ClickHouse cost rows are labeled `cost_usd` in the unified mart, but the
source metric is ClickHouse credits. Mention this when precision or billing
interpretation matters.
- Field IDs can change if Metabase models are rebuilt. If a query fails, search
Metabase for the table name and inspect the returned metadata before
changing the analysis.
+29 -286
View File
@@ -2,7 +2,12 @@
## Purpose
Establish consistency and best practices across Langfuse's backend packages (web, worker, packages/shared) using Next.js 14, tRPC, BullMQ, and TypeScript patterns.
Establish consistency and best practices across Langfuse's backend packages
(`web`, `worker`, `packages/shared`) using Next.js, tRPC, BullMQ, and TypeScript
patterns. Check package manifests such as `web/package.json` for current
framework versions before version-sensitive work.
Keep this file as an entrypoint; open reference files only when the task needs
their details.
## When to Use This Skill
@@ -64,7 +69,7 @@ Use this guide when working on:
### Layered Architecture
```
# Web Package (Next.js 14)
# Web Package (Next.js)
┌─ tRPC API ──────────────────┐ ┌── Public REST API ──────────┐
│ │ │ │
@@ -105,150 +110,6 @@ for complete details.
---
## Directory Structure
### Web Package (`/web/`)
```
web/src/
├── features/ # Feature-organized code
│ ├── [feature-name]/
│ │ ├── server/ # Backend logic
│ │ │ ├── *Router.ts # tRPC router
│ │ │ └── service.ts # Business logic
│ │ ├── components/ # React components
│ │ └── types/ # Feature types
├── server/
│ ├── api/
│ │ ├── routers/ # tRPC routers
│ │ ├── trpc.ts # tRPC setup & middleware
│ │ └── root.ts # Main router
│ ├── auth.ts # NextAuth.js config
│ └── db.ts # Database client
├── pages/
│ ├── api/
│ │ ├── public/ # Public REST APIs
│ │ └── trpc/ # tRPC endpoint
│ └── [routes].tsx # Next.js pages
├── __tests__/ # Jest tests
│ └── async/ # Integration tests
├── instrumentation.ts # OpenTelemetry (FIRST IMPORT)
└── env.mjs # Environment config
```
### Worker Package (`/worker/`)
```
worker/src/
├── queues/ # BullMQ processors
│ ├── evalQueue.ts
│ ├── ingestionQueue.ts
│ └── workerManager.ts
├── features/ # Business logic
│ └── [feature]/
│ └── service.ts
├── instrumentation.ts # OpenTelemetry (FIRST IMPORT)
├── app.ts # Express setup + queue registration
├── env.ts # Environment config
└── index.ts # Server start
```
### Shared Package (`/packages/shared/`)
```
shared/src/
├── server/ # Server utilities
│ ├── auth/ # Authentication helpers
│ ├── clickhouse/ # ClickHouse client & schema
│ ├── instrumentation/ # OpenTelemetry helpers
│ ├── llm/ # LLM integration utilities
│ ├── redis/ # Redis queues & cache
│ ├── repositories/ # Data repositories
│ ├── services/ # Shared services
│ ├── utils/ # Server utilities
│ ├── logger.ts
│ └── queues.ts
├── encryption/ # Encryption utilities
├── features/ # Feature-specific code
├── tableDefinitions/ # Table schemas
├── utils/ # Shared utilities
├── constants.ts
├── db.ts # Prisma client
├── env.ts # Environment config
└── index.ts # Main exports
```
**Import Paths (package.json exports):**
The shared package exposes specific import paths for different use cases:
| Import Path | Maps To | Use For |
| ------------------------------------------ | --------------------------------- | --------------------------------------------------------------- |
| `@langfuse/shared` | `dist/src/index.js` | General types, schemas, utilities, constants |
| `@langfuse/shared/src/db` | `dist/src/db.js` | Prisma client and database types |
| `@langfuse/shared/src/server` | `dist/src/server/index.js` | Server-side utilities (queues, auth, services, instrumentation) |
| `@langfuse/shared/src/server/auth/apiKeys` | `dist/src/server/auth/apiKeys.js` | API key management utilities |
| `@langfuse/shared/encryption` | `dist/src/encryption/index.js` | Encryption and signature utilities |
**Usage Examples:**
```typescript
// General imports - types, schemas, constants, interfaces
import {
CloudConfigSchema,
StringNoHTML,
AnnotationQueueObjectType,
type APIScoreV2,
type ColumnDefinition,
Role,
} from "@langfuse/shared";
// Database - Prisma client and types
import { prisma, Prisma, JobExecutionStatus } from "@langfuse/shared/src/db";
import { type DB as Database } from "@langfuse/shared";
// Server utilities - queues, services, auth, instrumentation
import {
logger,
instrumentAsync,
traceException,
redis,
getTracesTable,
StorageService,
sendMembershipInvitationEmail,
invalidateApiKeysForProject,
recordIncrement,
recordHistogram,
} from "@langfuse/shared/src/server";
// API key management (specific path)
import { createAndAddApiKeysToDb } from "@langfuse/shared/src/server/auth/apiKeys";
// Encryption utilities
import { encrypt, decrypt, sign, verify } from "@langfuse/shared/encryption";
```
**What Goes Where:**
The shared package provides types, utilities, and server code used by both web and worker packages. It has **5 export paths** that control frontend vs backend access:
| Import Path | Usage | What's Included |
| ------------------------------------------ | --------------------- | ---------------------------------------------------------------------------------- |
| `@langfuse/shared` | ✅ Frontend + Backend | Prisma types, Zod schemas, constants, table definitions, domain models, utilities |
| `@langfuse/shared/src/db` | 🔒 Backend only | Prisma client instance |
| `@langfuse/shared/src/server` | 🔒 Backend only | Services, repositories, queues, auth, ClickHouse, LLM integration, instrumentation |
| `@langfuse/shared/src/server/auth/apiKeys` | 🔒 Backend only | API key management (separated to avoid circular deps) |
| `@langfuse/shared/encryption` | 🔒 Backend only | Database field encryption/decryption |
**Naming Conventions:**
- tRPC Routers: `camelCaseRouter.ts` - `datasetRouter.ts`
- Services: `service.ts` in feature directory
- Queue Processors: `camelCaseQueue.ts` - `evalQueue.ts`
- Public APIs: `kebab-case.ts` - `dataset-items.ts`
---
## Core Principles
### 1. tRPC Procedures Delegate to Services
@@ -355,51 +216,10 @@ const result = await instrumentAsync(
### 7. Comprehensive Testing Required
Write tests for all new features and bug fixes. See [testing-guide.md](references/testing-guide.md) for detailed examples.
**Test Types:**
| Type | Framework | Location | Purpose |
| ----------- | --------- | --------------------------------------- | ---------------------------- |
| Integration | Jest | `web/src/__tests__/async/` | Full API endpoint testing |
| tRPC | Jest | `web/src/__tests__/async/` | tRPC procedures with auth |
| Service | Jest | `web/src/__tests__/async/repositories/` | Repository/service functions |
| Worker | Vitest | `worker/src/__tests__/` | Queue processors & streams |
**Quick Examples:**
```typescript
// Integration Test (Public API)
const res = await makeZodVerifiedAPICall(
PostDatasetsV1Response, "POST", "/api/public/datasets",
{ name: "test-dataset" }, auth
);
expect(res.status).toBe(200);
// tRPC Test
const { caller } = await prepare(); // Creates session + caller
const response = await caller.automations.getAutomations({ projectId });
expect(response).toHaveLength(1);
// Service Test
const result = await getObservationsWithModelDataFromEventsTable({
projectId, filter: [...], limit: 1000, offset: 0
});
expect(result.length).toBeGreaterThan(0);
// Worker Test (vitest)
const stream = await getObservationStream({ projectId, filter: [] });
const rows = [];
for await (const chunk of stream) rows.push(chunk);
expect(rows).toHaveLength(2);
```
**Key Principles:**
- Use unique IDs (`randomUUID()`) to avoid test interference
- Clean up test data or use unique project IDs
- Tests must be independent and runnable in any order
- Prefer scoped cleanup or unique project IDs over global reset helpers
Add targeted tests for new backend behavior and bug fixes. Keep tests
independent and parallel-safe. See
[testing-guide.md](references/testing-guide.md) for tRPC, public API, service,
repository, and worker examples.
### 8. Always Filter by projectId for Tenant Isolation
@@ -448,72 +268,26 @@ Trace:
---
## Common Imports
## Live Examples
```typescript
// tRPC (Web)
import { z } from "zod/v4";
import {
createTRPCRouter,
protectedProjectProcedure,
} from "@/src/server/api/trpc";
import { TRPCError } from "@trpc/server";
// Database
import { prisma } from "@langfuse/shared/src/db";
import type { Prisma } from "@prisma/client";
// ClickHouse
import {
queryClickhouse,
queryClickhouseStream,
upsertClickhouse,
} from "@langfuse/shared/src/server";
// Observability - OpenTelemetry + DataDog (NOT Sentry for backend)
import {
logger, // Winston logger with OTEL/DataDog trace context
traceException, // Record exceptions to OpenTelemetry spans
instrumentAsync, // Create instrumented spans for operations
} from "@langfuse/shared/src/server";
// Config
import { env } from "@/src/env.mjs"; // web
// or
import { env } from "./env"; // worker
// Public API (Web)
import { withMiddlewares } from "@/src/features/public-api/server/withMiddlewares";
import { createAuthedProjectAPIRoute } from "@/src/features/public-api/server/createAuthedProjectAPIRoute";
// Queue Processing (Worker)
import { Job } from "bullmq";
import { QueueName, TQueueJobTypes } from "@langfuse/shared/src/server";
```
Reference existing Langfuse features for implementation patterns:
- tRPC router with project auth and Zod input:
`web/src/features/events/server/eventsRouter.ts`
- Public API route with middleware and typed request/response schemas:
`web/src/pages/api/public/datasets/index.ts`
- Worker queue processor with typed jobs, logging, and retry behavior:
`worker/src/queues/evalQueue.ts`
- Tenant filters for Prisma and ClickHouse:
`references/database-patterns.md`
---
## Quick Reference
## Naming Conventions
### HTTP Status Codes
| Code | Use Case |
| ---- | ------------ |
| 200 | Success |
| 201 | Created |
| 400 | Bad Request |
| 401 | Unauthorized |
| 403 | Forbidden |
| 404 | Not Found |
| 500 | Server Error |
### Example Features to Reference
Reference existing Langfuse features for implementation patterns:
- **Datasets** (`web/src/features/datasets/`) - Complete feature with tRPC router, public API, and service
- **Prompts** (`web/src/features/prompts/`) - Feature with versioning and templates
- **Evaluations** (`web/src/features/evals/`) - Complex feature with worker integration
- **Public API** (`web/src/features/public-api/`) - Middleware and route patterns
- tRPC routers: `camelCaseRouter.ts`, e.g. `datasetRouter.ts`
- Services: `service.ts` in the feature server directory
- Queue processors: `camelCaseQueue.ts`, e.g. `evalQueue.ts`
- Public API routes: kebab-case filenames, e.g. `dataset-items.ts`
---
@@ -530,6 +304,9 @@ Reference existing Langfuse features for implementation patterns:
## Navigation Guide
Keep detailed backend guidance in these focused reference files and open only
the one that matches the task.
| Need to... | Read this |
| ------------------------- | ------------------------------------------------------------ |
| Understand architecture | [architecture-overview.md](references/architecture-overview.md) |
@@ -541,37 +318,3 @@ Reference existing Langfuse features for implementation patterns:
| Write tests | [testing-guide.md](references/testing-guide.md) |
---
## Reference Files
### [architecture-overview.md](references/architecture-overview.md)
Three-layer architecture (tRPC/Public API → Services → Data Access), request lifecycle for tRPC/Public API/Worker, Next.js 14 directory structure, dual database system (PostgreSQL + ClickHouse), separation of concerns, repository pattern for complex queries
### [routing-and-controllers.md](references/routing-and-controllers.md)
Next.js file-based routing, tRPC router patterns, Public REST API routes, layered architecture (Entry Points → Services → Repositories → Database), service layer organization, anti-patterns to avoid
### [services-and-repositories.md](references/services-and-repositories.md)
Service layer overview, dependency injection patterns, singleton patterns, repository pattern for data access, service design principles, caching strategies, testing services
### [middleware-guide.md](references/middleware-guide.md)
tRPC middleware (withErrorHandling, withOtelInstrumentation, enforceUserIsAuthed), seven tRPC procedure types (publicProcedure, authenticatedProcedure, protectedProjectProcedure, etc.), Public API middleware (withMiddlewares, createAuthedProjectAPIRoute), authentication patterns (NextAuth for tRPC, Basic Auth for Public API)
### [database-patterns.md](references/database-patterns.md)
Dual database architecture (PostgreSQL via Prisma + ClickHouse via direct client), PostgreSQL CRUD operations, ClickHouse query patterns (queryClickhouse, queryClickhouseStream, upsertClickhouse), repository pattern for complex queries, tenant isolation with projectId filtering, when to use which database
### [configuration.md](references/configuration.md)
Environment variable validation with Zod, package-specific configs (web/env.mjs with t3-oss/env-nextjs, worker/env.ts, shared/env.ts), NEXT_PUBLIC_LANGFUSE_CLOUD_REGION usage, LANGFUSE_EE_LICENSE_KEY for enterprise features, best practices for env management
### [testing-guide.md](references/testing-guide.md)
Integration tests (Public API with makeZodVerifiedAPICall), tRPC tests (createInnerTRPCContext, appRouter.createCaller), service-level tests (repository/service functions), worker tests (vitest with streams), test isolation principles, running tests (Jest for web, vitest for worker)
**Skill Status**: COMPLETE ✅
**Line Count**: ~540 lines
**Progressive Disclosure**: 7 reference files ✅
@@ -1,6 +1,6 @@
---
name: backend-dev-guidelines
description: Shared backend guide for Langfuse's Next.js 14, tRPC, BullMQ, and TypeScript monorepo. Use when creating or reviewing tRPC routers, public REST endpoints, BullMQ queue processors, backend services, middleware, Prisma or ClickHouse data access, OpenTelemetry instrumentation, Zod validation, env configuration, or backend tests across web, worker, or packages/shared.
description: Shared backend guide for Langfuse's Next.js, tRPC, BullMQ, and TypeScript monorepo. Use when creating or reviewing tRPC routers, public REST endpoints, BullMQ queue processors, backend services, middleware, Prisma or ClickHouse data access, OpenTelemetry instrumentation, Zod validation, env configuration, or backend tests across web, worker, or packages/shared.
---
# Backend Development Guidelines
@@ -1,6 +1,6 @@
# Architecture Overview - Langfuse Backend
Complete guide to the layered architecture pattern used in Langfuse's Next.js 14/tRPC/Express monorepo.
Complete guide to the layered architecture pattern used in Langfuse's Next.js/tRPC/Express monorepo. Check package manifests such as `web/package.json` for current framework versions before version-sensitive work.
## Table of Contents
@@ -20,7 +20,7 @@ Langfuse uses a **three-layer architecture** with two primary entry points (tRPC
### The Three Layers
```
# Web Package (Next.js 14)
# Web Package (Next.js)
┌─ tRPC API ──────────────────┐ ┌── Public REST API ──────────┐
│ │ │ │
@@ -163,7 +163,7 @@ Two types of entry points:
```typescript
1. HTTP POST /api/public/datasets
2. Next.js API route handler (pages/api/public/datasets.ts)
2. Next.js API route handler (pages/api/public/datasets/index.ts)
3. withMiddlewares wrapper executes:
- Basic auth verification
@@ -571,7 +571,7 @@ export async function createDataset(data: {
**Public API (Alternative Entry Point):**
```typescript
// web/src/pages/api/public/datasets.ts
// web/src/pages/api/public/datasets/index.ts
import { withMiddlewares } from "@/src/features/public-api/server/withMiddlewares";
import { createAuthedProjectAPIRoute } from "@/src/features/public-api/server/createAuthedProjectAPIRoute";
import { createDataset } from "@/src/features/datasets/server/service";
@@ -412,190 +412,19 @@ Repository: "Here's the Prisma query that does that"
- ❌ Know about HTTP
- ❌ Make decisions (that's service layer)
### Repository Template
### Langfuse Repository Examples
```typescript
// repositories/UserRepository.ts
import { PrismaService } from "@project-lifecycle-portal/database";
import type { User, Prisma } from "@project-lifecycle-portal/database";
Use these current Langfuse files as repository templates:
export class UserRepository {
/**
* Find user by ID with optimized query
*/
async findById(userId: string): Promise<User | null> {
try {
return await PrismaService.main.user.findUnique({
where: { userID: userId },
select: {
userID: true,
email: true,
name: true,
isActive: true,
roles: true,
createdAt: true,
updatedAt: true,
},
});
} catch (error) {
console.error("[UserRepository] Error finding user by ID:", error);
throw new Error(`Failed to find user: ${userId}`);
}
}
- PostgreSQL repository with project-scoped filters:
`packages/shared/src/server/repositories/comments.ts`
- ClickHouse repository with project-scoped filters and query helpers:
`packages/shared/src/server/repositories/traces.ts`
- Repository tests:
`web/src/__tests__/server/repositories/event-repository.servertest.ts`
/**
* Find all active users
*/
async findActive(options?: {
orderBy?: Prisma.UserOrderByWithRelationInput;
}): Promise<User[]> {
try {
return await PrismaService.main.user.findMany({
where: { isActive: true },
orderBy: options?.orderBy || { name: "asc" },
select: {
userID: true,
email: true,
name: true,
roles: true,
},
});
} catch (error) {
console.error("[UserRepository] Error finding active users:", error);
throw new Error("Failed to find active users");
}
}
/**
* Find user by email
*/
async findByEmail(email: string): Promise<User | null> {
try {
return await PrismaService.main.user.findUnique({
where: { email },
});
} catch (error) {
console.error("[UserRepository] Error finding user by email:", error);
throw new Error(`Failed to find user with email: ${email}`);
}
}
/**
* Create new user
*/
async create(data: Prisma.UserCreateInput): Promise<User> {
try {
return await PrismaService.main.user.create({ data });
} catch (error) {
console.error("[UserRepository] Error creating user:", error);
throw new Error("Failed to create user");
}
}
/**
* Update user
*/
async update(userId: string, data: Prisma.UserUpdateInput): Promise<User> {
try {
return await PrismaService.main.user.update({
where: { userID: userId },
data,
});
} catch (error) {
console.error("[UserRepository] Error updating user:", error);
throw new Error(`Failed to update user: ${userId}`);
}
}
/**
* Delete user (soft delete by setting isActive = false)
*/
async delete(userId: string): Promise<User> {
try {
return await PrismaService.main.user.update({
where: { userID: userId },
data: { isActive: false },
});
} catch (error) {
console.error("[UserRepository] Error deleting user:", error);
throw new Error(`Failed to delete user: ${userId}`);
}
}
/**
* Check if email exists
*/
async emailExists(email: string): Promise<boolean> {
try {
const count = await PrismaService.main.user.count({
where: { email },
});
return count > 0;
} catch (error) {
console.error("[UserRepository] Error checking email exists:", error);
throw new Error("Failed to check if email exists");
}
}
}
// Export singleton instance
export const userRepository = new UserRepository();
```
**Using Repository in Service:**
```typescript
// services/userService.ts
import { userRepository } from "../repositories/UserRepository";
import { ConflictError, NotFoundError } from "../utils/errors";
export class UserService {
/**
* Create new user with business rules
*/
async createUser(data: {
email: string;
name: string;
roles: string[];
}): Promise<User> {
// Business rule: Check if email already exists
const emailExists = await userRepository.emailExists(data.email);
if (emailExists) {
throw new ConflictError("Email already exists");
}
// Business rule: Validate roles
const validRoles = ["admin", "operations", "user"];
const invalidRoles = data.roles.filter(
(role) => !validRoles.includes(role),
);
if (invalidRoles.length > 0) {
throw new ValidationError(`Invalid roles: ${invalidRoles.join(", ")}`);
}
// Create user via repository
return await userRepository.create({
email: data.email,
name: data.name,
roles: data.roles,
isActive: true,
});
}
/**
* Get user by ID
*/
async getUser(userId: string): Promise<User> {
const user = await userRepository.findById(userId);
if (!user) {
throw new NotFoundError(`User not found: ${userId}`);
}
return user;
}
}
```
Keep data-access concerns in repositories and business decisions in services.
Project-scoped queries must include `projectId` or `project_id` filters.
---
@@ -803,69 +632,13 @@ class UserService {
## Testing Services
### Unit Tests
Use `testing-guide.md` for backend test patterns. Prefer current Langfuse tests
over invented examples:
```typescript
// tests/userService.test.ts
import { UserService } from "../services/userService";
import { userRepository } from "../repositories/UserRepository";
import { ConflictError } from "../utils/errors";
// Mock repository
jest.mock("../repositories/UserRepository");
describe("UserService", () => {
let userService: UserService;
beforeEach(() => {
userService = new UserService();
jest.clearAllMocks();
});
describe("createUser", () => {
it("should create user when email does not exist", async () => {
// Arrange
const userData = {
email: "test@example.com",
name: "Test User",
roles: ["user"],
};
(userRepository.emailExists as jest.Mock).mockResolvedValue(false);
(userRepository.create as jest.Mock).mockResolvedValue({
userID: "123",
...userData,
});
// Act
const user = await userService.createUser(userData);
// Assert
expect(user).toBeDefined();
expect(user.email).toBe(userData.email);
expect(userRepository.emailExists).toHaveBeenCalledWith(userData.email);
expect(userRepository.create).toHaveBeenCalled();
});
it("should throw ConflictError when email exists", async () => {
// Arrange
const userData = {
email: "existing@example.com",
name: "Test User",
roles: ["user"],
};
(userRepository.emailExists as jest.Mock).mockResolvedValue(true);
// Act & Assert
await expect(userService.createUser(userData)).rejects.toThrow(
ConflictError,
);
expect(userRepository.create).not.toHaveBeenCalled();
});
});
});
```
- Repository tests:
`web/src/__tests__/server/repositories/event-repository.servertest.ts`
- Pure service unit tests:
`web/src/__tests__/server/unit/`
---
@@ -4,25 +4,65 @@ Complete guide to testing Langfuse backend services across web, worker, and shar
## Table of Contents
- [Key Testing Principles](#key-testing-principles)
- [Test Types Overview](#test-types-overview)
- [Integration Tests (Public API)](#integration-tests-public-api)
- [Service-Level Tests (Repository/Service)](#service-level-tests-repositoryservice)
- [tRPC Tests (Procedure Testing)](#trpc-tests-procedure-testing)
- [Worker Tests (Queue Processing)](#worker-tests-queue-processing)
- [Key Testing Principles](#key-testing-principles)
- [Running Tests](#running-tests)
---
## Key Testing Principles
### General Principles
1. **Test Isolation**: Each test should be independent and runnable in any order
2. **Unique IDs**: Use `randomUUID()` or unique project IDs to avoid test interference
3. **Cleanup**: Always clean up test data in service tests (or use unique project IDs)
4. **Avoid Global Resets**: Prefer scoped cleanup or unique project IDs over global reset helpers
5. **Flags and Fallbacks**: When code branches on env flags, feature flags, or fallback data paths, test both branches and ensure fixtures are written to the same store the branch reads from
### By Test Type
| Test Type | Key Principles |
|-----------|----------------|
| **Integration** | Test HTTP endpoints, validate status codes and response shapes |
| **tRPC** | Use `createInnerTRPCContext` and `appRouter.createCaller`, test auth/permissions |
| **Service** | Test individual functions with isolated data, always cleanup |
| **Worker** | Use vitest, test streams with async iteration, test filtering logic |
### Test Data Management
```typescript
// ✅ GOOD: Use unique IDs
const projectId = randomUUID();
const traceId = randomUUID();
// ✅ GOOD: Cleanup in service tests
afterAll(async () => {
await prisma.model.delete({ where: { id: modelId } });
});
// ✅ GOOD: Use unique projects (no cleanup needed)
const { projectId } = await createOrgProjectAndApiKey();
// ❌ BAD: Shared test data between tests
const projectId = "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a";
```
---
## Test Types Overview
Langfuse uses multiple testing strategies for different layers:
| Test Type | Framework | Location | Purpose |
|-----------|-----------|----------|---------|
| Integration | Jest | `web/src/__tests__/async/` | Full API endpoint testing |
| tRPC | Jest | `web/src/__tests__/async/` | tRPC procedure testing with auth |
| Service | Jest | `web/src/__tests__/async/repositories/` | Repository/service function testing |
| Integration | Vitest | `web/src/__tests__/server/` | Full API endpoint testing |
| tRPC | Vitest | `web/src/__tests__/server/` | tRPC procedure testing with auth |
| Service | Vitest | `web/src/__tests__/server/repositories/` | Repository/service function testing |
| Worker | Vitest | `worker/src/__tests__/` | Queue processors and streams |
---
@@ -31,7 +71,7 @@ Langfuse uses multiple testing strategies for different layers:
Test full REST API endpoints end-to-end using HTTP requests.
**File location:** `web/src/__tests__/async/datasets-api.servertest.ts`
**File location:** `web/src/__tests__/server/datasets-api.servertest.ts`
```typescript
import { makeZodVerifiedAPICall } from "../helpers";
@@ -73,7 +113,7 @@ describe("Dataset API", () => {
Test individual repository/service functions with isolated data.
**File location:** `web/src/__tests__/async/repositories/event-repository.servertest.ts`
**File location:** `web/src/__tests__/server/repositories/event-repository.servertest.ts`
```typescript
import {
@@ -186,7 +226,7 @@ describe("Event Repository Tests", () => {
Test tRPC procedures with caller pattern and auth context.
**File location:** `web/src/__tests__/async/automations-trpc.servertest.ts`
**File location:** `web/src/__tests__/server/automations-trpc.servertest.ts`
```typescript
import { appRouter } from "@/src/server/api/root";
@@ -464,88 +504,15 @@ describe("batch export test suite", () => {
---
## Key Testing Principles
### General Principles
1. **Test Isolation**: Each test should be independent and runnable in any order
2. **Unique IDs**: Use `randomUUID()` or unique project IDs to avoid test interference
3. **Cleanup**: Always clean up test data in service tests (or use unique project IDs)
4. **Avoid Global Resets**: Prefer scoped cleanup or unique project IDs over global reset helpers
### By Test Type
| Test Type | Key Principles |
|-----------|----------------|
| **Integration** | Test HTTP endpoints, validate status codes and response shapes |
| **tRPC** | Use `createInnerTRPCContext` and `appRouter.createCaller`, test auth/permissions |
| **Service** | Test individual functions with isolated data, always cleanup |
| **Worker** | Use vitest, test streams with async iteration, test filtering logic |
### Test Data Management
```typescript
// ✅ GOOD: Use unique IDs
const projectId = randomUUID();
const traceId = randomUUID();
// ✅ GOOD: Cleanup in service tests
afterAll(async () => {
await prisma.model.delete({ where: { id: modelId } });
});
// ✅ GOOD: Use unique projects (no cleanup needed)
const { projectId } = await createOrgProjectAndApiKey();
// ❌ BAD: Shared test data between tests
const projectId = "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a";
```
---
## Running Tests
### Web Tests (Jest)
Use the nearest package `AGENTS.md` as the source of truth for current test
commands.
```bash
# Run all tests
pnpm test
# Run sync tests
pnpm test-sync
# Run async tests
pnpm test -- --testPathPattern="async"
# Run specific test file
pnpm test -- --testPathPattern="datasets-api"
# Run specific test
pnpm test -- --testPathPattern="datasets-api" --testNamePattern="should create dataset"
```
### Worker Tests (Vitest)
```bash
# Run all worker tests
pnpm run test --filter=worker
# Run specific test file
pnpm run test --filter=worker -- batchExport
# Run specific test
pnpm run test --filter=worker -- batchExport -t "should export observations"
```
### Coverage
```bash
# Web coverage
pnpm test -- --coverage
# Worker coverage
pnpm run test --filter=worker -- --coverage
```
Common targeted forms:
- Web server tests: `pnpm --filter web run test -- <pattern>`
- Web client tests: `pnpm --filter web run test-client -- <pattern>`
- Worker tests: `pnpm --filter worker run test <file-or-pattern>`
---
File diff suppressed because it is too large Load Diff
@@ -25,9 +25,20 @@ Comprehensive guidance for ClickHouse covering schema design, query optimization
**Why rules take priority:** ClickHouse has specific behaviors (columnar storage, sparse indexes, merge tree mechanics) where general database intuition can be misleading. The rules encode validated, ClickHouse-specific guidance.
### For Formal Reviews
## Langfuse-Specific Rules
When performing a formal review of schemas, queries, or data ingestion:
- Use `packages/shared/src/server/queries/clickhouse-sql/event-query-builder.ts`
for queries against the `events` table. Do not hand-roll `events` SQL unless
you first confirm the query builder cannot express the query.
- Never use `FINAL` on the `events` table; it is designed so `FINAL` is not
required and the keyword hurts performance.
- Any migration in `packages/shared/clickhouse/migrations/clustered/**` with
more than one `ALTER` on the same table must end every metadata `ALTER`
(`ADD/DROP/MODIFY COLUMN`, `ADD/DROP INDEX`) with `SETTINGS alter_sync = 2`,
and every mutation-creating `ALTER` (`MATERIALIZE …`, `UPDATE`, `DELETE`)
with `SETTINGS mutations_sync = 2`. The matching `unclustered/` file runs
against plain `MergeTree` and does not need (and should not duplicate)
these settings.
---
@@ -53,6 +64,7 @@ When performing a formal review of schemas, queries, or data ingestion:
- [ ] LowCardinality applied to appropriate string columns
- [ ] Partition key cardinality bounded (100-1,000 values)
- [ ] ReplacingMergeTree has version column if used
- [ ] Clustered migration files with multiple ALTERs on the same table use `SETTINGS alter_sync = 2` (metadata) and `SETTINGS mutations_sync = 2` (`MATERIALIZE …`, `UPDATE`, `DELETE`); unclustered mirror has none
### For Query Reviews (SELECT, JOIN, aggregations)
@@ -227,8 +239,8 @@ Each rule file in `rules/` contains:
---
## Full Compiled Document
## Compatibility Entrypoint
For the complete guide with all rules expanded inline: `AGENTS.md`
Use `AGENTS.md` when you need to check multiple rules quickly without reading individual files.
`AGENTS.md` is a short compatibility index for agents that open that file
directly. The authoritative workflow lives in this `SKILL.md`, and detailed rule
bodies live in `rules/`.
@@ -0,0 +1,73 @@
---
name: datadog-query-recipes
description: |
Langfuse-specific Datadog query recipes for production telemetry research.
Use when asked to investigate tenant or project activity, public API endpoint
usage, queue consumer behavior, spans, logs, metrics, or ad hoc production
questions across prod-us, prod-eu, prod-hipaa, and prod-jp. This skill is for
reusable query shapes and measured research; pair it with
debug-issue-with-datadog when the task is an incident or root-cause analysis.
---
# Datadog Query Recipes
Use this skill for Langfuse production telemetry research where the main work is
finding the right Datadog data path. Keep findings evidence-based and include
the exact Datadog links or query shapes that support the answer.
## Required Scope
Unless the user explicitly narrows the scope, cover every production
environment:
- `prod-us`
- `prod-eu`
- `prod-hipaa`
- `prod-jp`
Query both Datadog sites when needed. Default to the EU site for `prod-eu` and
the US site for the other prod environments, but verify with a small count or
facet query before concluding an environment has no data.
Before querying live Datadog, load the relevant Datadog MCP guidance for the
data domain you need: traces, logs, metrics, and visualizations.
## Workflow
1. Identify the entity and signal: tenant ID, org ID, project ID, route, queue,
service, error class, or metric.
2. Read only the relevant reference:
- Prod environment/site routing:
[`references/environments.md`](references/environments.md)
- Public API tenant or legacy endpoint usage:
[`references/public-api-tenant-usage.md`](references/public-api-tenant-usage.md)
- Queue inventory, queue consumers, and queue metrics:
[`references/queue-consumers.md`](references/queue-consumers.md)
3. Start with aggregate queries, grouped by environment, service, route,
queue, project, org, status, or error facets as appropriate.
4. Fetch raw spans, logs, or traces only after aggregation identifies the
cluster or sample you need.
5. For tenant-specific HTTP usage, prefer trace correlation over single-span
queries when tenant tags and route tags live on different spans.
6. Report the windows, environments, sites, query links, and any sampling or
missing-data caveats.
## When To Use Other Skills
- Use [`debug-issue-with-datadog`](../debug-issue-with-datadog/SKILL.md) when a
Linear issue, GitHub issue, incident report, or monitor needs root-cause
analysis and patch recommendations.
- Use [`detect-prod-regressions`](../detect-prod-regressions/SKILL.md) when the
user asks for a proactive production sweep or baseline comparison.
- Use [`linear-bug-triage`](../linear-bug-triage/SKILL.md) only after a human
approves sharing measured findings in Linear.
## Output Expectations
Summarize what was checked, including:
- Datadog site and `env` values covered.
- Time windows.
- Core filters or metrics used.
- Count, rate, latency, queue depth, trace sample, or "No measurements found".
- Datadog links or trace IDs that let the human rerun the query.
@@ -0,0 +1,4 @@
interface:
display_name: "Datadog Query Recipes"
short_description: "Langfuse production telemetry queries"
default_prompt: "Use $datadog-query-recipes to investigate Langfuse production telemetry for a tenant, endpoint, queue, or regression."
@@ -0,0 +1,45 @@
# Production Environments
Langfuse production deploys cover these environments and services. The deploy
matrix is defined in `.github/workflows/deploy.yml`.
| Environment | Primary Datadog site | Common services |
| --- | --- | --- |
| `prod-us` | US, `datadoghq.com` | `web`, `web-ingestion`, `web-iso`, `worker`, `worker-cpu` |
| `prod-eu` | EU, `datadoghq.eu` | `web`, `web-ingestion`, `web-iso`, `worker`, `worker-cpu` |
| `prod-hipaa` | US, `datadoghq.com` | `web`, `web-ingestion`, `web-iso`, `worker`, `worker-cpu` |
| `prod-jp` | US, `datadoghq.com` | `web`, `web-ingestion`, `web-iso`, `worker`, `worker-cpu` |
The site mapping is a starting point, not proof. For cross-region research, run
a small count or facet query on both Datadog sites before saying an environment
has no data.
## Starter Filters
Use these as first-pass filters, then add the subsystem-specific route, queue,
tenant, or error facets.
```text
env:prod-us
env:prod-eu
env:prod-hipaa
env:prod-jp
```
```text
env:<env> (service:web OR service:web-ingestion OR service:web-iso)
env:<env> (service:worker OR service:worker-cpu)
```
For HTTP routes, start with `service:web` or `service:web-ingestion` depending
on the endpoint. For queue consumers, start with `service:worker` and
`service:worker-cpu`.
## Cross-Site Rule
When a query returns zero:
1. Check the time window and spelling of `env`, `service`, and route/resource.
2. Query facets on the same site for `env` and `service`.
3. Repeat a small count query on the other Datadog site.
4. Only then report "No measurements found".
@@ -0,0 +1,99 @@
# Public API Tenant Usage
Use this recipe when checking whether an org or project calls a public API
route, including legacy endpoints such as:
- `/api/public/traces`
- `/api/public/traces/<traceId>` (`/api/public/traces/[traceId]` in Next.js)
- `/api/public/observations`
- `/api/public/observations/<observationId>`
(`/api/public/observations/[observationId]` in Next.js)
- `/api/public/metrics`
Migrated endpoints commonly include:
- `/api/public/v2/observations`
- `/api/public/v2/metrics`
- `/api/public/otel/v1/traces`
## Why Correlation Is Needed
Public API auth attaches tenant attributes to the `api-auth-verify` child span:
- `@langfuse.project.id`: project-scoped API key target.
- `@langfuse.org.id`: owning org or org-scoped key target.
- `@langfuse.org.plan`: plan when present.
The HTTP route is on the request root span, usually in `http.path_group`,
`http.route`, `http.target`, or `resource_name`.
Because tenant tags and route tags often live on different spans in the same
trace, do not expect this single-span query to work:
```text
@langfuse.project.id:<id> @http.path_group:/api/public/observations
```
## Per-Tenant Endpoint Recipe
1. Aggregate auth spans for the tenant to confirm the identifier and active
projects.
```text
env:<env> @langfuse.org.id:<orgId> resource_name:api-auth-verify
env:<env> @langfuse.project.id:<projectId> resource_name:api-auth-verify
```
Group by `service`, `@langfuse.project.id`, and optionally a daily interval.
2. Fetch representative matching auth spans with `search_datadog_spans`.
Include custom attributes such as `langfuse.*`, then copy representative
`traceid` values.
3. Open those traces with `get_datadog_trace`. Request service-entry spans and
include HTTP and Langfuse attributes:
```text
only_service_entry_spans: true
extra_fields: ["http.*", "next.*", "langfuse.*"]
```
4. Read the request root span's `http.path_group`, `http.route`,
`http.target`, and `resource_name` to identify the endpoint.
ID lookup routes may appear with the concrete ID in `http.target` or with
the normalized Next.js route, such as
`/api/public/traces/[traceId]` or
`/api/public/observations/[observationId]`.
5. Repeat across all relevant prod environments and both Datadog sites when the
user asks for a global answer.
Treat per-tenant endpoint results as sampled unless you have an unsampled
metric or log source with tenant and route on the same event.
## Fleet-Level Endpoint Volume
For endpoint volume without tenant scoping, aggregate request spans directly:
```text
env:<env> service:web resource_name:"GET /api/public/observations*"
env:<env> service:web resource_name:"GET /api/public/observations/*"
env:<env> service:web resource_name:"POST /api/public/traces*"
env:<env> service:web resource_name:"GET /api/public/traces/*"
env:<env> service:web resource_name:"POST /api/public/metrics*"
```
Use route facets where available:
```text
env:<env> service:web @http.path_group:/api/public/observations
env:<env> service:web @http.path_group:/api/public/observations/*
env:<env> service:web @http.path_group:/api/public/observations/[observationId]
env:<env> service:web @http.route:/api/public/observations
env:<env> service:web @http.route:/api/public/observations/[observationId]
env:<env> service:web @http.path_group:/api/public/traces/[traceId]
env:<env> service:web @http.route:/api/public/traces/[traceId]
```
If route facets and resource names disagree, fetch a few traces and inspect the
root span before reporting the result.
@@ -0,0 +1,234 @@
# Queue Consumers
Use this reference when a task asks which Langfuse queues exist, whether a
consumer is running, how much work a queue has, or how to query queue processor
spans.
## Source Of Truth
- Queue names and job names:
`packages/shared/src/server/queues.ts` (`QueueName`, `QueueJobs`).
- Queue producer classes and shard naming:
`packages/shared/src/server/redis/*.ts`.
- Worker consumer registration and feature gates:
`worker/src/app.ts`.
- Worker consumer env vars:
`worker/src/env.ts` (`QUEUE_CONSUMER_*_IS_ENABLED` plus feature-specific
gates).
- Worker registration, request/error counters, wait/processing time, and
sampled old-style depth metrics:
`worker/src/queues/workerManager.ts`.
- Queue depth background reporter:
`worker/src/features/queue-metrics-runner/index.ts`.
- Metric name conversion:
`packages/shared/src/server/instrumentation/index.ts`
(`convertQueueNameToMetricName`).
- Sharded queue registry:
`worker/src/queues/shardedQueueRegistry.ts`.
- BullMQ tracing setup:
`worker/src/instrumentation.ts` (`BullMQInstrumentation`).
## Queue Inventory
Current `QueueName` values:
| Queue | Notes |
| --- | --- |
| `trace-upsert` | Sharded. Registers all `TraceUpsertQueue` shards. |
| `trace-delete` | Delete traces from storage. |
| `project-delete` | Project deletion cleanup. |
| `evaluation-execution-queue` | Sharded eval execution. |
| `secondary-evaluation-execution-queue` | Sharded secondary eval execution. |
| `llm-as-a-judge-execution-queue` | Sharded observation-based eval execution. |
| `dataset-run-item-upsert-queue` | Dataset run item upserts. |
| `batch-export-queue` | Batch exports. |
| `otel-ingestion-queue` | Sharded OTel ingestion. |
| `secondary-otel-ingestion-queue` | Sharded secondary OTel ingestion. |
| `ingestion-queue` | Sharded single-event ingestion. |
| `secondary-ingestion-queue` | Sharded secondary single-event ingestion. |
| `cloud-usage-metering-queue` | Cloud-only, Stripe-gated. |
| `cloud-spend-alert-queue` | Cloud-only, Stripe-gated. |
| `cloud-free-tier-usage-threshold-queue` | Cloud-only, Stripe-gated. |
| `experiment-create-queue` | Experiment creation. |
| `posthog-integration-queue` | Schedules PostHog integration jobs. |
| `posthog-integration-processing-queue` | Processes PostHog projects. |
| `mixpanel-integration-queue` | Schedules Mixpanel integration jobs. |
| `mixpanel-integration-processing-queue` | Processes Mixpanel projects. |
| `blobstorage-integration-queue` | Schedules blob storage jobs. |
| `blobstorage-integration-processing-queue` | Processes blob storage projects. |
| `core-data-s3-export-queue` | Cloud export feature gate. |
| `metering-data-postgres-export-queue` | Cloud export feature gate. |
| `data-retention-queue` | Schedules data retention jobs. |
| `data-retention-processing-queue` | Processes data retention projects. |
| `batch-action-queue` | Batch actions. |
| `create-eval-queue` | Eval job creation. |
| `score-delete` | Score deletion cleanup. |
| `dataset-delete-queue` | Dataset deletion cleanup. |
| `dead-letter-retry-queue` | Dead letter retry worker. |
| `webhook-queue` | Webhook delivery. |
| `entity-change-queue` | Entity change propagation. |
| `event-propagation-queue` | Experiment event propagation gate. |
| `notification-queue` | Notifications. |
Sharded queues use the base queue for shard 0 and append `-1`, `-2`, etc. for
additional shards. The sharded base queues are:
- `trace-upsert`
- `evaluation-execution-queue`
- `secondary-evaluation-execution-queue`
- `llm-as-a-judge-execution-queue`
- `otel-ingestion-queue`
- `secondary-otel-ingestion-queue`
- `ingestion-queue`
- `secondary-ingestion-queue`
## Consumer Gates
Consumer registration is in `worker/src/app.ts`. Some gates register multiple
queues or every shard for a sharded queue.
| Gate | Queues registered |
| --- | --- |
| `QUEUE_CONSUMER_TRACE_UPSERT_QUEUE_IS_ENABLED` | `trace-upsert` shards |
| `QUEUE_CONSUMER_CREATE_EVAL_QUEUE_IS_ENABLED` | `create-eval-queue` |
| `LANGFUSE_S3_CORE_DATA_EXPORT_IS_ENABLED` | `core-data-s3-export-queue` |
| `LANGFUSE_POSTGRES_METERING_DATA_EXPORT_IS_ENABLED` | `metering-data-postgres-export-queue` |
| `QUEUE_CONSUMER_TRACE_DELETE_QUEUE_IS_ENABLED` | `trace-delete` |
| `QUEUE_CONSUMER_SCORE_DELETE_QUEUE_IS_ENABLED` | `score-delete` |
| `QUEUE_CONSUMER_DATASET_DELETE_QUEUE_IS_ENABLED` | `dataset-delete-queue` |
| `QUEUE_CONSUMER_PROJECT_DELETE_QUEUE_IS_ENABLED` | `project-delete` |
| `QUEUE_CONSUMER_DATASET_RUN_ITEM_UPSERT_QUEUE_IS_ENABLED` | `dataset-run-item-upsert-queue` |
| `QUEUE_CONSUMER_EVAL_EXECUTION_QUEUE_IS_ENABLED` | `evaluation-execution-queue` shards, `llm-as-a-judge-execution-queue` shards |
| `QUEUE_CONSUMER_EVAL_EXECUTION_SECONDARY_QUEUE_IS_ENABLED` | `secondary-evaluation-execution-queue` shards |
| `QUEUE_CONSUMER_BATCH_EXPORT_QUEUE_IS_ENABLED` | `batch-export-queue` |
| `QUEUE_CONSUMER_BATCH_ACTION_QUEUE_IS_ENABLED` | `batch-action-queue` |
| `QUEUE_CONSUMER_OTEL_INGESTION_QUEUE_IS_ENABLED` | `otel-ingestion-queue` shards |
| `QUEUE_CONSUMER_OTEL_INGESTION_SECONDARY_QUEUE_IS_ENABLED` | `secondary-otel-ingestion-queue` shards |
| `QUEUE_CONSUMER_INGESTION_QUEUE_IS_ENABLED` | `ingestion-queue` shards |
| `QUEUE_CONSUMER_INGESTION_SECONDARY_QUEUE_IS_ENABLED` | `secondary-ingestion-queue` shards |
| `QUEUE_CONSUMER_CLOUD_USAGE_METERING_QUEUE_IS_ENABLED` plus `STRIPE_SECRET_KEY` | `cloud-usage-metering-queue` |
| `QUEUE_CONSUMER_CLOUD_SPEND_ALERT_QUEUE_IS_ENABLED` plus `STRIPE_SECRET_KEY` | `cloud-spend-alert-queue` |
| `QUEUE_CONSUMER_FREE_TIER_USAGE_THRESHOLD_QUEUE_IS_ENABLED` plus cloud region and Stripe gates | `cloud-free-tier-usage-threshold-queue` |
| `QUEUE_CONSUMER_EXPERIMENT_CREATE_QUEUE_IS_ENABLED` | `experiment-create-queue` |
| `QUEUE_CONSUMER_POSTHOG_INTEGRATION_QUEUE_IS_ENABLED` | `posthog-integration-queue`, `posthog-integration-processing-queue` |
| `QUEUE_CONSUMER_MIXPANEL_INTEGRATION_QUEUE_IS_ENABLED` | `mixpanel-integration-queue`, `mixpanel-integration-processing-queue` |
| `QUEUE_CONSUMER_BLOB_STORAGE_INTEGRATION_QUEUE_IS_ENABLED` | `blobstorage-integration-queue`, `blobstorage-integration-processing-queue` |
| `QUEUE_CONSUMER_DATA_RETENTION_QUEUE_IS_ENABLED` | `data-retention-queue`, `data-retention-processing-queue` |
| `QUEUE_CONSUMER_DEAD_LETTER_RETRY_QUEUE_IS_ENABLED` | `dead-letter-retry-queue` |
| `QUEUE_CONSUMER_WEBHOOK_QUEUE_IS_ENABLED` | `webhook-queue` |
| `QUEUE_CONSUMER_ENTITY_CHANGE_QUEUE_IS_ENABLED` | `entity-change-queue` |
| `QUEUE_CONSUMER_EVENT_PROPAGATION_QUEUE_IS_ENABLED` plus events-table experiment gate | `event-propagation-queue` |
| `QUEUE_CONSUMER_NOTIFICATION_QUEUE_IS_ENABLED` | `notification-queue` |
## Query Consumer Spans
Start with aggregate spans on worker services:
```text
env:<env> (service:worker OR service:worker-cpu) operation_name:bullmq.process
```
Then group by `resource_name`, queue facets such as `bullmq.queue` or
`messaging.*`, and error fields. Facet names can differ between Datadog sites,
so inspect one sample span before relying on a specific facet.
Queue-specific starter query:
```text
env:<env> (service:worker OR service:worker-cpu) operation_name:bullmq.process (resource_name:"process otel-ingestion-queue" OR resource_name:"Worker.run otel-ingestion-queue" OR bullmq.queue:otel-ingestion-queue)
```
For sharded queues, query the base queue and shard suffixes:
```text
env:<env> (service:worker OR service:worker-cpu) operation_name:bullmq.process resource_name:"*otel-ingestion-queue*"
```
If a queue file wraps the handler with `instrumentAsync`, also search the
domain-specific resource name. Examples:
| Subsystem | Resource name |
| --- | --- |
| PostHog project processing | `process posthog-integration-project` |
| Mixpanel project processing | `process mixpanel-integration-project` |
| Blob storage project processing | `process blob-storage-project` |
| Data retention project processing | `process data-retention-project` |
| Event propagation | `process event-propagation` |
| Cloud usage metering | `process cloud-usage-metering` |
| Free-tier usage threshold | `process cloud-free-tier-usage-threshold` |
Useful aggregations:
- Count by `env`, `service`, and `resource_name`.
- Count by queue facet and status.
- Count by `error.type` and `error.message`.
- p50, p95, and p99 duration by queue or shard.
- Count by `messaging.bullmq.job.input.projectId` when the processor attaches
project IDs to the current span.
## Query Queue Metrics
Metric base names come from `convertQueueNameToMetricName(queueName)`:
```text
langfuse.queue.<queue-name-with-hyphens-replaced-by-underscores-and-trailing-_queue-removed>
```
Examples:
| Queue | Metric base |
| --- | --- |
| `ingestion-queue` | `langfuse.queue.ingestion` |
| `otel-ingestion-queue` | `langfuse.queue.otel_ingestion` |
| `secondary-otel-ingestion-queue` | `langfuse.queue.secondary_otel_ingestion` |
| `evaluation-execution-queue` | `langfuse.queue.evaluation_execution` |
| `trace-upsert` | `langfuse.queue.trace_upsert` |
| `batch-export-queue` | `langfuse.queue.batch_export` |
Prefer the newer tagged metrics:
```text
<metric_base>.depth{env:<env>,type:waiting}
<metric_base>.depth{env:<env>,type:failed}
<metric_base>.depth{env:<env>,type:active}
<metric_base>.rate{env:<env>,type:request}
<metric_base>.rate{env:<env>,type:failed}
<metric_base>.rate{env:<env>,type:error}
<metric_base>.time{env:<env>,type:wait}
<metric_base>.time{env:<env>,type:processing}
```
For sharded queues, use the `shard` tag when present. `shard:all` is emitted by
the depth runner for aggregate depth across shards.
Backward-compatible metrics may still appear:
```text
<metric_base>.length
<metric_base>.dlq_length
<metric_base>.active
<metric_base>.request
<metric_base>.failed
<metric_base>.error
<metric_base>.wait_time
<metric_base>.processing_time
```
For non-BullMQ internal write buffering, `ClickhouseWriter` emits
`langfuse.queue.clickhouse_writer.*` metrics, but it is not a `QueueName`
consumer.
## Consumer Running Checklist
To establish whether a consumer is running in production:
1. Check queue depth metrics for waiting, failed, and active counts.
2. Check `rate{type:request}` or old `.request` metrics for recent processing.
3. Search BullMQ processor spans on `worker` and `worker-cpu`.
4. Search worker logs for the queue name or processor-specific log prefix.
5. If all signals are empty, verify the relevant `QUEUE_CONSUMER_*_IS_ENABLED`
gate and any feature-specific gates in `worker/src/app.ts`.
The queue metrics runner only polls queues with registered workers. Missing
depth metrics can mean the consumer is not registered on that worker, queue
metrics are disabled, or the data is on the other Datadog site.
@@ -0,0 +1,121 @@
---
name: debug-issue-with-datadog
description: |
Debug a user-reported issue, Linear ticket, or incident report by combining
Datadog (APM, logs, metrics) with the Langfuse repo to establish a
root cause. Use when given a Linear issue URL/ID (e.g. LFE-XXXX), a GitHub
issue, or a pasted error/report and asked to investigate, root-cause, or
triage. Produces a structured analysis — error breakdown, hypothesis-by-class,
suggested patches with code references.
---
# Debug Issue with Datadog
Use this skill whenever the task is **investigative** rather than
implementational: a user, customer, or oncall has surfaced a problem and you
need to figure out *what is actually happening in production* and *where in the
code it lives*. The deliverable is an analysis, not a patch — though the
analysis should make the right patch obvious.
## When to Apply
- A Linear issue (typically with an `LFE-XXXX` ID) describes a production
failure, error spike, or customer report.
- A GitHub issue or pasted incident/error report needs triage.
- A monitor alerted and you need to understand *why* before deciding what to
fix.
- Existing tickets under the "Make monitoring useful again" project (parent
`LFE-8837`) and similar — these expect the structured analysis output below.
If the task is "implement this fix" rather than "figure out what's broken",
this is the wrong skill — go to `backend-dev-guidelines` or the relevant
package guide.
## Workflow
Read the inputs first, then plan the Datadog sweep, then read the code, then
write the analysis. Do not skip ahead to suggested patches before the data
supports them.
1. **Intake.** Pull every signal already available in the report. See
[`references/intake.md`](references/intake.md). For a Linear URL/ID, fetch
the issue *and* its comments via the Linear MCP — the description is often
updated inline as triage proceeds. For a GitHub issue, use `gh issue view`.
For pasted text, treat it as the description.
2. **Scope the sweep.** From the intake, pick the affected subsystem and time
window. Use [`references/repo-debug-map.md`](references/repo-debug-map.md)
to translate "PostHog integration", "ingestion failures", "evals stuck",
etc. into the Datadog filters and source files you should be looking at.
3. **Run the broad Datadog sweep.** Default to the full sweep in
[`references/datadog-playbook.md`](references/datadog-playbook.md): APM
spans, error logs, metrics, and monitors — split across `prod-eu`
and `prod-us` (and `prod-hipaa` / `prod-jp` when relevant). Always check
regional disparity first; it usually rules whole hypotheses in or out.
Use [`datadog-query-recipes`](../datadog-query-recipes/SKILL.md) for
reusable tenant, public API, queue consumer, and cross-environment query
shapes.
4. **Cluster the errors.** Group by `(projectId, error.message)` or
`(error.type, error.message)`. Treat each distinct cluster as its own
hypothesis — Langfuse incidents commonly have *multiple* coexisting root
causes, not one.
5. **Map clusters to code.** For each cluster, open the relevant handler file
from the repo-debug map and read enough of it to confirm or refute the
hypothesis. Cite specific files and line ranges in the output.
6. **Write the analysis** using
[`references/output-template.md`](references/output-template.md).
7. **Deliver.** Default: print the analysis in chat. If the user asked for it,
also save under the workflow they specified (file, Linear comment via, etc.).
## Datadog MCP Usage Notes
Two Datadog MCP servers are typically available — one bound to the EU site
(`datadoghq.eu`) and one to the US site (`datadoghq.com`). Always run
region-relevant queries against **both** unless intake clearly localizes the
incident. The `prod-eu` / `prod-us` env tags live on each side respectively.
- Span search filter pattern:
`service:worker resource_name:"process posthog-integration-project" status:error`
- Log search filter pattern:
`service:worker env:prod-eu @langfuse.project.id:cm1r6u… status:error`
- For high-volume queries, prefer `aggregate_spans` / `aggregate_events`
grouped by `(error.message, projectId)` over fetching individual traces.
- Always link to the Datadog UI for the queries you ran (final section of the
output template).
See [`references/datadog-playbook.md`](references/datadog-playbook.md) for the
full set of starter queries and parameter shapes.
## Output Expectations
From the output template:
- Header: data source, time window, region split (EU vs US table).
- Hotspots: per-`projectId` (or per-cluster) error counts.
- Root cause by error class: each cluster gets a short hypothesis with
reasoning, distinguishing primary causes from symptoms.
- Suggested patches: P0/P1/P2 grouped, with concrete file paths and short code
sketches. Reference the actual handler in `worker/src/features/**` or
`web/src/**`.
- Dashboards: paste the Datadog query URLs at the end.
Findings come first, recommendations last. If the data is thin, say so
explicitly and propose what would need to be true to confirm each hypothesis —
do not invent root causes.
## Cross-References
- Production telemetry query recipes, tenant/public API usage, and queue
consumer measurements:
[`datadog-query-recipes`](../datadog-query-recipes/SKILL.md)
- Backend layout, queue contracts, instrumentation patterns:
[`backend-dev-guidelines`](../backend-dev-guidelines/SKILL.md)
- ClickHouse-related findings (memory ceilings, JOIN spills, slow queries):
[`clickhouse-best-practices`](../clickhouse-best-practices/SKILL.md)
- Once a fix is identified and you switch to implementation, hand off to the
package `AGENTS.md` for the affected directory.
@@ -0,0 +1,141 @@
# Datadog Query Playbook
The default for this skill is the **broad sweep**: APM spans + logs + metrics
+ monitors + incidents, run against both EU and US sites unless intake clearly
localizes. Cluster results before drilling in.
Two MCP servers are typically connected — one for `datadoghq.eu`, one for
`datadoghq.com`. Run the same query on both and compare. The contrast itself
is often the most informative finding (e.g. LFE-9475's 23.5% EU vs 0.7% US
error rate immediately ruled out PostHog Cloud as the global cause).
## Tag Vocabulary
- **Site:** `datadoghq.eu` for EU, `datadoghq.com` for US.
- **Region tag (`env`):** `prod-eu`, `prod-us`, `prod-hipaa`, `prod-jp`.
- **Service:** `worker` (default in `worker/src/env.ts`) or `web` (default in
`web/src/env.mjs`). Some deployments override to `langfuse`.
- **Span resource names** for worker async jobs follow the pattern
`process <queue-name>` — see `repo-debug-map.md`.
## 1. APM Span Sweep — find the failing handler
Use `aggregate_spans` first; only fetch individual traces once a cluster is
identified.
Starter shape (rename the resource for the relevant subsystem):
```text
service:worker resource_name:"process posthog-integration-project" status:error
```
Aggregations to run, in order:
1. Count by `env` — confirms region split.
2. Count by `error.message` — primary error classes.
3. Count by `(projectId, error.message)` — which tenants are affected, by
class. `projectId` lives on span tags as `@projectId` for log search and as
a tag for spans (depends on instrumentation site).
4. p50 / p95 / p99 duration by region — fingerprints timeouts vs. crashes vs.
slow successes.
If `aggregate_spans` returns no results, check:
- the resource name is right (case-sensitive, see `repo-debug-map.md`);
- the time window covers when the issue was actually firing;
- the region tag matches reality (the EU MCP only sees EU traces).
### Public API tenant / legacy-endpoint usage
For tenant-specific public API route usage, use
[`../../datadog-query-recipes/references/public-api-tenant-usage.md`](../../datadog-query-recipes/references/public-api-tenant-usage.md).
The key gotcha is that tenant tags usually live on the `api-auth-verify` child
span while the HTTP route lives on the request root span, so correlate by
`traceid` rather than relying on one combined span filter.
## 2. Log Sweep — read what the handler said
Logs are the right tool for *messages* the handler emitted. Spans are the
right tool for *which handler invocations failed*.
Starter shapes:
```text
service:worker env:prod-eu @projectId:cm1r6u1iq00ccfvrkoy8vg3ms status:error
service:worker env:prod-eu "[POSTHOG]" status:error
```
Useful log facets:
- `@projectId` or `@langfuse.project.id` — Langfuse project (cuid).
- `@error.kind` / `@error.message` / `@error.stack` — when the Winston logger
serialized an Error.
- `@queue` / `@jobName` — when set by BullMQ instrumentation.
For high-volume subsystems (`ingestion-queue`, `otel-ingestion-queue`),
prefer `analyze_datadog_logs` with grouping over `search_datadog_logs` — the
raw matches are too noisy.
## 3. Metric Sweep — confirm the trend
Pick 23 metrics that match the subsystem. Common ones:
- `trace.bullmq.process.errors` and `trace.bullmq.process.duration`
per-queue health from the BullMQ OTel instrumentation. Filter by
`resource_name:"process <queue-name>"`.
- `trace.http_request.errors` and `trace.http_request.duration` for HTTP
handlers (`service:web`).
- ClickHouse: cluster-level `clickhouse.query.duration`,
`clickhouse.memory_usage` — the worker doesn't emit these directly, they
come from the `clickhouse` integration in the infra repo.
- Postgres: `aurora.databaseconnections`, `aurora.deadlocks` — relevant when
the symptom is `connection_limit` / `connection pool` errors.
If the subsystem isn't already known, run `search_datadog_metrics` for the
subsystem name and pick the obvious counter / gauge / histogram triplet.
## 4. Monitors & Incidents
- `search_datadog_monitors` for the subsystem name — tells you what alerts
*would* have fired and what their thresholds are. A muted monitor on the
affected subsystem is itself a finding (see LFE-9475: "EU alert muted for
a week").
- `search_datadog_incidents` for the time window — links any pre-existing
incident the user may not have referenced.
## 5. RUM / Frontend (only when the symptom is user-facing)
Skip unless the issue is "page broken" / "slow load". Then:
- `search_datadog_rum_events` filtered by `@view.url:` patterns matching the
affected route.
- Cross-reference with `service:web` API errors at the same time.
## 6. Trace Drill-Down
Once a cluster is identified, fetch one or two representative traces with
`get_datadog_trace` to read the actual stack and confirm where in the handler
the throw originates. This is what lets you point at a specific file and
line range in the analysis.
## Anti-Patterns
- Don't fetch individual logs/traces before aggregating. You'll burn context
on noise and miss the cluster pattern.
- Don't trust a single-region query as global. Always compare EU and US.
- Don't read an `error.message` literally if it goes through a custom error
wrapper — `validateWebhookURL` rejections, for example, are re-logged as
"DNS lookup failed" but are actually validator rejections.
- Don't assume monitors are firing just because errors exist — check if the
monitor is muted.
## Linking Out
End the analysis with the actual Datadog UI URLs you queried, e.g.:
```text
https://app.datadoghq.eu/apm/traces?query=resource_name%3A%22process+posthog-integration-project%22+status%3Aerror
https://app.datadoghq.com/apm/traces?query=resource_name%3A%22process+posthog-integration-project%22+status%3Aerror
```
so the human reader can re-run the same query.
@@ -0,0 +1,72 @@
# Intake — What Are We Actually Investigating?
Goal: extract every signal from the input *before* you touch Datadog. Cheap
to do, expensive to skip — the wrong time window or wrong service tag can
hide the real cause.
## Source-by-Source Recipes
### Linear issue (URL or `LFE-XXXX` ID)
1. Fetch the issue via the Linear MCP:
`mcp__d39d26f2-…__get_issue` with `id: "LFE-XXXX"` and
`includeRelations: true`.
2. Fetch comments separately:
`mcp__d39d26f2-…__list_comments` with the same `issueId`.
3. Note: Langfuse triages **inside the issue description**. The description
often grows reaction sections (`projectId: <one-line note>`) as oncall
investigates. Treat both description and comments as authoritative state.
4. Pull `attachments` for linked PRs/commits — these show what's already been
tried. Reading the diff of a half-merged fix often reveals the original
theory of the bug.
5. Pull labels (`integration-posthog`, `feat-exports`, etc.) — they map
directly to the subsystem clusters in `repo-debug-map.md`.
### GitHub issue
1. `gh issue view <url-or-number> --json title,body,labels,comments,assignees`.
2. If there's a stack trace in the issue body, copy the top frames into your
notes — those frames usually map straight to a handler file.
### Pasted error / incident text
1. Treat as the issue description. If it's a stack trace, identify:
- the throwing module (handler vs. SDK vs. infra),
- whether it looks like a *user-input* failure (HTTP 4xx, validation, auth)
or an *infra* failure (5xx, timeout, OOM, DNS, pool exhaustion),
- the error class (`Error`, `TypeError`, `PrismaClientKnownRequestError`,
etc.).
2. If a `projectId` appears, that's gold — anchor every Datadog query on it.
3. If a `traceId` (Datadog's, not Langfuse's) appears, jump straight to
`get_datadog_trace`.
## Extract The Following Before Querying
Build a small notes block. If a value is missing, mark it `?` rather than
guessing — Datadog will tell you what's missing.
- **Subsystem.** PostHog integration, blob-storage export, evaluation
execution, OTel ingestion, batch action, webhook delivery, etc. Map to
`repo-debug-map.md`.
- **Region(s).** `prod-eu` / `prod-us` / `prod-hipaa` / `prod-jp`. If unknown,
query both EU and US.
- **Service.** Most issues are `service:worker`; UI/API timeouts are
`service:web`. Check for both when unsure.
- **Time window.** Default to 7 days back from the issue's `createdAt`. If
the issue references a specific incident or alert, use that window ±1 day.
- **`projectId`(s).** Project IDs in Langfuse are `cuid`-shaped
(`cl…` / `cm…`, 25 chars). The reaction blocks in Linear descriptions
often *are* lists of affected project IDs.
- **Error message fragments.** Exact substrings to grep for in DD logs:
`Header overflow`, `Timeout error.`, `HTTP 403`, `DNS lookup failed`,
`Cannot write to canceled buffer`, `connection pool`, etc.
- **Already-attempted fixes.** Linked PRs/commits on the issue. Read their
diffs — your analysis must not re-recommend something that's already
shipped.
## Output Of The Intake Step
A short bullet list (not yet formatted as the final analysis) with each of
the above filled in. The remaining steps key off this — `datadog-playbook.md`
expects subsystem + region + window, `repo-debug-map.md` expects subsystem,
and the output template wants the affected projects.
@@ -0,0 +1,127 @@
# Output Template
The analysis should be structured so it can be pasted directly as the first
investigative comment on the Linear issue. The example to anchor on is the
first comment on `LFE-9475` (PostHog Integration Processing Failures).
Findings come first, recommendations last. If the data doesn't support a
hypothesis, say so — do not invent root causes to fill the template.
## Section Order
1. **Header** — data source, time window, scope of sweep.
2. **Volume & error-rate split** — table by region (always).
3. **Hotspots** — table by `(projectId, dominant cause)` or by cluster.
4. **Root cause by error class** — one numbered subsection per cluster.
5. **Suggested patches** — P0 / P1 / P2, each with file paths and a short
code sketch.
6. **Dashboards** — Datadog UI URLs for the queries you ran.
## Skeleton (fill in with your findings)
````markdown
## Datadog APM + log analysis (<N>-day window, <YYYY-MM-DD> → <YYYY-MM-DD>)
Source: APM spans with `resource_name:"process <queue-name>"` across EU and US.
### Volume & error rate — <one-line summary of regional split>
| Region | Total spans | Errors | Error rate |
|---|---|---|---|
| EU (`prod-eu`) | <n> | <n> | **<pct>%** |
| US (`prod-us`) | <n> | <n> | **<pct>%** |
<One sentence explaining where the noise actually lives.>
### Hotspots — concentrated on ~<N> <region> projects
<Region> errors break down by `(projectId, error.message)`:
| ProjectId | Errors | Dominant cause |
|---|---|---|
| `<projectId>` | <n> | `<error message>` (<n>) + others |
| ... | ... | ... |
<Optional: contrast with another subsystem if relevant — e.g.
"Unlike blob storage, PostHog has multiple distinct root causes — not one
hotspot pattern.">
## Root cause by error class
### 1. `<error message>` — <n> errors, <n> projects
<24 sentences explaining what this error class actually is at the
implementation level (which library, which call site). Then list candidate
causes in order of likelihood. Mark which ones are confirmed by the data
vs. speculative.>
### 2. `<error message>` — <n> errors, mostly <n> projects
<Same pattern.>
### 3. <next class>
<...>
### <N>. <Symptom of upstream failure>
<Use this slot when a class is a *symptom* of another class rather than an
independent bug — call it out so suggested patches don't double-count.>
## Suggested patches
### P0 — <one-line summary, e.g. "Auto-disable integrations on persistent
auth failures">
<Why this is P0 — what noise it kills, what data it stops corrupting, what
unblocks downstream work.>
```ts
// <relative path from repo root>
// Short code sketch (520 lines). It does not need to compile —
// it must communicate the shape of the change.
```
### P0 — <next P0>
<...>
### P1 — <smaller / less urgent fix>
<Same shape.>
### P2 — <separate-but-surfaced finding>
<E.g. a Prisma pool sizing issue surfaced incidentally by this analysis but
not the original bug. Call it out with its own section so it doesn't get
lost.>
### Regional split explanation (only if relevant)
<One paragraph explaining why EU vs. US asymmetry exists — usually not an
infra bug, just where the affected tenants happen to live.>
Dashboards:
- EU APM: <url>
- US APM: <url>
- (logs / metrics / monitor links as relevant)
````
## Style Rules
- Lead with numbers, not adjectives. "23.5% error rate" beats "very noisy".
- Distinguish **primary causes** from **symptoms** explicitly. Symptoms
shouldn't get their own P0 patch.
- Always cite specific files when proposing a code change. A patch
recommendation without `worker/src/features/<…>/<file>.ts` is unfinished.
- Code sketches are illustrative — clearly mark them as sketches if they
hand-wave types. The next agent / human will write the real diff.
- If the analysis surfaces a finding *outside* the original ticket scope
(e.g. a Prisma pool issue while debugging PostHog), include it as a P2
with a sentence explaining it's separate.
- If the data refuses to converge on a single root cause, say so. The
template handles N classes — use as many subsections as the data warrants.
## When To Skip Sections
- **Single-region deployments:** if the issue clearly affects only one
region, you can replace the "Volume & error rate" table with a single-row
variant, but still note that the other region was checked and clean.
- **No code change recommended:** if the only finding is "the affected
tenants have misconfigured credentials and we should reach out", the
Suggested-patches section can be a single sentence — but still include
the dashboards.
- **Aborted investigation:** if Datadog access fails or the data is
insufficient, write what you tried, what was missing, and what would let
the next investigator pick it up.
@@ -0,0 +1,100 @@
# Repo Debug Map — Subsystem → Code → Datadog Filters
For each subsystem we ship monitors and incidents on, this is the canonical
map between the symptom, the Datadog query that surfaces it, and the source
files where the bug almost certainly lives.
When intake gives you a subsystem (PostHog, evals, exports, etc.), start
here to pick the right Datadog filters and the right files to read.
## Worker Async Jobs
Worker handlers are wrapped by `instrumentAsync` in their queue file. The
span resource name follows the pattern `process <queue-name>`. Queue and job
name constants live in
`packages/shared/src/server/queues.ts`
(`QueueName` and `QueueJobs` enums).
| Subsystem | Queue file | Handler dir | Span `resource_name` | Log prefix |
| --- | --- | --- | --- | --- |
| PostHog integration | `worker/src/queues/postHogIntegrationQueue.ts` | `worker/src/features/posthog/` | `process posthog-integration-project` | `[POSTHOG]` |
| Mixpanel integration | `worker/src/queues/mixpanelIntegrationQueue.ts` | `worker/src/features/mixpanel/` | `process mixpanel-integration-project` | `[MIXPANEL]` |
| Blob storage export | `worker/src/queues/blobStorageIntegrationQueue.ts` | `worker/src/features/blobstorage/` | `process blob-storage-project` | `[BLOBSTORAGE]` |
| Data retention | `worker/src/queues/dataRetentionQueue.ts` | `worker/src/features/batch-data-retention-cleaner/` | `process data-retention-project` | n/a |
| Event propagation | `worker/src/queues/eventPropagationQueue.ts` | `worker/src/features/eventPropagation/` | `process event-propagation` | n/a |
| Cloud usage metering | `worker/src/queues/cloudUsageMeteringQueue.ts` | `worker/src/ee/` (cloud-only) | `process cloud-usage-metering` | n/a |
| Free-tier usage threshold | `worker/src/queues/cloudFreeTierUsageThresholdQueue.ts` | `worker/src/ee/usageThresholds/` | `process cloud-free-tier-usage-threshold` | n/a |
| Ingestion (single event) | `worker/src/queues/ingestionQueue.ts` | `worker/src/features/ingestion/` (and `IngestionService`) | BullMQ default span | n/a |
| OTel ingestion | `worker/src/queues/otelIngestionQueue.ts` | `worker/src/features/otel/` | BullMQ default span | n/a |
| Evaluation execution | `worker/src/queues/evalQueue.ts` | `worker/src/features/evaluation/` | BullMQ default span | n/a |
| Batch export | `worker/src/queues/batchExportQueue.ts` | `worker/src/features/batchExport/` | BullMQ default span | n/a |
| Webhook delivery | `worker/src/queues/webhooks.ts` | `worker/src/features/webhooks/` | BullMQ default span | n/a |
| Trace / score / dataset / project delete | `worker/src/queues/{traceDelete,scoreDelete,datasetDelete,projectDelete}.ts` | `worker/src/features/traces/`, `…/scores/`, `…/datasets/` | BullMQ default span | n/a |
For queues using BullMQ default spans (no `instrumentAsync` wrapper), search
APM with `service:worker operation_name:bullmq.process` filtered by
`bullmq.queue:<queue-name>`. For queue inventory, sharded queue naming, and
queue metric recipes, use
[`../../datadog-query-recipes/references/queue-consumers.md`](../../datadog-query-recipes/references/queue-consumers.md).
## Web (Next.js / tRPC / public API)
| Subsystem | Code | Span / log filter |
| --- | --- | --- |
| Public REST API | `web/src/pages/api/public/**` | Request span: `service:web resource_name:"GET /api/public/<path>"`; tenant span: `resource_name:api-auth-verify` with `@langfuse.project.id` / `@langfuse.org.id` |
| tRPC procedures | `web/src/server/api/routers/**` | `service:web resource_name:"POST /api/trpc/<router>.<proc>"` |
| Auth / API key verification | `web/src/features/public-api/server/apiAuth.ts` | look for `verifyAuthHeaderAndReturnScope` spans |
| Stripe billing | `web/src/ee/features/billing/server/stripeBillingService.ts` | wrapped in `instrumentAsync`; spans named after the method |
For tenant-specific public API usage questions, first query
`resource_name:api-auth-verify` by `@langfuse.project.id` or
`@langfuse.org.id`, then open representative trace IDs and inspect the request
root span for `http.path_group`, `http.route`, and `http.target`. The tenant
tags and endpoint path are usually on different spans, so a single-span query
combining both may return no results even when the trace proves usage.
For the full reusable recipe, use
[`../../datadog-query-recipes/references/public-api-tenant-usage.md`](../../datadog-query-recipes/references/public-api-tenant-usage.md).
## Shared Layers
These are not subsystems on their own, but are *frequently the actual cause*
behind a worker subsystem failure.
| Layer | Location | Common failure modes |
| --- | --- | --- |
| ClickHouse access | `packages/shared/src/server/clickhouse/`, `packages/shared/src/server/repositories/` | OOM (`Code: 241`), buffer cancel (`Code: 734`), JOIN spills, slow queries on un-pre-filtered traces |
| Prisma access | `packages/shared/src/db.ts` and per-feature repos | `connection pool timeout` (worker default `connection_limit=5`), N+1 queries |
| Queue contracts | `packages/shared/src/server/queues.ts` | wrong queue name, missing schema validation |
| Logger / instrumentation | `packages/shared/src/server/logger.ts`, `packages/shared/src/server/instrumentation.ts` | log silently dropped because `LANGFUSE_LOG_LEVEL` set wrong, or span missing because handler doesn't call `instrumentAsync` |
| Webhook URL validation | `packages/shared/src/server/validateWebhookURL.ts` | rejects with messages that *look* like DNS errors but are SSRF guard rejections |
| Encryption | `packages/shared/encryption` | bad keys → 403/auth-style failures masquerading as upstream errors |
## Common Symptoms → First Files To Read
- **"403 from upstream":** check the per-integration credentials table in
Postgres (`PostHogIntegration`, `BlobStorageIntegration`, `WebhookConfig`,
etc.) and the encryption layer.
- **"Timeout":** check the SDK timeout default and the per-stream
flush/batch size in the handler. Worker async jobs default to long-running
but the upstream SDK does not.
- **"DNS lookup failed":** distinguish actual DNS from `validateWebhookURL`
rejection. The error message wrapping is misleading on purpose.
- **"Cannot write to canceled buffer" (CH):** ClickHouse stream wasn't
aborted when the downstream consumer threw. Look for an `AbortController`
threaded through the handler.
- **"Connection pool timeout" (Prisma):** worker `connection_limit` is set
in the connection string; jobs doing per-row `findFirst()` exhaust it.
Check whether the integration row could be cached in closure scope.
- **"memory limit exceeded" (CH):** look for unbounded JOINs without a
pre-filter CTE, especially in analytics integrations.
- **"Header overflow":** Node HTTP parser's default 80 KB ceiling. Either
raise `--max-http-header-size` for the worker, or replace the SDK's HTTP
client.
## Where to Look for Already-Shipped Fixes
Before recommending a patch, confirm it isn't already merged or in flight:
- `attachments` on the Linear issue (PRs and commits are auto-linked).
- `git log --oneline --since=<recent-window> -- <handler-path>`.
- Open PRs touching the file via `gh pr list --search "<filename>"`.
@@ -0,0 +1,171 @@
---
name: detect-prod-regressions
description: |
Proactively detect production regressions in Langfuse by comparing recent
Datadog errors, error logs, error spans, and API route latency signals against
baseline benchmarks or traces across prod-us, prod-eu, prod-hipaa, and
prod-jp. Use when asked to sweep production for new bugs, catch regressions
early, catch low-occurrence coding bugs or edge cases, compare recent changes
to Datadog measurements, or prepare measured production evidence for human
review before any optional Linear handoff.
---
# Detect Prod Regressions
Run this skill as an evidence-first production sweep. The deliverable is a set
of measured candidate bugs summarized for a human reviewer in a compact table,
plus a short summary of what was checked. Do not touch Linear automatically.
## Required Scope
Always review all production environments unless the user explicitly narrows the
scope:
- `prod-us`
- `prod-eu`
- `prod-hipaa`
- `prod-jp`
Query both Datadog sites when needed. Default to the EU site for `prod-eu` and
the US site for the other production envs, but verify by querying facets or
running a small count query rather than assuming where a tag lives.
## Measurement Rules
- Ground every bug claim in a measurable signal: counts, rates, p50/p95/p99
duration, trace samples, flamegraphs, monitor thresholds, or benchmark
comparisons.
- If a requested measurement is missing or unavailable, write exactly
"No measurements found" for that signal.
- Treat a bug as new only when a recent window shows a new or materially worse
error cluster or latency regression versus a baseline.
- Do not rely only on top-volume clusters. Also inspect new low-occurrence
errors that look like coding bugs or edge cases, such as invariant failures,
unexpected null/undefined values, validation surprises, unhandled promise
rejections, serialization failures, impossible enum states, or one-off 500s
on uncommon routes.
- For latency, error-rate, throughput, saturation, or performance findings,
focus on how the signal worsened over time: recent versus preceding window,
recent versus same window 7 days earlier, and post-change versus pre-change
when deployment markers are available.
- Prefer high-level aggregations before individual events; drill into example
logs, spans, traces, or flamegraphs only after a cluster is identified.
- Do not infer customer impact, root cause, or severity beyond what the
measurements support.
## Baseline Window
Use the user's stated window when provided. Otherwise:
1. Compare the most recent 24 hours with the preceding 24 hours.
2. Add a same-day or same-hour historical baseline, usually the matching window
7 days earlier, when Datadog data is available.
3. If release or deployment markers, service versions, git SHAs, or change
timestamps are visible, compare post-change versus pre-change windows.
Include the recent window and baseline window in any later handoff to
`linear-bug-triage` after the human explicitly approves sharing findings in
Linear.
## Datadog Sweep
Before querying Datadog, load the relevant Datadog MCP guidance for logs,
traces, metrics, and visualizations. Use the existing
[`datadog-query-recipes`](../datadog-query-recipes/SKILL.md) skill for query
syntax, production environment coverage, tenant/public API usage, and queue
consumer measurements. Use
[`debug-issue-with-datadog`](../debug-issue-with-datadog/SKILL.md) when a
candidate regression becomes an incident-style root-cause analysis.
For each environment, check:
1. **Datadog errors**
- Use Datadog Error Tracking to review error groups, new issues,
regressions, affected services, and occurrence timelines. Also check the
EU Datadog site equivalent when environment data is stored there.
- Aggregate error counts and rates by `env`, `service`, route/resource,
status code, `error.type`, and `error.message`.
- Compare recent counts/rates to the baseline.
- Include low-occurrence new error groups when the stack, message, route, or
affected code path suggests a coding bug or edge case, even if occurrence
counts are too small for top-N dashboards.
2. **Datadog error logs**
- Aggregate `status:error` logs by service, route/resource, source,
`error.message`, tenant/project identifiers when present, and environment.
- Open representative logs only for clusters with measurable increase.
3. **Datadog error spans**
- Aggregate error spans by `env`, `service`, `resource_name`, `http.route`,
`error.type`, and `error.message`.
- Fetch representative traces only after grouping identifies a candidate
regression.
4. **API route latencies**
- Focus on `service:web` HTTP spans and metrics such as
`trace.http_request.duration` when available.
- Compare p50, p95, and p99 by route/resource and environment.
- Rank candidates by worsening over time, not only by absolute latency:
recent versus baseline deltas, slope, and newly introduced tail latency.
- Use trace samples or flamegraphs for routes whose latency materially
regressed.
Keep Datadog links for every query, trace, dashboard, or flamegraph used as
evidence.
## Linear Handoff
Before doing anything in Linear, show the human reviewer a findings table in
chat and ask for permission to share the findings in Linear. The table should
include one row per candidate with:
- Candidate / cluster name.
- Environments.
- Service and route/resource.
- Recent window measurement.
- Baseline measurement.
- Delta / regression summary.
- Key evidence links.
- Recommended Linear action (`comment existing`, `create new`, or `none`).
Use a concrete markdown table shaped like this:
```markdown
| Candidate | Envs | Service / Route | Recent Window | Baseline | Delta | Evidence | Proposed Linear Action |
| --- | --- | --- | --- | --- | --- | --- | --- |
| Public scores latency regression | prod-us | `web` `GET /api/public/v2/scores/index` | p95 `7.44s`, p99 `12.77s`, count `71,448` | p95 `4.34s`, p99 `6.60s`, count `26,024` | p95 `+72%`, p99 `+94%`, volume `+174%` | [metrics](https://app.datadoghq.com/) [spans](https://app.datadoghq.com/) [trace](https://app.datadoghq.com/) | comment existing |
| ClickHouse socket hang up cluster | prod-us | `web-iso` `POST /` | errors `14,088` | errors `6,026` | `+134%` errors | [spans](https://app.datadoghq.com/) [trace](https://app.datadoghq.com/) | comment existing |
| Ingestion DLQ monitor noise | prod-eu | `worker-cpu` `langfuse.queue.ingestion.dlq_length` | max `150` | max `150` | flat | [metrics](https://app.datadoghq.eu/) | none |
```
If a requested signal is unavailable, write `No measurements found` in the
relevant cell instead of leaving it blank.
Only if the human explicitly approves, use
[`linear-bug-triage`](../linear-bug-triage/SKILL.md) for Linear search,
deduplication, evidence comments, Triage issue creation, labels, and ticket
formatting. Treat that skill as the source of truth for Linear behavior once
approval is granted.
Hand off:
- Recent window and baseline window.
- Measured deltas or `No measurements found` for unavailable signals.
- Affected environments, services, routes/resources, status codes, and top error
messages.
- Datadog links for every query, trace, dashboard, metric graph, or flamegraph
used as evidence.
## Final Response
Summarize:
- The windows compared and all prod environments checked.
- The findings table shown to the human.
- Whether the human approved sharing findings in Linear.
- New Linear issues created, with links, only if approval was granted.
- Existing Linear issues commented on, with links, only if approval was
granted.
- Candidate signals skipped with "No measurements found".
- A compact "no new bugs found" statement when no issue-worthy regressions were
measured.
@@ -0,0 +1,4 @@
interface:
display_name: "Detect Prod Regressions"
short_description: "Datadog regression sweep with Linear handoff"
default_prompt: "Use $detect-prod-regressions to compare recent production Datadog signals against baselines across all prod environments and hand measured bugs to $linear-bug-triage."
+116
View File
@@ -0,0 +1,116 @@
---
name: linear-bug-triage
description: |
Deduplicate measured bug or regression evidence against Linear, then either
add evidence comments to related existing issues or create concise Linear bug
issues in Triage. Use when Codex has confirmed evidence from Datadog,
benchmarks, traces, timings, flamegraphs, logs, or production measurements and
needs Linear search, comments, labels, Datadog links, or bug ticket creation
without fix suggestions, but only after a human has approved sharing the
findings in Linear.
---
# Linear Bug Triage
Use this skill after a bug or regression candidate has measured evidence. This
skill owns Linear search, deduplication, evidence comments, and ticket creation;
the calling skill owns deciding whether the signal is issue-worthy.
## Human Approval Gate
Before doing anything in Linear, first show the findings to the human in a
compact markdown table and ask for explicit permission to share them in Linear.
The table should include one row per candidate with:
- Candidate / cluster name.
- Environments.
- Service and route/resource.
- Recent window measurement.
- Baseline measurement.
- Delta / regression summary.
- Key Datadog evidence links.
- Proposed Linear action (`comment existing`, `create new`, or `none`).
If the human does not explicitly approve, stop after presenting the table. Do
not search Linear, do not comment on issues, and do not create issues.
If this skill was invoked by `detect-prod-regressions` and that calling skill
already showed the findings table and obtained explicit human approval for a
Linear handoff, skip this gate and proceed directly to deduplication.
## Required Evidence
For each candidate, gather:
- Recent window and baseline window as absolute time ranges with timezone.
- Measured signal: counts, rates, p50/p95/p99 latency, trace samples,
flamegraphs, monitor thresholds, or benchmark deltas.
- Affected environments, services, routes/resources, status codes, and top error
messages.
- Datadog links for logs, spans, traces, metrics, dashboards, or flamegraphs
used as evidence.
- The exact text `No measurements found` for requested measurements that are
unavailable.
Do not create or comment based on guesses, unsupported impact claims, or missing
measurements alone.
## Deduplication
After the human explicitly approves, before creating a new issue:
1. Search Linear for related open issues using exact error text, route/resource,
service, environment, monitor name, and Datadog link keywords.
2. Search recently closed or canceled issues if the error is recurring or the
wording is distinctive.
3. If a related issue exists, add a concise evidence comment instead of creating
a duplicate.
4. If no related issue exists, create one Linear issue in the `Triage` state for
each distinct bug cluster.
## Existing Issue Comments
For related existing issues, add only:
- Recent window and baseline window.
- Measured delta or `No measurements found` for unavailable signals.
- Affected environments, services, routes/resources, and top error messages.
- Datadog links.
Do not add fix suggestions, root-cause guesses, implementation notes, owner
assignments, or next steps.
## New Issue Format
Create new issues with:
- State/status `Triage`; pass the Linear state explicitly on creation and do not
rely on workspace defaults.
- Label `bug`.
- Additional existing labels that match the evidence, such as affected service,
environment, API, ingestion, latency, ClickHouse, Postgres, integrations, or
observability labels. Query labels first and use the repository/team's exact
label names.
- Concise title: `bug: <service or route> <measured symptom> in <envs>`.
- Concise body, evidence-only:
```markdown
Recent window: <absolute time range and timezone>
Baseline: <absolute time range and timezone>
Signal:
- <count/rate/latency delta with env/service/route>
- <"No measurements found" for missing requested measurements>
Evidence:
- Datadog logs: <url>
- Datadog spans/traces: <url>
- Datadog metrics, dashboard, or latency graph: <url>
Related Linear search:
- <brief search terms used and result>
```
Do not include fix suggestions, root-cause guesses, implementation notes, owner
assignments, or next steps unless the user explicitly asks outside the Linear
issue or comment.
@@ -0,0 +1,4 @@
interface:
display_name: "Linear Bug Triage"
short_description: "Deduplicate and file Linear bug evidence"
default_prompt: "Use $linear-bug-triage to deduplicate measured bug evidence and create or comment Linear triage issues."
@@ -31,9 +31,6 @@ pnpm workspace.
lock refresh / reinstall path before changing `package.json`.
- If the current parent range does not cover the requested version, upgrade
the direct parent dependency that pulls the package in.
- If a compatible transitive package still stays pinned after the normal
refresh path, you may suggest `pnpm dedupe` to the user as an optional
manual follow-up, but do not run it automatically and do not require it.
- Do not add the transitive package directly unless the user explicitly asks.
4. Ask before changing `minimumReleaseAgeExclude`.
@@ -53,6 +50,9 @@ pnpm workspace.
- Use the nearest package `AGENTS.md` plus the root verification matrix.
- Finish with `pnpm why -r <package>`.
- If companions moved too, run `pnpm why -r <companion-package>` for them as well.
- After fixing or upgrading a package, strongly suggest that the user run
`pnpm dedupe` as an optional cleanup step, but do not run it automatically
and do not require it.
## Quick Commands
@@ -64,6 +64,8 @@ pnpm workspace.
`npm view <parent>@<installedVersion> dependencies peerDependencies optionalDependencies --json`
- Final graph verification:
`pnpm why -r <package>`
- Optional lockfile cleanup for the user to run after the fix:
`pnpm dedupe`
- Bump in the root workspace:
`pnpm -w up <package>@<version>`
- Bump in one workspace:
+6 -3
View File
@@ -28,9 +28,12 @@ Use this skill for interactive dependency bumps in Langfuse.
- If the current parent range does not cover the requested transitive version,
upgrade that parent dependency instead of adding the target package directly
unless the user explicitly wants that.
- If a compatible transitive package still stays pinned after the normal
refresh path, you may suggest `pnpm dedupe` to the user as an optional manual
follow-up, but do not run it automatically and do not require it.
- Never manually edit `pnpm-lock.yaml`; regenerate lockfile changes with
`pnpm` commands only. If a lockfile-only refresh causes unrelated churn,
adjust the pnpm command and rerun instead of patching the lockfile by hand.
- After fixing or upgrading a package, strongly suggest that the user run
`pnpm dedupe` as an optional cleanup step, but do not run it automatically
and do not require it.
- Resolve the registry latest version, but do not silently upgrade to latest
unless the user asked for latest.
- Compare the target version with the latest version installable under the
@@ -94,13 +94,13 @@ cd packages/utils && bun add lodash
```bash
# pnpm
pnpm add jest --save-dev --filter=web --filter=@repo/ui
pnpm add vitest --save-dev --filter=web --filter=@repo/ui
# npm
npm install jest --save-dev --workspace=web --workspace=@repo/ui
npm install vitest --save-dev --workspace=web --workspace=@repo/ui
# yarn (v2+)
yarn workspaces foreach -R --from '{web,@repo/ui}' add jest --dev
yarn workspaces foreach -R --from '{web,@repo/ui}' add vitest --dev
```
### Internal Packages
+196
View File
@@ -0,0 +1,196 @@
#####################################################################
# .env (template) — OCI Object Storage / S3-compatible configuration
#
# IMPORTANT SECURITY NOTES (Oracle best practice)
# - Prefer OCI-native auth (Instance Principal / Workload Identity / Resource Principal)
# over static keys.
# - If you must use static keys, store them in a secure secret manager
# (e.g., Kubernetes Secret / OCI Vault) and inject at runtime.
# - Rotate/revoke any credentials that were previously shared or committed.
#####################################################################
#####################################################################
# 1) Storage/Auth category (CHOOSE ONE)
#
# The app can read/write/download to/from an OCI object store for:
# - Batch exports (exports/)
# - Media uploads (media/)
# - Event uploads (events/)
#
# Pick exactly ONE auth mechanism for OCI-native object storage by setting:
# LANGFUSE_USE_OCI_NATIVE_OBJECT_STORAGE=true
# LANGFUSE_OCI_AUTH_TYPE=<one of the values below>
#
# Supported values:
# workload_identity | instance_principal | resource_principal | oci_profile | session_token
#####################################################################
#####################################################################
# Category A — OCI Object Storage with INSTANCE PRINCIPAL (recommended on OCI Compute)
# Use when:
# - Running on OCI Compute with IAM set up (dynamic group + policies)
#
# Set:
# LANGFUSE_USE_OCI_NATIVE_OBJECT_STORAGE=true
# LANGFUSE_OCI_AUTH_TYPE=instance_principal
#
# NOTE: Do NOT set *_ACCESS_KEY_ID / *_SECRET_ACCESS_KEY in this category.
#####################################################################
#####################################################################
# Category B — OCI Object Storage with WORKLOAD IDENTITY (common on OKE)
# Use when:
# - Running on OKE with OCI Workload Identity configured
#
# Set:
# LANGFUSE_USE_OCI_NATIVE_OBJECT_STORAGE=true
# LANGFUSE_OCI_AUTH_TYPE=workload_identity
#
# Optional (only if your environment requires additional CA trust):
# NODE_EXTRA_CA_CERTS=/var/run/secrets/kubernetes.io/serviceaccount/ca.crt
#
# NOTE: Do NOT set *_ACCESS_KEY_ID / *_SECRET_ACCESS_KEY in this category.
#####################################################################
#####################################################################
# Category C — OCI Object Storage with RESOURCE PRINCIPAL (common for OCI services)
# Use when:
# - Running inside an OCI service/runtime that injects Resource Principal env vars
# (e.g., certain managed services / automation contexts)
#
# Set:
# LANGFUSE_USE_OCI_NATIVE_OBJECT_STORAGE=true
# LANGFUSE_OCI_AUTH_TYPE=resource_principal
#
# NOTE: Do NOT set *_ACCESS_KEY_ID / *_SECRET_ACCESS_KEY in this category.
#####################################################################
#####################################################################
# Category D — OCI Object Storage with OCI CONFIG PROFILE (developer local)
# Use when:
# - You have an OCI config file locally or mounted in the runtime
# - You want to use a named profile
#
# Set:
# LANGFUSE_USE_OCI_NATIVE_OBJECT_STORAGE=true
# LANGFUSE_OCI_AUTH_TYPE=oci_profile
# OCI_CONFIG_FILE=/path/to/oci/config
# OCI_CONFIG_PROFILE=DEFAULT
#
# NOTE: Avoid adding config files into images; mount/inject securely.
#####################################################################
#####################################################################
# Category E — OCI Object Storage with SESSION TOKEN (short-lived user auth)
# Use when:
# - You use OCI CLI session authentication (short-lived token flow)
# - USE oci session authenticate
# - Appropriate for interactive/dev use; less common for long-running services
#
# Set:
# LANGFUSE_USE_OCI_NATIVE_OBJECT_STORAGE=true
# LANGFUSE_OCI_AUTH_TYPE=session_token
# OCI_CONFIG_FILE=/path/to/oci/config
# OCI_CONFIG_PROFILE=DEFAULT
#####################################################################
#####################################################################
# Other possible setup — Non-OCI provider (AWS S3 / GCP / Azure / MinIO / etc.)
# Use when:
# - Your object storage is NOT OCI Object Storage
#
# Set:
# LANGFUSE_USE_OCI_NATIVE_OBJECT_STORAGE=false
#
# Then configure endpoints/regions/credentials for your provider.
#####################################################################
#####################################################################
# 2) Feature: S3 Batch Export
#
# Required (when enabled):
# - *_BUCKET, *_REGION, *_ENDPOINT, *_PREFIX
# Optional:
# - *_EXTERNAL_ENDPOINT
# - *_FORCE_PATH_STYLE=true (needed for many S3-compatible providers like MinIO)
#
# Credentials:
# - Set *_ACCESS_KEY_ID/_SECRET_ACCESS_KEY ONLY for static-key auth
# (non-OCI S3-compatible providers)
#####################################################################
LANGFUSE_S3_BATCH_EXPORT_ENABLED=true
LANGFUSE_S3_BATCH_EXPORT_BUCKET=langfuse-bucket
LANGFUSE_S3_BATCH_EXPORT_PREFIX=exports/
# OCI example region/endpoint:
LANGFUSE_S3_BATCH_EXPORT_REGION=us-chicago-1
LANGFUSE_S3_BATCH_EXPORT_ENDPOINT=https://objectstorage.us-chicago-1.oraclecloud.com
LANGFUSE_S3_BATCH_EXPORT_EXTERNAL_ENDPOINT=https://objectstorage.us-chicago-1.oraclecloud.com
# MinIO / S3-compat setting (safe to keep true for many S3-compatible endpoints)
LANGFUSE_S3_BATCH_EXPORT_FORCE_PATH_STYLE=true
# Static-key auth (non-OCI). Leave blank/commented for OCI-native auth types above.
LANGFUSE_S3_BATCH_EXPORT_ACCESS_KEY_ID=__REPLACE_ME__
LANGFUSE_S3_BATCH_EXPORT_SECRET_ACCESS_KEY=__REPLACE_ME__
#####################################################################
# 3) Feature: S3 Media Upload
#####################################################################
LANGFUSE_S3_MEDIA_UPLOAD_BUCKET=langfuse-bucket
LANGFUSE_S3_MEDIA_UPLOAD_PREFIX=media/
LANGFUSE_S3_MEDIA_UPLOAD_REGION=us-chicago-1
LANGFUSE_S3_MEDIA_UPLOAD_ENDPOINT=https://objectstorage.us-chicago-1.oraclecloud.com
LANGFUSE_S3_MEDIA_UPLOAD_FORCE_PATH_STYLE=true
# Static-key auth (non-OCI). Leave blank/commented for OCI-native auth types above.
LANGFUSE_S3_MEDIA_UPLOAD_ACCESS_KEY_ID=__REPLACE_ME__
LANGFUSE_S3_MEDIA_UPLOAD_SECRET_ACCESS_KEY=__REPLACE_ME__
#####################################################################
# 4) Feature: S3 Event Upload (optional)
#####################################################################
LANGFUSE_S3_EVENT_UPLOAD_BUCKET=langfuse-bucket
LANGFUSE_S3_EVENT_UPLOAD_PREFIX=events/
LANGFUSE_S3_EVENT_UPLOAD_REGION=us-chicago-1
LANGFUSE_S3_EVENT_UPLOAD_ENDPOINT=https://objectstorage.us-chicago-1.oraclecloud.com
LANGFUSE_S3_EVENT_UPLOAD_FORCE_PATH_STYLE=true
# Static-key auth (non-OCI). Leave blank/commented for OCI-native auth types above.
LANGFUSE_S3_EVENT_UPLOAD_ACCESS_KEY_ID=__REPLACE_ME__
LANGFUSE_S3_EVENT_UPLOAD_SECRET_ACCESS_KEY=__REPLACE_ME__
#####################################################################
# 5) OCI native auth configuration (used by oci_profile / session_token)
#####################################################################
# Only required when LANGFUSE_OCI_AUTH_TYPE is: oci_profile OR session_token
OCI_CONFIG_FILE=__REPLACE_ME__/config
OCI_CONFIG_PROFILE=DEFAULT
#####################################################################
# 6) Troubleshooting notes (comments only)
#
# - If you see TLS errors to the endpoint in Kubernetes/OKE, set NODE_EXTRA_CA_CERTS to the
# correct CA bundle path for your environment.
# - If using MinIO or certain S3-compatible providers and you get bucket addressing errors,
# set *_FORCE_PATH_STYLE=true.
# - If downloads work inside the cluster but not externally, configure
# LANGFUSE_S3_BATCH_EXPORT_EXTERNAL_ENDPOINT to a publicly reachable endpoint/DNS.
#####################################################################
+1
View File
@@ -81,6 +81,7 @@ REDIS_CLUSTER_NODES="127.0.0.1:6370,127.0.0.1:6371,127.0.0.1:6372,127.0.0.1:6373
LANGFUSE_INGESTION_QUEUE_SHARD_COUNT=8
LANGFUSE_INGESTION_SECONDARY_QUEUE_SHARD_COUNT=8
LANGFUSE_OTEL_INGESTION_QUEUE_SHARD_COUNT=4
LANGFUSE_OTEL_INGESTION_SECONDARY_QUEUE_SHARD_COUNT=4
LANGFUSE_EVAL_EXECUTION_QUEUE_SHARD_COUNT=4
LANGFUSE_EVAL_EXECUTION_SECONDARY_QUEUE_SHARD_COUNT=4
LANGFUSE_LLM_AS_JUDGE_EXECUTION_QUEUE_SHARD_COUNT=4
+2
View File
@@ -153,11 +153,13 @@ LANGFUSE_AI_FEATURES_PROJECT_ID=7a88fb47-b4e2-43b8-a06c-a5ce950dc53a
# Langfuse AI Bedrock credentials
AWS_ACCESS_KEY_ID="A123456789"
AWS_SECRET_ACCESS_KEY="SAK123456789"
LANGFUSE_LLM_CONNECTION_BEDROCK_API_KEY="1234567890abcdef"
LANGFUSE_AWS_BEDROCK_REGION="eu-west-1"
LANGFUSE_AWS_BEDROCK_MODEL="eu.anthropic.claude-3-haiku-20240307-v1:0"
# Events table migration
LANGFUSE_ENABLE_EVENTS_TABLE_OBSERVATIONS=true
LANGFUSE_ENABLE_EVENTS_TABLE_UI=true
LANGFUSE_ENABLE_EVENTS_TABLE_FLAGS=true
LANGFUSE_ENABLE_EVENTS_TABLE_V2_APIS=true
LANGFUSE_EXPERIMENT_INSERT_INTO_EVENTS_TABLE=true
+9
View File
@@ -69,6 +69,7 @@ OTEL_SERVICE_NAME="langfuse"
# AUTH_GOOGLE_ALLOWED_DOMAINS=langfuse.com,google.com # optional allowlist of workspace domains that can sign in via Google
# AUTH_GOOGLE_CLIENT_AUTH_METHOD=
# AUTH_GOOGLE_CHECKS=
# AUTH_GOOGLE_ID_TOKEN_SIGNED_RESPONSE_ALG=
# AUTH_GITHUB_CLIENT_ID=
# AUTH_GITHUB_CLIENT_SECRET=
# AUTH_GITHUB_ALLOW_ACCOUNT_LINKING=false
@@ -86,6 +87,7 @@ OTEL_SERVICE_NAME="langfuse"
# AUTH_GITLAB_ISSUER=
# AUTH_GITLAB_CLIENT_AUTH_METHOD=
# AUTH_GITLAB_CHECKS=
# AUTH_GITLAB_ID_TOKEN_SIGNED_RESPONSE_ALG=
# AUTH_GITLAB_URL=
# AUTH_AZURE_AD_CLIENT_ID=
# AUTH_AZURE_AD_CLIENT_SECRET=
@@ -93,30 +95,35 @@ OTEL_SERVICE_NAME="langfuse"
# AUTH_AZURE_AD_ALLOW_ACCOUNT_LINKING=false
# AUTH_AZURE_AD_CLIENT_AUTH_METHOD=
# AUTH_AZURE_AD_CHECKS=
# AUTH_AZURE_AD_ID_TOKEN_SIGNED_RESPONSE_ALG=
# AUTH_OKTA_CLIENT_ID=
# AUTH_OKTA_CLIENT_SECRET=
# AUTH_OKTA_ISSUER=
# AUTH_OKTA_ALLOW_ACCOUNT_LINKING=false
# AUTH_OKTA_CLIENT_AUTH_METHOD=
# AUTH_OKTA_CHECKS=
# AUTH_OKTA_ID_TOKEN_SIGNED_RESPONSE_ALG=
# AUTH_AUTH0_CLIENT_ID=
# AUTH_AUTH0_CLIENT_SECRET=
# AUTH_AUTH0_ISSUER=
# AUTH_AUTH0_ALLOW_ACCOUNT_LINKING=false
# AUTH_AUTH0_CLIENT_AUTH_METHOD=
# AUTH_AUTH0_CHECKS=
# AUTH_AUTH0_ID_TOKEN_SIGNED_RESPONSE_ALG=
# AUTH_COGNITO_CLIENT_ID=
# AUTH_COGNITO_CLIENT_SECRET=
# AUTH_COGNITO_ISSUER=
# AUTH_COGNITO_ALLOW_ACCOUNT_LINKING=false
# AUTH_COGNITO_CLIENT_AUTH_METHOD=
# AUTH_COGNITO_CHECKS=
# AUTH_COGNITO_ID_TOKEN_SIGNED_RESPONSE_ALG=
# AUTH_KEYCLOAK_CLIENT_ID=
# AUTH_KEYCLOAK_CLIENT_SECRET=
# AUTH_KEYCLOAK_ISSUER=
# AUTH_KEYCLOAK_ALLOW_ACCOUNT_LINKING=false
# AUTH_KEYCLOAK_CLIENT_AUTH_METHOD=
# AUTH_KEYCLOAK_CHECKS=
# AUTH_KEYCLOAK_ID_TOKEN_SIGNED_RESPONSE_ALG=
# AUTH_KEYCLOAK_NAME=
# AUTH_WORKOS_CLIENT_ID=
# AUTH_WORKOS_CLIENT_SECRET=
@@ -133,12 +140,14 @@ OTEL_SERVICE_NAME="langfuse"
# AUTH_CUSTOM_ID_TOKEN=false # optional, default is true
# AUTH_CUSTOM_CLIENT_AUTH_METHOD=
# AUTH_CUSTOM_CHECKS=
# AUTH_CUSTOM_ID_TOKEN_SIGNED_RESPONSE_ALG=
# AUTH_JUMPCLOUD_CLIENT_ID=
# AUTH_JUMPCLOUD_CLIENT_SECRET=
# AUTH_JUMPCLOUD_ISSUER=
# AUTH_JUMPCLOUD_ALLOW_ACCOUNT_LINKING=
# AUTH_JUMPCLOUD_CLIENT_AUTH_METHOD=
# AUTH_JUMPCLOUD_CHECKS=
# AUTH_JUMPCLOUD_ID_TOKEN_SIGNED_RESPONSE_ALG=
# AUTH_JUMPCLOUD_SCOPE=
# Transactional email, optional
+2 -2
View File
@@ -15,10 +15,10 @@ Fixes # (issue)
<!-- Please delete bullets that are not relevant. -->
- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] Chore (refactoring code, technical debt, workflow improvements)
- [ ] Chore (tooling, dependencies, CI, workflows, repo upkeep, or other maintenance work)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
- [ ] Refactor (does not change functionality, e.g. code style improvements, linting)
- [ ] Refactor (restructures existing code without changing behavior, e.g. simplify logic, split modules, reduce duplication)
- [ ] This change requires a documentation update
## Mandatory Tasks
+3 -1
View File
@@ -12,7 +12,7 @@ updates:
schedule:
interval: "daily"
cooldown:
default-days: 5
default-days: 7
versioning-strategy: "increase"
commit-message:
prefix: chore
@@ -54,6 +54,8 @@ updates:
directory: "/"
schedule:
interval: "weekly"
cooldown:
default-days: 8
commit-message:
prefix: ci
include: scope
+47 -20
View File
@@ -10,10 +10,21 @@ on:
type: string
description: Name of the service to be deployed, e.g. web-ingestion, web, or worker.
required: true
# Environment secrets don't auto-resolve in reusable workflows.
# See: https://github.com/actions/runner/issues/3206
secrets:
AWS_ACCESS_KEY_ID:
required: true
AWS_SECRET_ACCESS_KEY:
required: true
SENTRY_AUTH_TOKEN:
required: false
jobs:
ecs-deploy:
runs-on: blacksmith-4vcpu-ubuntu-2404
environment: ${{ inputs.environment }}
permissions:
contents: read
steps:
- name: Get app name
uses: winterjung/split@a211a1c46e35fcdc4097d59dd6282d4a9859651b # v2
@@ -22,52 +33,68 @@ jobs:
msg: ${{ inputs.service }}
separator: "-"
- name: Checkout code
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Authenticate with AWS
# GitHub/AWS recommend to use OIDC here: https://github.com/aws-actions/configure-aws-credentials?tab=readme-ov-file#oidc
# Probably more painful to configure, but would remove all long-lived credentials.
uses: aws-actions/configure-aws-credentials@7474bc4690e29a8392af63c5b98e7449536d5c3a # v4
uses: aws-actions/configure-aws-credentials@ec61189d14ec14c8efccab744f656cffd0e33f37 # v6.1.0
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: ${{ vars.AWS_REGION }}
- name: Login to AWS ECR
id: login-ecr
uses: aws-actions/amazon-ecr-login@183a1442edf41672e66566b7fc560e297a290896 # v2
uses: aws-actions/amazon-ecr-login@f2e9fc6c2b355c1890b65e6f6f0e2ac3e6e22f78 # v2.1.2
- name: Build, tag, and push Docker image
env:
REGISTRY: ${{ steps.login-ecr.outputs.registry }}
REPOSITORY: ${{ steps.split.outputs._0 }}
IMAGE_TAG: ${{ github.sha }}
STEPS_SPLIT_OUTPUTS__0: ${{ steps.split.outputs._0 }}
VARS_NEXT_PUBLIC_LANGFUSE_CLOUD_REGION: ${{ vars.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION }}
VARS_NEXT_LANGFUSE_TRACING_SAMPLE_RATE: ${{ vars.NEXT_LANGFUSE_TRACING_SAMPLE_RATE }}
VARS_NEXT_PUBLIC_SENTRY_ENVIRONMENT: ${{ vars.NEXT_PUBLIC_SENTRY_ENVIRONMENT }}
VARS_NEXT_PUBLIC_DEMO_ORG_ID: ${{ vars.NEXT_PUBLIC_DEMO_ORG_ID }}
VARS_NEXT_PUBLIC_DEMO_PROJECT_ID: ${{ vars.NEXT_PUBLIC_DEMO_PROJECT_ID }}
VARS_NEXT_PUBLIC_SENTRY_DSN: ${{ vars.NEXT_PUBLIC_SENTRY_DSN }}
VARS_NEXT_PUBLIC_POSTHOG_KEY: ${{ vars.NEXT_PUBLIC_POSTHOG_KEY }}
VARS_NEXT_PUBLIC_POSTHOG_HOST: ${{ vars.NEXT_PUBLIC_POSTHOG_HOST }}
VARS_NEXT_PUBLIC_PLAIN_APP_ID: ${{ vars.NEXT_PUBLIC_PLAIN_APP_ID }}
VARS_SENTRY_ORG: ${{ vars.SENTRY_ORG }}
VARS_SENTRY_PROJECT: ${{ vars.SENTRY_PROJECT }}
VARS_NEXT_PUBLIC_LANGFUSE_TRACING_SAMPLE_RATE: ${{ vars.NEXT_PUBLIC_LANGFUSE_TRACING_SAMPLE_RATE }}
SECRETS_SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
run: |
docker build \
-t $REGISTRY/$REPOSITORY:$IMAGE_TAG \
-f ./${{ steps.split.outputs._0 }}/Dockerfile \
--build-arg NEXT_PUBLIC_LANGFUSE_CLOUD_REGION=${{ vars.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION }} \
--build-arg NEXT_LANGFUSE_TRACING_SAMPLE_RATE=${{ vars.NEXT_LANGFUSE_TRACING_SAMPLE_RATE }} \
--build-arg NEXT_PUBLIC_SENTRY_ENVIRONMENT=${{ vars.NEXT_PUBLIC_SENTRY_ENVIRONMENT }} \
--build-arg NEXT_PUBLIC_DEMO_ORG_ID=${{ vars.NEXT_PUBLIC_DEMO_ORG_ID }} \
--build-arg NEXT_PUBLIC_DEMO_PROJECT_ID=${{ vars.NEXT_PUBLIC_DEMO_PROJECT_ID }} \
--build-arg NEXT_PUBLIC_SENTRY_DSN=${{ vars.NEXT_PUBLIC_SENTRY_DSN }} \
--build-arg NEXT_PUBLIC_BUILD_ID=${{ github.sha }} \
--build-arg NEXT_PUBLIC_POSTHOG_KEY=${{ vars.NEXT_PUBLIC_POSTHOG_KEY }} \
--build-arg NEXT_PUBLIC_POSTHOG_HOST=${{ vars.NEXT_PUBLIC_POSTHOG_HOST }} \
--build-arg NEXT_PUBLIC_PLAIN_APP_ID=${{ vars.NEXT_PUBLIC_PLAIN_APP_ID }} \
--build-arg SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }} \
--build-arg SENTRY_ORG=${{ vars.SENTRY_ORG }} \
--build-arg SENTRY_PROJECT=${{ vars.SENTRY_PROJECT }} \
--build-arg NEXT_PUBLIC_LANGFUSE_TRACING_SAMPLE_RATE=${{ vars.NEXT_PUBLIC_LANGFUSE_TRACING_SAMPLE_RATE }} \
-f ./${STEPS_SPLIT_OUTPUTS__0}/Dockerfile \
--build-arg NEXT_PUBLIC_LANGFUSE_CLOUD_REGION=${VARS_NEXT_PUBLIC_LANGFUSE_CLOUD_REGION} \
--build-arg NEXT_LANGFUSE_TRACING_SAMPLE_RATE=${VARS_NEXT_LANGFUSE_TRACING_SAMPLE_RATE} \
--build-arg NEXT_PUBLIC_SENTRY_ENVIRONMENT=${VARS_NEXT_PUBLIC_SENTRY_ENVIRONMENT} \
--build-arg NEXT_PUBLIC_DEMO_ORG_ID=${VARS_NEXT_PUBLIC_DEMO_ORG_ID} \
--build-arg NEXT_PUBLIC_DEMO_PROJECT_ID=${VARS_NEXT_PUBLIC_DEMO_PROJECT_ID} \
--build-arg NEXT_PUBLIC_SENTRY_DSN=${VARS_NEXT_PUBLIC_SENTRY_DSN} \
--build-arg NEXT_PUBLIC_BUILD_ID=${IMAGE_TAG} \
--build-arg NEXT_PUBLIC_POSTHOG_KEY=${VARS_NEXT_PUBLIC_POSTHOG_KEY} \
--build-arg NEXT_PUBLIC_POSTHOG_HOST=${VARS_NEXT_PUBLIC_POSTHOG_HOST} \
--build-arg NEXT_PUBLIC_PLAIN_APP_ID=${VARS_NEXT_PUBLIC_PLAIN_APP_ID} \
--build-arg SENTRY_AUTH_TOKEN=${SECRETS_SENTRY_AUTH_TOKEN} \
--build-arg SENTRY_ORG=${VARS_SENTRY_ORG} \
--build-arg SENTRY_PROJECT=${VARS_SENTRY_PROJECT} \
--build-arg NEXT_PUBLIC_LANGFUSE_TRACING_SAMPLE_RATE=${VARS_NEXT_PUBLIC_LANGFUSE_TRACING_SAMPLE_RATE} \
.
docker push $REGISTRY/$REPOSITORY:$IMAGE_TAG
- name: Render AWS ECS Task Definition
id: render-task-definition
uses: aws-actions/amazon-ecs-render-task-definition@77954e213ba1f9f9cb016b86a1d4f6fcdea0d57e # v1
uses: aws-actions/amazon-ecs-render-task-definition@77954e213ba1f9f9cb016b86a1d4f6fcdea0d57e # v1.8.4
with:
container-name: ${{ inputs.service }}
image: ${{ steps.login-ecr.outputs.registry }}/${{ steps.split.outputs._0 }}:${{ github.sha }}
task-definition-family: ${{ inputs.environment }}-${{ inputs.service }}
- name: Update AWS ECS Service
uses: aws-actions/amazon-ecs-deploy-task-definition@fc8fc60f3a60ffd500fcb13b209c59d221ac8c8c # v2
uses: aws-actions/amazon-ecs-deploy-task-definition@fc8fc60f3a60ffd500fcb13b209c59d221ac8c8c # v2.6.1
with:
task-definition: ${{ steps.render-task-definition.outputs.task-definition }}
service: ${{ inputs.environment }}-${{ inputs.service }}
+2
View File
@@ -9,6 +9,8 @@ on:
issue_comment:
types: [created]
permissions: {}
jobs:
retrigger_cla:
# Only run on PR comments (not issue comments) with the /check-cla command
@@ -0,0 +1,70 @@
name: Security review
on:
pull_request:
types:
- opened
- reopened
- synchronize
- ready_for_review
permissions:
contents: read
pull-requests: write
jobs:
security-review:
name: Security review
if: github.event.pull_request.draft == false && github.event.pull_request.head.repo.full_name == github.repository
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- name: Checkout
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
ref: ${{ github.event.pull_request.head.sha }}
fetch-depth: 2
persist-credentials: false
- name: Checkout Claude Code security review action
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
repository: anthropics/claude-code-security-review
ref: 0c6a49f1fa56a1d472575da86a94dbc1edb78eda
path: .github/actions/claude-code-security-review
fetch-depth: 1
persist-credentials: false
- name: Patch Claude API validation model
shell: bash
run: |
python <<'PY'
import os
from pathlib import Path
action_path = (
Path(os.environ["GITHUB_WORKSPACE"])
/ ".github"
/ "actions"
/ "claude-code-security-review"
/ "claudecode"
/ "claude_api_client.py"
)
if not action_path.exists():
raise SystemExit(f"Expected Claude API client not found at {action_path}")
source = action_path.read_text()
old = 'model="claude-3-5-haiku-20241022",'
new = "model=self.model,"
if old not in source:
raise SystemExit(f"Expected validation model not found in {action_path}")
action_path.write_text(source.replace(old, new))
PY
- name: Run Claude Code security review
uses: ./.github/actions/claude-code-security-review
with:
claude-api-key: ${{ secrets.CLAUDE_API_KEY }}
claude-model: claude-opus-4-1-20250805
comment-pr: true
run-every-commit: true
@@ -1,14 +1,15 @@
name: Claude Review on Maintainer PRs
on:
pull_request_target:
pull_request:
types:
- opened
- ready_for_review
jobs:
comment:
if: github.event.pull_request.draft == false
# Only run on PRs that are not drafts and are from the same repository (i.e., not from forks)
if: github.event.pull_request.draft == false && github.event.pull_request.head.repo.full_name == github.repository
runs-on: ubuntu-latest
permissions:
issues: write
@@ -16,7 +17,7 @@ jobs:
steps:
- name: Check author permission and existing review request
id: check
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
script: |
const owner = context.repo.owner;
@@ -57,7 +58,7 @@ jobs:
- name: Add Claude review comment
if: steps.check.outputs.should_comment == 'true'
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
script: |
await github.rest.issues.createComment({
+5 -3
View File
@@ -55,11 +55,13 @@ jobs:
# your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages
steps:
- name: Checkout repository
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@5c8a8a642e79153f5d047b10ec1cba1d1cc65699 # v3
uses: github/codeql-action/init@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1
with:
languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }}
@@ -87,6 +89,6 @@ jobs:
exit 1
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@5c8a8a642e79153f5d047b10ec1cba1d1cc65699 # v3
uses: github/codeql-action/analyze@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1
with:
category: "/language:${{matrix.language}}"
+4 -2
View File
@@ -22,6 +22,8 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Codespell
uses: codespell-project/actions-codespell@406322ec52dd7b488e48c1c4b82e2a8b3a1bf630 # v2
uses: codespell-project/actions-codespell@8f01853be192eb0f849a5c7d721450e7a467c579 # v2.2
@@ -6,6 +6,8 @@ on:
- main
workflow_dispatch:
permissions: {}
jobs:
rebase-dependabot:
runs-on: ubuntu-latest
+14 -5
View File
@@ -27,6 +27,8 @@ on:
- prod-jp
required: true
permissions: {}
concurrency:
# Support concurrent `push` and `workflow_dispatch`` actions
group: deploy-${{ github.event_name }}-${{ github.ref }}
@@ -41,7 +43,7 @@ jobs:
steps:
- name: Get affected services
id: affected-services
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
script: |
if (context.eventName === "workflow_dispatch") {
@@ -56,7 +58,7 @@ jobs:
return "[]"
result-encoding: string
- name: Print services to build
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
env:
services: ${{ steps.affected-services.outputs.result }}
with:
@@ -71,7 +73,7 @@ jobs:
steps:
- name: Get affected environments
id: affected-environments
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
script: |
if (context.eventName === "workflow_dispatch") {
@@ -88,7 +90,7 @@ jobs:
return "[]"
result-encoding: string
- name: Print environments to build
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
env:
environments: ${{ steps.affected-environments.outputs.result }}
with:
@@ -99,7 +101,14 @@ jobs:
ecs-deploy:
uses: ./.github/workflows/_deploy_ecs_service.yml
needs: [affected-services, affected-environments]
secrets: inherit
permissions:
contents: read
# Environment secrets must be passed explicitly to reusable workflows.
# See: https://github.com/actions/runner/issues/3206
secrets:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
strategy:
matrix:
service: ${{ fromJson(needs.affected-services.outputs.services) }}
+12 -4
View File
@@ -9,15 +9,21 @@ on:
branches:
- "main"
permissions:
contents: read
checks: write # Needed to create a check run for the license compliance check results
jobs:
license_check:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Setup node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version: 18
@@ -32,7 +38,7 @@ jobs:
- name: Check license-checker CSV file without headers
id: license_check_report
uses: pilosus/action-pip-license-checker@cc7a461bfa27b44ad187b8578c881ef5138c13fd # v2
uses: pilosus/action-pip-license-checker@e909b0226ff49d3235c99c4585bc617f49fff16a # v3.1.0
with:
external: "npm-license-checker.csv"
external-format: "csv"
@@ -44,6 +50,8 @@ jobs:
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Echo error
if: failure()
run: echo "::error::${{ steps.license_check_report.outputs.report }}"
run: echo "::error::${STEPS_LICENSE_CHECK_REPORT_OUTPUTS_REPORT}"
env:
STEPS_LICENSE_CHECK_REPORT_OUTPUTS_REPORT: ${{ steps.license_check_report.outputs.report }}
- name: Delete license-checker CSV file
run: rm npm-license-checker.csv
+380 -110
View File
@@ -12,10 +12,28 @@ on:
branches:
- "**"
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
env:
# Keep the test job node-version matrices in sync. GitHub does not allow the
# env context in jobs.<job_id>.name, so matrix jobs use matrix.node-version
# for their display names.
NODE_VERSION: 24
# Disable CI cache restores for fork PRs.
#
# Fork PRs can only read base-branch caches, and CI cache paths must not
# contain secrets. This is conservative defense-in-depth, not required for
# release cache integrity: PR-created caches are scoped to the PR merge ref,
# main does not restore them, and release image builds intentionally do not
# restore CI caches.
CI_CACHE_ALLOWED: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }}
jobs:
pre-job:
runs-on: blacksmith-4vcpu-ubuntu-2404
@@ -24,19 +42,44 @@ jobs:
llm_connections_changed: ${{ steps.filter.outputs.llm_connections }}
timeout-minutes: 15
permissions:
contents: read
pull-requests: read
steps:
# Replaces fkirc/skip-duplicate-actions — skips runs whose git tree
# was already tested in a prior successful run of this workflow.
- id: skip_check
uses: fkirc/skip-duplicate-actions@f75f66ce1886f00957d99748a42c724f4330bdcf # v5
with:
do_not_skip: '["workflow_dispatch"]'
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
env:
EVENT_NAME: ${{ github.event_name }}
GH_TOKEN: ${{ github.token }}
REPOSITORY: ${{ github.repository }}
COMMIT_SHA: ${{ github.sha }}
RUN_ID: ${{ github.run_id }}
run: |
if [[ "$EVENT_NAME" == "workflow_dispatch" ]]; then
echo "should_skip=false" >> "$GITHUB_OUTPUT"
exit 0
fi
CURRENT_TREE=$(gh api "repos/$REPOSITORY/git/commits/$COMMIT_SHA" --jq '.tree.sha')
MATCH=$(gh api "repos/$REPOSITORY/actions/workflows/pipeline.yml/runs?status=success&per_page=20" \
| jq -r --argjson run_id "$RUN_ID" --arg current_tree "$CURRENT_TREE" \
'[.workflow_runs[] | select(.id != $run_id and .head_commit.tree_id == $current_tree)] | first | .head_sha // empty')
if [[ -n "$MATCH" ]]; then
echo "::notice::Tree $CURRENT_TREE already tested in a prior successful run (commit $MATCH) — skipping"
echo "should_skip=true" >> "$GITHUB_OUTPUT"
else
echo "should_skip=false" >> "$GITHUB_OUTPUT"
fi
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
# Recommended for paths-filter action
# may save additional git fetch roundtrip if
# merge-base is found within latest N commits
fetch-depth: 20
- uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3
persist-credentials: false
- uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
id: filter
with:
filters: |
@@ -50,17 +93,28 @@ jobs:
- pre-job
if: needs.pre-job.outputs.should_skip != 'true'
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- uses: pnpm/action-setup@a3252b78c470c02df07e9d59298aecedc3ccdd6d # v3
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
with:
version: 10.33.0
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
- name: Use Node.js ${{ env.NODE_VERSION }} without cache
if: env.CI_CACHE_ALLOWED != 'true'
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version: 24
node-version: ${{ env.NODE_VERSION }}
package-manager-cache: false
- name: Use Node.js ${{ env.NODE_VERSION }} with pnpm cache
if: env.CI_CACHE_ALLOWED == 'true'
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 # zizmor: ignore[cache-poisoning] lint job dependency cache only; no released artifacts are built or published from this cached state
with:
node-version: ${{ env.NODE_VERSION }}
cache: "pnpm"
cache-dependency-path: "pnpm-lock.yaml"
- name: Setup Turbo cache
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
if: env.CI_CACHE_ALLOWED == 'true'
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 # zizmor: ignore[cache-poisoning] lint cache only; publish jobs rebuild artifacts and do not restore this cache
with:
path: .turbo
key: ${{ runner.os }}-turbo-lint-${{ github.sha }}
@@ -75,6 +129,8 @@ jobs:
cp .env.dev.example .env
- name: lint web
run: pnpm run lint
- name: typecheck
run: pnpm run typecheck
prettier-check:
runs-on: blacksmith-4vcpu-ubuntu-2404
@@ -82,15 +138,24 @@ jobs:
- pre-job
if: needs.pre-job.outputs.should_skip != 'true'
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
- uses: pnpm/action-setup@a3252b78c470c02df07e9d59298aecedc3ccdd6d # v3
persist-credentials: false
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
with:
version: 10.33.0
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
- name: Use Node.js ${{ env.NODE_VERSION }} without cache
if: env.CI_CACHE_ALLOWED != 'true'
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version: 24
node-version: ${{ env.NODE_VERSION }}
package-manager-cache: false
- name: Use Node.js ${{ env.NODE_VERSION }} with pnpm cache
if: env.CI_CACHE_ALLOWED == 'true'
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 # zizmor: ignore[cache-poisoning] prettier check dependency cache only; no released artifacts are built or published from this cached state
with:
node-version: ${{ env.NODE_VERSION }}
cache: "pnpm"
cache-dependency-path: "pnpm-lock.yaml"
- name: install dependencies
@@ -100,25 +165,56 @@ jobs:
run: |
cp .env.dev.example .env
- name: Check formatting on changed files
env:
EVENT_NAME: ${{ github.event_name }}
PR_BASE_SHA: ${{ github.event.pull_request.base.sha }}
run: |
if [ "${{ github.event_name }}" = "pull_request" ]; then
BASE_SHA=${{ github.event.pull_request.base.sha }}
if [ "$EVENT_NAME" = "pull_request" ]; then
BASE_SHA="$PR_BASE_SHA"
else
BASE_SHA=$(git merge-base origin/main HEAD)
fi
echo "Checking files changed from $BASE_SHA to HEAD"
# Get changed files
CHANGED_FILES=$(git diff --name-only $BASE_SHA HEAD -- '*.js' '*.jsx' '*.ts' '*.tsx' '*.css' | tr '\n' ' ')
if [ -n "$CHANGED_FILES" ] && [ "$CHANGED_FILES" != " " ]; then
echo "Files to check: $CHANGED_FILES"
pnpm prettier --check --experimental-cli $CHANGED_FILES
else
if git diff --quiet --exit-code --diff-filter=d "$BASE_SHA" HEAD -- '*.js' '*.jsx' '*.ts' '*.tsx' '*.css'; then
echo "No JS/TS/CSS files changed - skipping prettier check"
else
git diff --name-only -z --diff-filter=d "$BASE_SHA" HEAD -- '*.js' '*.jsx' '*.ts' '*.tsx' '*.css' \
| xargs -0 --no-run-if-empty pnpm prettier --check --experimental-cli --
fi
tests-eslint-plugin:
runs-on: blacksmith-4vcpu-ubuntu-2404
needs:
- pre-job
if: needs.pre-job.outputs.should_skip != 'true'
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
with:
version: 10.33.0
- name: Use Node.js ${{ env.NODE_VERSION }} without cache
if: env.CI_CACHE_ALLOWED != 'true'
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version: ${{ env.NODE_VERSION }}
package-manager-cache: false
- name: Use Node.js ${{ env.NODE_VERSION }} with pnpm cache
if: env.CI_CACHE_ALLOWED == 'true'
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 # zizmor: ignore[cache-poisoning] eslint-plugin test dependency cache only; no published artifacts are built from this cached state
with:
node-version: ${{ env.NODE_VERSION }}
cache: "pnpm"
cache-dependency-path: "pnpm-lock.yaml"
- name: install dependencies
run: |
pnpm install
- name: run eslint-plugin tests
run: pnpm --filter @repo/eslint-plugin run test
test-docker-build:
timeout-minutes: 20
runs-on: blacksmith-4vcpu-ubuntu-2404
@@ -129,10 +225,12 @@ jobs:
DOCKER_COMPOSE_FILE: docker-compose.build.yml
steps:
- name: Checkout
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Login to Docker Hub
if: github.repository == 'langfuse/langfuse' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository)
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
with:
username: ${{ secrets.DOCKERHUB_USERNAME_READ }}
password: ${{ secrets.DOCKERHUB_TOKEN_READ }}
@@ -178,7 +276,7 @@ jobs:
done
- name: Upload docker diagnostics
if: failure()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: test-docker-build-diagnostics-${{ github.run_id }}-${{ github.run_attempt }}
path: /tmp/docker-diagnostics
@@ -189,31 +287,42 @@ jobs:
needs:
- pre-job
if: needs.pre-job.outputs.should_skip != 'true'
name: tests-web (node${{ matrix.node-version }}, pg${{ matrix.postgres-version }}, mode${{ matrix.deploy-mode }}, shard${{ matrix.shard }}/3)
name: tests-web (node${{ matrix.node-version }}, pg${{ matrix.postgres-version }}, mode${{ matrix.deploy-mode }})
strategy:
matrix:
node-version: [24]
postgres-version: [12, 15]
deploy-mode: ["", "-azure", "-redis-cluster"]
shard: [1, 2, 3]
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Install golang-migrate for Clickhouse migrations
run: |
curl -L https://github.com/golang-migrate/migrate/releases/download/v4.19.1/migrate.linux-amd64.tar.gz | tar xvz
curl --fail --location --retry 5 --retry-delay 2 --retry-all-errors \
--output migrate.linux-amd64.tar.gz \
https://github.com/golang-migrate/migrate/releases/download/v4.19.1/migrate.linux-amd64.tar.gz
tar xzf migrate.linux-amd64.tar.gz
sudo mv migrate /usr/bin/migrate
which migrate
- uses: pnpm/action-setup@a3252b78c470c02df07e9d59298aecedc3ccdd6d # v3
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
with:
version: 10.33.0
- name: Login to Docker Hub
if: github.repository == 'langfuse/langfuse' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository)
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
with:
username: ${{ secrets.DOCKERHUB_USERNAME_READ }}
password: ${{ secrets.DOCKERHUB_TOKEN_READ }}
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
- name: Use Node.js ${{ matrix.node-version }} without cache
if: env.CI_CACHE_ALLOWED != 'true'
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version: ${{ matrix.node-version }}
package-manager-cache: false
- name: Use Node.js ${{ matrix.node-version }} with pnpm cache
if: env.CI_CACHE_ALLOWED == 'true'
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 # zizmor: ignore[cache-poisoning] test job dependency cache only; no released artifacts are built or published from this cached state
with:
node-version: ${{ matrix.node-version }}
cache: "pnpm"
@@ -228,18 +337,22 @@ jobs:
echo "LANGFUSE_INGESTION_QUEUE_DELAY_MS=1" >> .env
echo "LANGFUSE_CACHE_PROMPT_ENABLED=false" >> .env
echo "LANGFUSE_INGESTION_CLICKHOUSE_WRITE_INTERVAL_MS=1" >> .env
echo "LANGFUSE_TRACE_DELETE_DELAY_MS=1" >> .env
echo "LANGFUSE_TRACE_DELETE_CONCURRENCY=100" >> .env
echo "ADMIN_API_KEY=admin-api-key" >> .env
echo "LANGFUSE_EE_LICENSE_KEY=langfuse_ee_test" >> .env
echo "LANGFUSE_SKIP_EVALUATOR_MODEL_CALL_VALIDATION=true" >> .env
- name: Setup Turbo cache
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
if: env.CI_CACHE_ALLOWED == 'true'
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 # zizmor: ignore[cache-poisoning] test job cache only; publish jobs rebuild artifacts and do not restore this cache
with:
path: .turbo
key: ${{ runner.os }}-turbo-${{ github.sha }}
restore-keys: |
${{ runner.os }}-turbo-
- name: Cache Next.js builds
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
if: env.CI_CACHE_ALLOWED == 'true'
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 # zizmor: ignore[cache-poisoning] test-only Next.js cache; not consumed by artifact publishing or release jobs
with:
path: |
~/.npm
@@ -276,6 +389,9 @@ jobs:
run: pnpm run build
env:
NODE_OPTIONS: --max_old_space_size=8192
# TypeScript is checked explicitly in the lint job; skip duplicate
# Next.js type checks in test builds to reduce CI runtime.
NEXT_IGNORE_BUILD_ERRORS: "true"
- name: Start Langfuse
run: (pnpm run start&)
env:
@@ -291,11 +407,11 @@ jobs:
LANGFUSE_INIT_USER_PASSWORD: "password"
- name: run tests
working-directory: web
run: npx dotenv -e ../.env.test -e ../.env -- npx jest --verbose --runInBand --detectOpenHandles --selectProjects server --shard=${{ matrix.shard }}/3
run: npx dotenv -e ../.env.test -e ../.env -- vitest run --project server
- name: run test-client
if: matrix.deploy-mode == '' && matrix.shard == 1
if: matrix.deploy-mode == ''
working-directory: web
run: npx dotenv -e ../.env.test -e ../.env -- npx jest --verbose --runInBand --detectOpenHandles --selectProjects client
run: npx dotenv -e ../.env.test -e ../.env -- vitest run --project client
tests-worker:
timeout-minutes: 20
@@ -310,18 +426,27 @@ jobs:
postgres-version: [12, 15]
deploy-mode: ["", "-azure", "-redis-cluster"]
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- uses: pnpm/action-setup@a3252b78c470c02df07e9d59298aecedc3ccdd6d # v3
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
with:
version: 10.33.0
- name: Login to Docker Hub
if: github.repository == 'langfuse/langfuse' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository)
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
with:
username: ${{ secrets.DOCKERHUB_USERNAME_READ }}
password: ${{ secrets.DOCKERHUB_TOKEN_READ }}
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
- name: Use Node.js ${{ matrix.node-version }} without cache
if: env.CI_CACHE_ALLOWED != 'true'
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version: ${{ matrix.node-version }}
package-manager-cache: false
- name: Use Node.js ${{ matrix.node-version }} with pnpm cache
if: env.CI_CACHE_ALLOWED == 'true'
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 # zizmor: ignore[cache-poisoning] worker test dependency cache only; no release artifacts are produced from this cache
with:
node-version: ${{ matrix.node-version }}
cache: "pnpm"
@@ -331,7 +456,10 @@ jobs:
pnpm install
- name: Install golang-migrate for Clickhouse migrations
run: |
curl -L https://github.com/golang-migrate/migrate/releases/download/v4.19.1/migrate.linux-amd64.tar.gz | tar xvz
curl --fail --location --retry 5 --retry-delay 2 --retry-all-errors \
--output migrate.linux-amd64.tar.gz \
https://github.com/golang-migrate/migrate/releases/download/v4.19.1/migrate.linux-amd64.tar.gz
tar xzf migrate.linux-amd64.tar.gz
sudo mv migrate /usr/bin/migrate
which migrate
- name: Load default env
@@ -381,28 +509,34 @@ jobs:
if: startsWith(github.ref, 'refs/tags/') || (needs.pre-job.outputs.should_skip != 'true' && (needs.pre-job.outputs.llm_connections_changed == 'true' || github.event_name == 'workflow_dispatch'))
name: test-worker-llm-connections (node24, pg15)
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- uses: pnpm/action-setup@a3252b78c470c02df07e9d59298aecedc3ccdd6d # v3
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
with:
version: 10.33.0
- name: Login to Docker Hub
if: github.repository == 'langfuse/langfuse' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository)
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
with:
username: ${{ secrets.DOCKERHUB_USERNAME_READ }}
password: ${{ secrets.DOCKERHUB_TOKEN_READ }}
- name: Use Node.js 24
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
# Keep this secret-bearing job cache-cold to avoid exposing real provider
# credentials to potentially poisoned shared package-manager state.
- name: Use Node.js ${{ env.NODE_VERSION }}
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version: 24
cache: "pnpm"
cache-dependency-path: "pnpm-lock.yaml"
node-version: ${{ env.NODE_VERSION }}
package-manager-cache: false
- name: install dependencies
run: |
pnpm install
- name: Install golang-migrate for Clickhouse migrations
run: |
curl -L https://github.com/golang-migrate/migrate/releases/download/v4.19.1/migrate.linux-amd64.tar.gz | tar xvz
curl --fail --location --retry 5 --retry-delay 2 --retry-all-errors \
--output migrate.linux-amd64.tar.gz \
https://github.com/golang-migrate/migrate/releases/download/v4.19.1/migrate.linux-amd64.tar.gz
tar xzf migrate.linux-amd64.tar.gz
sudo mv migrate /usr/bin/migrate
which migrate
- name: Load default env
@@ -442,6 +576,7 @@ jobs:
LANGFUSE_LLM_CONNECTION_BEDROCK_ACCESS_KEY_ID: ${{ secrets.LANGFUSE_LLM_CONNECTION_BEDROCK_ACCESS_KEY_ID }}
LANGFUSE_LLM_CONNECTION_BEDROCK_SECRET_ACCESS_KEY: ${{ secrets.LANGFUSE_LLM_CONNECTION_BEDROCK_SECRET_ACCESS_KEY }}
LANGFUSE_LLM_CONNECTION_BEDROCK_REGION: ${{ secrets.LANGFUSE_LLM_CONNECTION_BEDROCK_REGION }}
LANGFUSE_LLM_CONNECTION_BEDROCK_API_KEY: ${{ secrets.LANGFUSE_LLM_CONNECTION_BEDROCK_API_KEY }}
LANGFUSE_LLM_CONNECTION_VERTEXAI_KEY: ${{ secrets.LANGFUSE_LLM_CONNECTION_VERTEXAI_KEY }}
LANGFUSE_LLM_CONNECTION_GOOGLEAISTUDIO_KEY: ${{ secrets.LANGFUSE_LLM_CONNECTION_GOOGLEAISTUDIO_KEY }}
@@ -451,23 +586,34 @@ jobs:
- pre-job
if: needs.pre-job.outputs.should_skip != 'true'
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- uses: pnpm/action-setup@a3252b78c470c02df07e9d59298aecedc3ccdd6d # v3
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
with:
version: 10.33.0
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
- name: Use Node.js ${{ env.NODE_VERSION }} without cache
if: env.CI_CACHE_ALLOWED != 'true'
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version: 24
node-version: ${{ env.NODE_VERSION }}
package-manager-cache: false
- name: Use Node.js ${{ env.NODE_VERSION }} with pnpm cache
if: env.CI_CACHE_ALLOWED == 'true'
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 # zizmor: ignore[cache-poisoning] e2e dependency cache only; release images are rebuilt later without restoring this cache
with:
node-version: ${{ env.NODE_VERSION }}
cache: "pnpm"
cache-dependency-path: "pnpm-lock.yaml"
- name: Login to Docker Hub
if: github.repository == 'langfuse/langfuse' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository)
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
with:
username: ${{ secrets.DOCKERHUB_USERNAME_READ }}
password: ${{ secrets.DOCKERHUB_TOKEN_READ }}
- name: Setup Turbo cache
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
if: env.CI_CACHE_ALLOWED == 'true'
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 # zizmor: ignore[cache-poisoning] e2e cache only; no published artifact path restores this cache
with:
path: .turbo
key: ${{ runner.os }}-turbo-e2e-${{ github.sha }}
@@ -475,7 +621,8 @@ jobs:
${{ runner.os }}-turbo-e2e-
${{ runner.os }}-turbo-
- name: Cache Next.js builds
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
if: env.CI_CACHE_ALLOWED == 'true'
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 # zizmor: ignore[cache-poisoning] e2e-only Next.js cache; not part of any artifact build or publishing flow
with:
path: |
~/.npm
@@ -493,7 +640,10 @@ jobs:
cp .env.dev.example web/.env
- name: Install golang-migrate for Clickhouse migrations
run: |
curl -L https://github.com/golang-migrate/migrate/releases/download/v4.19.1/migrate.linux-amd64.tar.gz | tar xvz
curl --fail --location --retry 5 --retry-delay 2 --retry-all-errors \
--output migrate.linux-amd64.tar.gz \
https://github.com/golang-migrate/migrate/releases/download/v4.19.1/migrate.linux-amd64.tar.gz
tar xzf migrate.linux-amd64.tar.gz
sudo mv migrate /usr/bin/migrate
which migrate
- name: Run + migrate
@@ -517,8 +667,11 @@ jobs:
run: pnpm run build
env:
NODE_OPTIONS: --max_old_space_size=8192
# TypeScript is checked explicitly in the lint job; skip duplicate
# Next.js type checks in test builds to reduce CI runtime.
NEXT_IGNORE_BUILD_ERRORS: "true"
- name: Install playwright
run: pnpm --filter=web exec playwright install --with-deps
run: pnpm --filter=web exec playwright install --with-deps --only-shell chromium
- name: Run e2e tests
run: pnpm --filter=web run test:e2e
@@ -528,19 +681,29 @@ jobs:
- pre-job
if: needs.pre-job.outputs.should_skip != 'true'
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Login to Docker Hub
if: github.repository == 'langfuse/langfuse' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository)
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
with:
username: ${{ secrets.DOCKERHUB_USERNAME_READ }}
password: ${{ secrets.DOCKERHUB_TOKEN_READ }}
- uses: pnpm/action-setup@a3252b78c470c02df07e9d59298aecedc3ccdd6d # v3
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
with:
version: 10.33.0
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
- name: Use Node.js ${{ env.NODE_VERSION }} without cache
if: env.CI_CACHE_ALLOWED != 'true'
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version: 24
node-version: ${{ env.NODE_VERSION }}
package-manager-cache: false
- name: Use Node.js ${{ env.NODE_VERSION }} with pnpm cache
if: env.CI_CACHE_ALLOWED == 'true'
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 # zizmor: ignore[cache-poisoning] server e2e dependency cache only; release/publish steps rebuild separately
with:
node-version: ${{ env.NODE_VERSION }}
cache: "pnpm"
cache-dependency-path: "pnpm-lock.yaml"
- name: install dependencies
@@ -548,7 +711,10 @@ jobs:
pnpm install
- name: Install golang-migrate for Clickhouse migrations
run: |
curl -L https://github.com/golang-migrate/migrate/releases/download/v4.19.1/migrate.linux-amd64.tar.gz | tar xvz
curl --fail --location --retry 5 --retry-delay 2 --retry-all-errors \
--output migrate.linux-amd64.tar.gz \
https://github.com/golang-migrate/migrate/releases/download/v4.19.1/migrate.linux-amd64.tar.gz
tar xzf migrate.linux-amd64.tar.gz
sudo mv migrate /usr/bin/migrate
which migrate
- name: Load default env
@@ -575,6 +741,9 @@ jobs:
run: pnpm run build
env:
NODE_OPTIONS: --max_old_space_size=8192
# TypeScript is checked explicitly in the lint job; skip duplicate
# Next.js type checks in test builds to reduce CI runtime.
NEXT_IGNORE_BUILD_ERRORS: "true"
- name: Run server
run: (pnpm run start&)
- name: Check worker health
@@ -589,9 +758,11 @@ jobs:
all-ci-passed:
# This allows us to have a branch protection rule for tests and deploys with matrix
runs-on: blacksmith-4vcpu-ubuntu-2404
needs: [
needs:
[
lint,
prettier-check,
tests-eslint-plugin,
tests-web,
tests-worker,
test-worker-llm-connections,
@@ -620,16 +791,58 @@ jobs:
run: exit 1
working-directory: .
- name: Notify Slack
uses: ravsamhq/notify-slack-action@be814b201e233b2dc673608aa46e5447c8ab13f2 # v2
if: always() && github.event_name == 'push' && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/'))
if: failure() && github.event_name == 'push' && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/'))
uses: slackapi/slack-github-action@af78098f536edbc4de71162a307590698245be95 # v3.0.1
with:
status: ${{ job.status }}
notify_when: "failure"
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
webhook: ${{ secrets.SLACK_WEBHOOK_URL }}
webhook-type: incoming-webhook
payload: |
{
"text": "❌ CI failed on ${{ github.ref_name }}",
"blocks": [
{
"type": "header",
"text": {
"type": "plain_text",
"text": "❌ CI Failed",
"emoji": true
}
},
{
"type": "section",
"fields": [
{
"type": "mrkdwn",
"text": "*Branch/Tag:*\n`${{ github.ref_name }}`"
},
{
"type": "mrkdwn",
"text": "*Triggered by:*\n${{ github.actor }}"
}
]
},
{
"type": "actions",
"elements": [
{
"type": "button",
"text": {
"type": "plain_text",
"text": "View Workflow Logs",
"emoji": true
},
"url": "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}",
"style": "danger"
}
]
}
]
}
- name: Output job results
run: |
echo "Job results: ${{ steps.set-success-output.outputs.success }}"
echo "Job results: ${STEPS_SET_SUCCESS_OUTPUT_OUTPUTS_SUCCESS}"
env:
STEPS_SET_SUCCESS_OUTPUT_OUTPUTS_SUCCESS: ${{ steps.set-success-output.outputs.success }}
build-docker-image-release:
needs: all-ci-passed
@@ -671,25 +884,27 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Set NEXT_PUBLIC_BUILD_ID
run: echo "NEXT_PUBLIC_BUILD_ID=$(git rev-parse --short HEAD)" >> $GITHUB_ENV
- name: Log in to the GitHub Container registry
uses: docker/login-action@465a07811f14bebb1938fbed4728c6a1ff8901fc # v2
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Log in to Docker Hub
uses: docker/login-action@465a07811f14bebb1938fbed4728c6a1ff8901fc # v2
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Setup Blacksmith Builder
uses: useblacksmith/setup-docker-builder@5241b2e9423e8b1fa37ed6050ecb62d0fb9a4e38 # v1
uses: useblacksmith/setup-docker-builder@5241b2e9423e8b1fa37ed6050ecb62d0fb9a4e38 # v1.6.0
- name: Extract metadata (labels) for Docker
id: meta
uses: docker/metadata-action@818d4b7b91585d195f67373fd9cb0332e31a7175 # v4
uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0
with:
images: |
ghcr.io/langfuse/${{ matrix.image_name }}
@@ -706,7 +921,7 @@ jobs:
type=raw,value=latest,enable=${{ startsWith(github.ref, 'refs/tags/v3') && !contains(github.ref, '-rc') }}
- name: Build and push image by digest to GitHub Container Registry (${{ matrix.component }}, ${{ matrix.platform_tag }})
id: build-ghcr
uses: useblacksmith/build-push-action@cbd1f60d194a98cb3be5523b15134501eaf0fbf3 # v2
uses: useblacksmith/build-push-action@cbd1f60d194a98cb3be5523b15134501eaf0fbf3 # v2.1.0
with:
context: .
file: ${{ matrix.dockerfile }}
@@ -717,7 +932,7 @@ jobs:
sbom: false
- name: Build and push image by digest to Docker Hub (${{ matrix.component }}, ${{ matrix.platform_tag }})
id: build-dockerhub
uses: useblacksmith/build-push-action@cbd1f60d194a98cb3be5523b15134501eaf0fbf3 # v2
uses: useblacksmith/build-push-action@cbd1f60d194a98cb3be5523b15134501eaf0fbf3 # v2.1.0
with:
context: .
file: ${{ matrix.dockerfile }}
@@ -727,17 +942,21 @@ jobs:
provenance: false
sbom: false
- name: Record pushed digests
env:
DIGEST_GHCR: ${{ steps.build-ghcr.outputs.digest }}
DIGEST_DOCKERHUB: ${{ steps.build-dockerhub.outputs.digest }}
PLATFORM_TAG: ${{ matrix.platform_tag }}
run: |
if [ -z '${{ steps.build-ghcr.outputs.digest }}' ] || [ -z '${{ steps.build-dockerhub.outputs.digest }}' ]; then
if [ -z "$DIGEST_GHCR" ] || [ -z "$DIGEST_DOCKERHUB" ]; then
echo "Missing registry digest output"
exit 1
fi
mkdir -p "$RUNNER_TEMP/digests/ghcr" "$RUNNER_TEMP/digests/dockerhub"
printf '%s\n' '${{ steps.build-ghcr.outputs.digest }}' > "$RUNNER_TEMP/digests/ghcr/${{ matrix.platform_tag }}.txt"
printf '%s\n' '${{ steps.build-dockerhub.outputs.digest }}' > "$RUNNER_TEMP/digests/dockerhub/${{ matrix.platform_tag }}.txt"
printf '%s\n' "$DIGEST_GHCR" > "$RUNNER_TEMP/digests/ghcr/${PLATFORM_TAG}.txt"
printf '%s\n' "$DIGEST_DOCKERHUB" > "$RUNNER_TEMP/digests/dockerhub/${PLATFORM_TAG}.txt"
- name: Upload release digests
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: release-digests-${{ matrix.component }}-${{ matrix.platform_tag }}
path: |
@@ -765,27 +984,27 @@ jobs:
steps:
- name: Log in to the GitHub Container registry
uses: docker/login-action@465a07811f14bebb1938fbed4728c6a1ff8901fc # v2
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Log in to Docker Hub
uses: docker/login-action@465a07811f14bebb1938fbed4728c6a1ff8901fc # v2
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Setup Blacksmith Builder
uses: useblacksmith/setup-docker-builder@5241b2e9423e8b1fa37ed6050ecb62d0fb9a4e38 # v1
uses: useblacksmith/setup-docker-builder@5241b2e9423e8b1fa37ed6050ecb62d0fb9a4e38 # v1.6.0
- name: Download release digests
uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
pattern: release-digests-${{ matrix.component }}-*
merge-multiple: true
path: ${{ runner.temp }}/digests
- name: Extract metadata (tags) for GitHub Container Registry
id: meta-ghcr
uses: docker/metadata-action@818d4b7b91585d195f67373fd9cb0332e31a7175 # v4
uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0
with:
images: ghcr.io/langfuse/${{ matrix.image_name }}
flavor: |
@@ -800,7 +1019,7 @@ jobs:
type=raw,value=latest,enable=${{ startsWith(github.ref, 'refs/tags/v3') && !contains(github.ref, '-rc') }}
- name: Extract metadata (tags) for Docker Hub
id: meta-dockerhub
uses: docker/metadata-action@818d4b7b91585d195f67373fd9cb0332e31a7175 # v4
uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0
with:
images: langfuse/${{ matrix.image_name }}
flavor: |
@@ -814,6 +1033,10 @@ jobs:
type=semver,pattern={{major}},enable=${{ !contains(github.ref, '-rc') }}
type=raw,value=latest,enable=${{ startsWith(github.ref, 'refs/tags/v3') && !contains(github.ref, '-rc') }}
- name: Publish multi-platform manifest to GitHub Container Registry
env:
STEPS_META_GHCR_OUTPUTS_TAGS: ${{ steps.meta-ghcr.outputs.tags }}
IMAGE_NAME: ${{ matrix.image_name }}
COMPONENT: ${{ matrix.component }}
run: |
ghcr_tags=()
ghcr_sources=()
@@ -821,23 +1044,27 @@ jobs:
while IFS= read -r tag; do
[ -n "$tag" ] || continue
ghcr_tags+=("-t" "$tag")
done <<'EOF'
${{ steps.meta-ghcr.outputs.tags }}
done <<EOF
${STEPS_META_GHCR_OUTPUTS_TAGS}
EOF
shopt -s nullglob
for digest_file in "$RUNNER_TEMP"/digests/ghcr/*.txt; do
digest="$(cat "$digest_file")"
ghcr_sources+=("ghcr.io/langfuse/${{ matrix.image_name }}@$digest")
ghcr_sources+=("ghcr.io/langfuse/${IMAGE_NAME}@$digest")
done
if [ "${#ghcr_sources[@]}" -lt 2 ]; then
echo "Expected amd64 and arm64 GHCR digests for ${{ matrix.component }}"
echo "Expected amd64 and arm64 GHCR digests for $COMPONENT"
exit 1
fi
docker buildx imagetools create "${ghcr_tags[@]}" "${ghcr_sources[@]}"
- name: Publish multi-platform manifest to Docker Hub
env:
STEPS_META_DOCKERHUB_OUTPUTS_TAGS: ${{ steps.meta-dockerhub.outputs.tags }}
IMAGE_NAME: ${{ matrix.image_name }}
COMPONENT: ${{ matrix.component }}
run: |
dockerhub_tags=()
dockerhub_sources=()
@@ -845,29 +1072,32 @@ jobs:
while IFS= read -r tag; do
[ -n "$tag" ] || continue
dockerhub_tags+=("-t" "$tag")
done <<'EOF'
${{ steps.meta-dockerhub.outputs.tags }}
done <<EOF
${STEPS_META_DOCKERHUB_OUTPUTS_TAGS}
EOF
shopt -s nullglob
for digest_file in "$RUNNER_TEMP"/digests/dockerhub/*.txt; do
digest="$(cat "$digest_file")"
dockerhub_sources+=("langfuse/${{ matrix.image_name }}@$digest")
dockerhub_sources+=("langfuse/${IMAGE_NAME}@$digest")
done
if [ "${#dockerhub_sources[@]}" -lt 2 ]; then
echo "Expected amd64 and arm64 Docker Hub digests for ${{ matrix.component }}"
echo "Expected amd64 and arm64 Docker Hub digests for $COMPONENT"
exit 1
fi
docker buildx imagetools create "${dockerhub_tags[@]}" "${dockerhub_sources[@]}"
- name: Inspect published manifests
run: |
ghcr_first_tag="$(printf '%s\n' "${{ steps.meta-ghcr.outputs.tags }}" | sed -n '1p')"
dockerhub_first_tag="$(printf '%s\n' "${{ steps.meta-dockerhub.outputs.tags }}" | sed -n '1p')"
ghcr_first_tag="$(printf '%s\n' "${STEPS_META_GHCR_OUTPUTS_TAGS}" | sed -n '1p')"
dockerhub_first_tag="$(printf '%s\n' "${STEPS_META_DOCKERHUB_OUTPUTS_TAGS}" | sed -n '1p')"
docker buildx imagetools inspect "$ghcr_first_tag"
docker buildx imagetools inspect "$dockerhub_first_tag"
env:
STEPS_META_GHCR_OUTPUTS_TAGS: ${{ steps.meta-ghcr.outputs.tags }}
STEPS_META_DOCKERHUB_OUTPUTS_TAGS: ${{ steps.meta-dockerhub.outputs.tags }}
notify-docker-image-release:
needs:
@@ -880,10 +1110,50 @@ jobs:
if: contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled')
run: exit 1
- name: Notify Slack
uses: ravsamhq/notify-slack-action@be814b201e233b2dc673608aa46e5447c8ab13f2 # v2
if: always()
if: failure()
uses: slackapi/slack-github-action@af78098f536edbc4de71162a307590698245be95 # v3.0.1
with:
status: ${{ job.status }}
notify_when: "failure"
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
webhook: ${{ secrets.SLACK_WEBHOOK_URL }}
webhook-type: incoming-webhook
payload: |
{
"text": "❌ Docker release failed on ${{ github.ref_name }}",
"blocks": [
{
"type": "header",
"text": {
"type": "plain_text",
"text": "❌ Docker Release Failed",
"emoji": true
}
},
{
"type": "section",
"fields": [
{
"type": "mrkdwn",
"text": "*Tag:*\n`${{ github.ref_name }}`"
},
{
"type": "mrkdwn",
"text": "*Triggered by:*\n${{ github.actor }}"
}
]
},
{
"type": "actions",
"elements": [
{
"type": "button",
"text": {
"type": "plain_text",
"text": "View Workflow Logs",
"emoji": true
},
"url": "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}",
"style": "danger"
}
]
}
]
}
@@ -12,6 +12,8 @@ concurrency:
group: promote-main-to-production
cancel-in-progress: false
permissions: {}
jobs:
promote:
runs-on: ubuntu-latest
@@ -19,16 +21,19 @@ jobs:
steps:
- name: Validate confirmation
run: |
if [ "${{ github.event.inputs.confirm }}" != "promote" ]; then
if [ "${INPUT_CONFIRM}" != "promote" ]; then
echo "Input 'confirm' must be 'promote'."
exit 1
fi
env:
INPUT_CONFIRM: ${{ github.event.inputs.confirm }}
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: main
fetch-depth: 0
token: ${{ secrets.GH_ACCESS_TOKEN }}
persist-credentials: false
- name: Print commit refs
run: |
@@ -41,4 +46,6 @@ jobs:
fi
- name: Force push main to production
run: git push origin +main:production
run: git push "https://x-access-token:${GH_ACCESS_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" +main:production
env:
GH_ACCESS_TOKEN: ${{ secrets.GH_ACCESS_TOKEN }}
+7 -2
View File
@@ -5,15 +5,20 @@ on:
tags:
- "v3.[0-9]+.[0-9]+" # Semantic version tags
permissions: {}
jobs:
release:
runs-on: ubuntu-latest
environment: "protected branches"
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: main # Always checkout main even for tagged releases
fetch-depth: 0
token: ${{ secrets.GH_ACCESS_TOKEN }}
persist-credentials: false
- name: Push to production
run: git push origin +main:production
run: git push "https://x-access-token:${GH_ACCESS_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" +main:production
env:
GH_ACCESS_TOKEN: ${{ secrets.GH_ACCESS_TOKEN }}
+13 -5
View File
@@ -12,23 +12,31 @@ concurrency:
group: sdk-api-spec-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: read
jobs:
generate-sdk-api-specs:
runs-on: ubuntu-latest
environment: "protected branches"
steps:
- name: Checkout repository
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Install pnpm
uses: pnpm/action-setup@eae0cfeb286e66ffb5155f1a79b90583a127a68b # v2
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
with:
version: 10.33.0
# Keep this secret-bearing job cache-cold to avoid exposing Fern and
# GitHub write credentials to potentially poisoned shared package-manager state.
- name: Setup Node.js
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version: "24"
cache: "pnpm"
package-manager-cache: false
- name: Install dependencies
run: pnpm install
@@ -39,7 +47,7 @@ jobs:
run: npx fern-api generate --api server --force
- name: Install uv
uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8
uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0
with:
version: "0.11.2"
+43
View File
@@ -0,0 +1,43 @@
---
name: Semgrep
on:
pull_request:
branches:
- "**"
types:
- opened
- reopened
- synchronize
- ready_for_review
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
semgrep:
name: Security scan
runs-on: ubuntu-latest
timeout-minutes: 30
container:
image: semgrep/semgrep:1.162.0@sha256:9349edbadf90c3f3c0c3f55867625354e89680e6fa10d9034042af52fdb0e0d0
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
persist-credentials: false
- name: Run Semgrep
env:
BASELINE_COMMIT: ${{ github.event.pull_request.base.sha }}
SEMGREP_SEND_METRICS: "off"
run: |
semgrep scan \
--config p/default \
--baseline-commit "$BASELINE_COMMIT" \
--error
+28 -19
View File
@@ -3,46 +3,55 @@ on:
push:
branches: ["production", "main"]
permissions:
contents: read
security-events: write
jobs:
snyk:
runs-on: ubuntu-latest
environment: snyk
steps:
- uses: actions/checkout@ee0669bd1cc54295c223e0bb666b733df41de1c5 # v2
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Set up Snyk CLI
uses: snyk/actions/setup@9adf32b1121593767fc3c057af55b55db032dc04 # master
with:
snyk-version: v1.1304.0
- name: Run Snyk to check Docker image for vulnerabilities
# Snyk can be used to break the build when it detects vulnerabilities.
# In this case we want to upload the issues to GitHub Code Scanning
continue-on-error: true
uses: snyk/actions/docker@9adf32b1121593767fc3c057af55b55db032dc04 # master
env:
# In order to use the Snyk Action you will need to have a Snyk API token.
# See https://docs.snyk.io/integrations/ci-cd-integrations/github-actions-integration#getting-your-snyk-token
# or you can sign up for free at https://snyk.io/login
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
with:
image: langfuse/langfuse
args: --sarif-file-output=snyk.sarif
- name: Fix SARIF file
run: |
snyk container test langfuse/langfuse \
--file=web/Dockerfile \
--sarif-file-output=snyk.sarif
- name: Normalize SARIF file
if: always()
run: |
if [ -f snyk.sarif ]; then
# Fix undefined security severity values in SARIF file
sed -i 's/"security-severity": "undefined"/"security-severity": "0"/g' snyk.sarif
# Snyk emits invalid security-severity values. upload-sarif requires a numeric string.
sed -i \
-e 's/"security-severity": "undefined"/"security-severity": "0"/g' \
-e 's/"security-severity": "null"/"security-severity": "0"/g' \
-e 's/"security-severity": null/"security-severity": "0"/g' \
snyk.sarif
echo "SARIF file fixed"
else
echo "No SARIF file found"
fi
- name: Echo SARIF file for debugging
if: always()
run: |
if [ -f snyk.sarif ]; then
echo "=== SARIF File Contents ==="
cat snyk.sarif
echo "=== End of SARIF File ==="
else
echo "No SARIF file found to display"
fi
- name: Upload result to GitHub Code Scanning
uses: github/codeql-action/upload-sarif@c10b8064de6f491fea524254123dbe5e09572f13 # v4
uses: github/codeql-action/upload-sarif@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1
if: always()
with:
sarif_file: snyk.sarif
category: snyk-container-web
- name: Echo SARIF file for debugging
if: failure() && hashFiles('snyk.sarif') != ''
run: cat snyk.sarif
+28 -19
View File
@@ -3,46 +3,55 @@ on:
push:
branches: ["production", "main"]
permissions:
contents: read
security-events: write
jobs:
snyk:
runs-on: ubuntu-latest
environment: snyk
steps:
- uses: actions/checkout@ee0669bd1cc54295c223e0bb666b733df41de1c5 # v2
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Set up Snyk CLI
uses: snyk/actions/setup@9adf32b1121593767fc3c057af55b55db032dc04 # master
with:
snyk-version: v1.1304.0
- name: Run Snyk to check Docker image for vulnerabilities
# Snyk can be used to break the build when it detects vulnerabilities.
# In this case we want to upload the issues to GitHub Code Scanning
continue-on-error: true
uses: snyk/actions/docker@9adf32b1121593767fc3c057af55b55db032dc04 # master
env:
# In order to use the Snyk Action you will need to have a Snyk API token.
# See https://docs.snyk.io/integrations/ci-cd-integrations/github-actions-integration#getting-your-snyk-token
# or you can sign up for free at https://snyk.io/login
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
with:
image: langfuse/langfuse-worker
args: --sarif-file-output=snyk.sarif
- name: Fix SARIF file
run: |
snyk container test langfuse/langfuse-worker \
--file=worker/Dockerfile \
--sarif-file-output=snyk.sarif
- name: Normalize SARIF file
if: always()
run: |
if [ -f snyk.sarif ]; then
# Fix undefined security severity values in SARIF file
sed -i 's/"security-severity": "undefined"/"security-severity": "0"/g' snyk.sarif
# Snyk emits invalid security-severity values. upload-sarif requires a numeric string.
sed -i \
-e 's/"security-severity": "undefined"/"security-severity": "0"/g' \
-e 's/"security-severity": "null"/"security-severity": "0"/g' \
-e 's/"security-severity": null/"security-severity": "0"/g' \
snyk.sarif
echo "SARIF file fixed"
else
echo "No SARIF file found"
fi
- name: Echo SARIF file for debugging
if: always()
run: |
if [ -f snyk.sarif ]; then
echo "=== SARIF File Contents ==="
cat snyk.sarif
echo "=== End of SARIF File ==="
else
echo "No SARIF file found to display"
fi
- name: Upload result to GitHub Code Scanning
uses: github/codeql-action/upload-sarif@c10b8064de6f491fea524254123dbe5e09572f13 # v4
uses: github/codeql-action/upload-sarif@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1
if: always()
with:
sarif_file: snyk.sarif
category: snyk-container-worker
- name: Echo SARIF file for debugging
if: failure() && hashFiles('snyk.sarif') != ''
run: cat snyk.sarif
+1 -1
View File
@@ -10,7 +10,7 @@ jobs:
issues: write
pull-requests: write
steps:
- uses: actions/stale@5bef64f19d7facfb25b37b414482c7164d639639 # v9
- uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10.2.0
with:
days-before-issue-stale: 30
days-before-issue-close: 14
+1 -1
View File
@@ -19,7 +19,7 @@ jobs:
pull-requests: read
steps:
- name: Validate PR title follows conventional commits
uses: amannn/action-semantic-pull-request@48f256284bd46cdaab1048c3721360e808335d50 # v6
uses: amannn/action-semantic-pull-request@48f256284bd46cdaab1048c3721360e808335d50 # v6.1.1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
+43
View File
@@ -0,0 +1,43 @@
---
name: Check GitHub Actions
on:
workflow_dispatch:
push:
branches:
- "main"
merge_group:
pull_request:
branches:
- "main"
permissions: {}
jobs:
zizmor:
name: Check GitHub Actions security
runs-on: ubuntu-latest
permissions:
security-events: write
contents: read
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Run zizmor (fork PR - fail on findings)
# Fork PRs don't get security-events: write on GITHUB_TOKEN, so SARIF
# upload isn't possible. Fail the job on findings instead so contributors
# see the error directly in the PR check.
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository
uses: zizmorcore/zizmor-action@b1d7e1fb5de872772f31590499237e7cce841e8e # v0.5.3
- name: Run zizmor (trusted - upload SARIF)
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
uses: zizmorcore/zizmor-action@b1d7e1fb5de872772f31590499237e7cce841e8e # v0.5.3
with:
# Means that the action will only report issues, but not fail the workflow.
# Blocking merges are handled by rulesets:
# https://docs.github.com/en/code-security/concepts/code-scanning/about-code-scanning-alerts#pull-request-check-failures-for-code-scanning-alerts
advanced-security: true
+21
View File
@@ -0,0 +1,21 @@
rules:
secrets-outside-env:
config:
allow:
# Shared read-only CI credentials used to avoid Docker Hub rate limits in test jobs.
- DOCKERHUB_USERNAME_READ
- DOCKERHUB_TOKEN_READ
# Shared integration-test credentials intentionally kept together for the multi-provider LLM connection test job.
- LANGFUSE_LLM_CONNECTION_OPENAI_KEY
- LANGFUSE_LLM_CONNECTION_ANTHROPIC_KEY
- LANGFUSE_LLM_CONNECTION_AZURE_KEY
- LANGFUSE_LLM_CONNECTION_AZURE_BASE_URL
- LANGFUSE_LLM_CONNECTION_AZURE_MODEL
- LANGFUSE_LLM_CONNECTION_BEDROCK_ACCESS_KEY_ID
- LANGFUSE_LLM_CONNECTION_BEDROCK_SECRET_ACCESS_KEY
- LANGFUSE_LLM_CONNECTION_BEDROCK_API_KEY
- LANGFUSE_LLM_CONNECTION_BEDROCK_REGION
- LANGFUSE_LLM_CONNECTION_VERTEXAI_KEY
- LANGFUSE_LLM_CONNECTION_GOOGLEAISTUDIO_KEY
# Fern token is generation-only; GitHub write actions are handled by GH_ACCESS_TOKEN in a protected environment.
- FERN_TOKEN
+5
View File
@@ -47,6 +47,7 @@ yarn-error.log*
!.env.dev-redis-cluster.example
!.env.prod.example
!.env.test.example
!.env.dev-oci.example
# vercel
.vercel
@@ -92,3 +93,7 @@ web/test-results/*
# Refactoring planning files (local only)
**/.refactor/
**/.pi-lens/
# Serena MCP server config (local only)
.serena/
+5 -7
View File
@@ -316,18 +316,16 @@ Tests automatically create the PostgreSQL test database if it doesn't exist and
### Tests in the `web` package (public API)
We're using Jest with in the `web` package. Therefore, if you want to provide an argument to the test runner, do it directly without an intermittent `--`.
There are two types of unit tests:
We're using Vitest in the `web` package. There are two types of unit tests:
- `test` (server tests)
- `test-client`
To run a specific test, for example the test: `"should handle special characters in prompt names"` in `prompts.v2.servertest.ts`, run:
To run a specific test by name within a file, run:
```sh
cd web # or with --filter=web
pnpm test --testPathPatterns="prompts\.v2\.servertest" --testNamePattern="should handle special characters in prompt names"
pnpm test -- prompts.v2.servertest -t "should handle special characters in prompt names"
```
To run all tests:
@@ -344,7 +342,7 @@ pnpm run test:watch
### Tests in the `worker` package
For the `worker` package, we're using `vitest` to run unit tests.
For the `worker` package, we're also using Vitest to run unit tests.
```sh
pnpm run test --filter=worker -- FILE_YOU_WANT_TO_TEST.ts -t "test name"
@@ -357,7 +355,7 @@ We use GitHub Actions for CI/CD, the configuration is in [`.github/workflows/pip
CI on `main` and `pull_request`
- Check Linting
- E2E test of API using Jest
- E2E test of API using Vitest
- E2E tests of UI using Playwright
CD on `main`
+2
View File
@@ -31,6 +31,8 @@ services:
CLICKHOUSE_PASSWORD: ${CLICKHOUSE_PASSWORD:-clickhouse} # CHANGEME
CLICKHOUSE_CLUSTER_ENABLED: ${CLICKHOUSE_CLUSTER_ENABLED:-false}
LANGFUSE_USE_AZURE_BLOB: ${LANGFUSE_USE_AZURE_BLOB:-false}
LANGFUSE_USE_OCI_NATIVE_OBJECT_STORAGE: ${LANGFUSE_USE_OCI_NATIVE_OBJECT_STORAGE:-false}
LANGFUSE_OCI_AUTH_TYPE: ${LANGFUSE_OCI_AUTH_TYPE:-workload_identity}
LANGFUSE_S3_EVENT_UPLOAD_BUCKET: ${LANGFUSE_S3_EVENT_UPLOAD_BUCKET:-langfuse}
LANGFUSE_S3_EVENT_UPLOAD_REGION: ${LANGFUSE_S3_EVENT_UPLOAD_REGION:-auto}
LANGFUSE_S3_EVENT_UPLOAD_ACCESS_KEY_ID: ${LANGFUSE_S3_EVENT_UPLOAD_ACCESS_KEY_ID:-minio}
+2 -2
View File
@@ -29,7 +29,7 @@
"@langfuse/shared": "workspace:*",
"@opentelemetry/api": ">=1.0.0 <1.10.0",
"https-proxy-agent": "^7.0.6",
"next": "16.2.3",
"next": "16.2.6",
"next-auth": "^4.24.13",
"zod": "^4.3.6"
},
@@ -39,7 +39,7 @@
"@types/node": "^24.3.0",
"@typescript/native-preview": "7.0.0-dev.20260122.3",
"eslint": "^9.39.2",
"prettier": "^3.8.1",
"prettier": "^3.8.3",
"ts-node": "^10.9.2",
"tsc-watch": "^6.2.0",
"typescript": "^5.7.2"
+19
View File
@@ -32,6 +32,9 @@ types:
configId:
type: optional<string>
docs: Reference a score config on a score. When set, the score name must equal the config name and scores must comply with the config's range and data type. For categorical scores, the value must map to a config category. Numeric scores might be constrained by the score config's max and min values
source:
type: optional<CreateScoreSource>
docs: The source of the score. Defaults to API. Set to ANNOTATION to prefill scores (e.g. from an LLM) for a human reviewer to verify in an annotation queue. When source is ANNOTATION, a configId is required unless dataType is CORRECTION. EVAL is reserved for internal evaluator outputs and is not accepted on this endpoint.
examples:
- value:
name: "novelty"
@@ -77,6 +80,22 @@ types:
name: "hallucination"
value: 0
datasetRunId: "7891-5678-90ab-hijk"
- value:
name: "accuracy"
value: 0.9
dataType: "NUMERIC"
configId: "9203-4567-89ab-cdef"
traceId: "cdef-1234-5678-90ab"
source: "ANNOTATION"
CreateScoreSource:
docs: |
Source values accepted when creating a score via the public API.
EVAL is reserved for internal evaluator outputs and is intentionally not
exposed here.
enum:
- API
- ANNOTATION
ScoreDataType:
enum:
@@ -56,10 +56,39 @@ types:
BlobStorageExportFrequency:
enum:
- every_20_minutes
- hourly
- daily
- weekly
BlobStorageExportSource:
docs: |
What data the integration exports.
- `TRACES_OBSERVATIONS`: legacy traces + observations + scores tables with a fixed column set. The `exportFieldGroups` field is not applicable.
- `EVENTS`: enriched observations_v2 events; columns are controlled by `exportFieldGroups`.
- `TRACES_OBSERVATIONS_EVENTS`: both sets. For the `EVENTS` portion, columns are controlled by `exportFieldGroups`.
**Note:** `EVENTS` and the events portion of `TRACES_OBSERVATIONS_EVENTS` rely on the observations_v2 events table (Langfuse Fast Preview / v4), which is currently available on Langfuse Cloud only. See https://langfuse.com/docs/v4.
enum:
- EVENTS
- TRACES_OBSERVATIONS
- TRACES_OBSERVATIONS_EVENTS
BlobStorageExportFieldGroup:
docs: Field group for the EVENTS export.
enum:
- core
- basic
- time
- io
- metadata
- model
- usage
- prompt
- metrics
- tools
- trace_context
CreateBlobStorageIntegrationRequest:
properties:
projectId:
@@ -99,6 +128,21 @@ types:
compressed:
type: optional<boolean>
docs: Enable gzip compression for exported files (.csv.gz, .json.gz, .jsonl.gz). Defaults to true.
exportSource:
type: optional<BlobStorageExportSource>
docs: |
Data to export. When omitted on update, the existing value is preserved; when omitted on create, the column default (`TRACES_OBSERVATIONS`) applies. Required when `exportFieldGroups` is provided.
exportFieldGroups:
type: optional<list<BlobStorageExportFieldGroup>>
docs: |
Field groups to include in each exported row.
For exportSource `EVENTS` or `TRACES_OBSERVATIONS_EVENTS`: must include `core` if provided. When omitted on create, the column default (all groups) applies. When omitted on update, the existing value is preserved.
For exportSource `TRACES_OBSERVATIONS`: this field must be omitted or null. Sending an array (including an empty array) returns 400, because that source uses a fixed column set and does not honor field groups.
`exportFieldGroups` requires `exportSource` to be provided in the same request.
BlobStorageIntegrationResponse:
properties:
@@ -117,6 +161,11 @@ types:
exportMode: BlobStorageExportMode
exportStartDate: nullable<datetime>
compressed: boolean
exportSource: BlobStorageExportSource
exportFieldGroups:
type: nullable<list<BlobStorageExportFieldGroup>>
docs: |
Field groups included in each exported row for `EVENTS` / `TRACES_OBSERVATIONS_EVENTS` sources. Always `null` when exportSource is `TRACES_OBSERVATIONS` (the field does not apply to that source; any legacy DB value is hidden from the public surface).
nextSyncAt: nullable<datetime>
lastSyncAt: nullable<datetime>
lastError: nullable<string>
@@ -138,8 +187,8 @@ types:
- `up_to_date` — all available data has been exported; next export is scheduled for the future
**ETL usage**: poll this endpoint and check for `up_to_date` status. Compare `lastSyncAt` against your
ETL bookmark to determine if new data is available. Note that exports run with a 30-minute lag buffer,
so `lastSyncAt` will always be at least 30 minutes behind real-time.
ETL bookmark to determine if new data is available. Note that exports run with a 20-minute lag buffer,
so `lastSyncAt` will always be at least 20 minutes behind real-time.
enum:
- idle
- queued
@@ -46,6 +46,9 @@ types:
configId:
type: optional<string>
docs: Reference a score config on a score. The unique langfuse identifier of a score config. When passing this field, the dataType and stringValue fields are automatically populated.
source:
type: optional<CreateScoreSource>
docs: The source of the score. Defaults to API. Set to ANNOTATION to prefill scores (e.g. from an LLM) for a human reviewer to verify in an annotation queue. When source is ANNOTATION, a configId is required unless dataType is CORRECTION. EVAL is reserved for internal evaluator outputs and is not accepted on this endpoint.
examples:
- value:
name: "novelty"
@@ -90,6 +93,22 @@ types:
value: "Great explanation of the concept"
dataType: "TEXT"
traceId: "cdef-1234-5678-90ab"
- value:
name: "accuracy"
value: 0.9
dataType: "NUMERIC"
configId: "9203-4567-89ab-cdef"
traceId: "cdef-1234-5678-90ab"
source: "ANNOTATION"
queueId: "aq-1234-5678-90ab-cdef"
CreateScoreSource:
docs: |
Source values accepted when creating a score via the public REST API.
EVAL is reserved for internal evaluator outputs and is intentionally not
exposed here — use commons.ScoreSource when reading scores.
enum:
- API
- ANNOTATION
CreateScoreResponse:
properties:
id:
@@ -26,6 +26,13 @@ service:
path: /llm-connections
request: UpsertLlmConnectionRequest
response: LlmConnection
delete:
method: DELETE
docs: Delete an LLM connection by id. Evaluators that depend on the deleted connection are automatically paused.
path: /llm-connections/{id}
path-parameters:
id: string
response: DeleteLlmConnectionResponse
types:
LlmConnection:
@@ -96,6 +103,10 @@ types:
- **VertexAI**: Optional. If provided, must be `{"location": "<gcp-location>"}` (e.g., `{"location":"us-central1"}`)
- **Other adapters**: Not supported. Omit this field or set to null.
DeleteLlmConnectionResponse:
properties:
message: string
LlmAdapter:
enum:
- value: anthropic
@@ -54,7 +54,9 @@ types:
meta: pagination.MetaResponse
CreateScoreConfigRequest:
properties:
name: string
name:
type: string
docs: "Name of the score config. Max 35 characters. Only letters, numbers, underscores, spaces, periods, parentheses, and hyphens are allowed."
dataType: commons.ScoreConfigDataType
categories:
type: optional<list<commons.ConfigCategory>>
@@ -75,7 +77,7 @@ types:
docs: The status of the score config showing if it is archived or not
name:
type: optional<string>
docs: The name of the score config
docs: "Name of the score config. Max 35 characters. Only letters, numbers, underscores, spaces, periods, parentheses, and hyphens are allowed."
categories:
type: optional<list<commons.ConfigCategory>>
docs: Configure custom categories for categorical scores. Pass a list of objects with `label` and `value` properties. Categories are autogenerated for boolean configs and cannot be passed
+1 -1
View File
@@ -18,7 +18,7 @@ service:
docs: Page number, starts at 1.
limit:
type: optional<integer>
docs: Limit of items per page. If you encounter api issues due to too large page sizes, try to reduce the limit.
docs: Limit of items per page. Maximum 100. Defaults to 50. Requests with a limit greater than 100 return HTTP 400. If you encounter api issues due to too large page sizes, try to reduce the limit.
userId:
type: optional<string>
docs: Retrieve only scores with this userId associated to the trace.
@@ -0,0 +1,564 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/fern-api/fern/main/fern.schema.json
types:
EvaluatorType:
docs: |
The evaluator engine type.
The unstable public API currently supports only LLM-as-a-judge evaluators.
enum:
- llm_as_judge
EvaluatorScope:
docs: |
Where an evaluator comes from.
- `project`: created in your project
- `managed`: provided by Langfuse
enum:
- project
- managed
EvaluationRuleTarget:
docs: |
The ingestion object type that should trigger evaluation runs.
Choose the target first, because it changes both the valid filter columns and the valid variable-mapping sources:
- `observation` evaluates live-ingested observations such as generations, spans, and events.
It supports mapping from `input`, `output`, and `metadata`.
- `experiment` evaluates live experiment executions and can additionally map `expected_output`.
It currently supports filtering by `datasetId`.
Discover valid dataset IDs with `GET /api/public/v2/datasets`, then use the returned dataset `id` values in your filter.
enum:
- observation
- experiment
EvaluationRuleStatus:
docs: |
Effective runtime status of the evaluation rule.
- `active`: enabled and currently runnable.
- `inactive`: disabled by configuration.
- `paused`: enabled, but Langfuse has blocked execution until the underlying issue is resolved.
enum:
- active
- inactive
- paused
EvaluationRuleMappingSource:
docs: |
Source field used to populate a prompt variable.
Use these values when mapping evaluator prompt variables to live data.
Target-specific rules:
- `target=observation` supports `input`, `output`, and `metadata`
- `target=experiment` supports `input`, `output`, `metadata`, and `expected_output`
Source semantics:
- `input`: the observation or experiment input payload
- `output`: the observation or experiment output payload
- `metadata`: the metadata object for the target. Combine with `jsonPath` when you need one nested field instead of the whole object.
- `expected_output`: the experiment item's expected output. Only valid for `target=experiment`.
enum:
- input
- output
- metadata
- expected_output
EvaluatorModelConfig:
docs: |
Optional explicit model configuration for an evaluator.
If omitted, Langfuse uses the project's default evaluation model.
If provided, the model must be available to the project when the evaluator or evaluation rule is enabled.
To discover valid configured `provider` values for a project, call `GET /api/public/llm-connections` and read the `provider` field from the returned connections.
Use a `provider` value that matches one of the connections already configured in the same project.
Recovery guidance:
- If evaluator creation returns `422` with `code=evaluator_preflight_failed`, either provide a valid explicit `modelConfig` here or configure the project's default evaluation model, then retry the same request.
properties:
provider:
type: string
docs: |
Provider identifier to use for this evaluator, for example `openai` or `anthropic`.
To discover valid values for the current project, call `GET /api/public/llm-connections` and use one of the returned `provider` values.
model:
type: string
docs: Model identifier exposed by the provider, for example `gpt-4.1-mini`.
examples:
- value:
provider: openai
model: gpt-4.1-mini
EvaluatorOutputDataType:
docs: |
Structured score type returned by an evaluator.
This controls the type of score value Langfuse stores for evaluation results:
- `NUMERIC`: a numeric score such as `0.82`
- `BOOLEAN`: a boolean score such as `true`
- `CATEGORICAL`: one or more category labels from a fixed list
enum:
- NUMERIC
- BOOLEAN
- CATEGORICAL
EvaluatorOutputFieldDefinition:
properties:
description:
type: string
docs: Human-readable instructions for what the evaluator should return in this field.
EvaluatorOutputDefinition:
docs: |
Structured output definition to send when creating an evaluator.
Agent guidance:
- `dataType` is required.
- Do not send `version`; that is an internal storage detail and is not part of the public request contract.
- For `NUMERIC` and `BOOLEAN`, provide `reasoning.description` and `score.description`.
- For `CATEGORICAL`, also provide `score.categories` and `score.shouldAllowMultipleMatches`.
discriminant: dataType
union:
NUMERIC:
type: PublicNumericEvaluatorOutputDefinition
BOOLEAN:
type: PublicBooleanEvaluatorOutputDefinition
CATEGORICAL:
type: PublicCategoricalEvaluatorOutputDefinition
examples:
- name: Numeric
value:
dataType: NUMERIC
reasoning:
description: Explain why the answer is correct or incorrect.
score:
description: Return a score between 0 and 1.
- name: Boolean
value:
dataType: BOOLEAN
reasoning:
description: Explain why the output satisfies the requirement.
score:
description: Return true if the output satisfies the requirement, otherwise false.
- name: Categorical
value:
dataType: CATEGORICAL
reasoning:
description: Explain which category best fits the output.
score:
description: Choose the best category.
categories:
- correct
- partially_correct
- incorrect
shouldAllowMultipleMatches: false
PublicNumericEvaluatorOutputDefinition:
properties:
dataType:
type: EvaluatorOutputDataType
docs: Always `NUMERIC`.
reasoning:
type: EvaluatorOutputFieldDefinition
score:
type: EvaluatorOutputFieldDefinition
PublicBooleanEvaluatorOutputDefinition:
properties:
dataType:
type: EvaluatorOutputDataType
docs: Always `BOOLEAN`.
reasoning:
type: EvaluatorOutputFieldDefinition
score:
type: EvaluatorOutputFieldDefinition
PublicCategoricalEvaluatorOutputScoreDefinition:
properties:
description:
type: string
categories:
type: list<string>
shouldAllowMultipleMatches:
type: boolean
PublicCategoricalEvaluatorOutputDefinition:
properties:
dataType:
type: EvaluatorOutputDataType
docs: Always `CATEGORICAL`.
reasoning:
type: EvaluatorOutputFieldDefinition
score:
type: PublicCategoricalEvaluatorOutputScoreDefinition
PublicEvaluatorOutputDefinition:
docs: |
Evaluator output definition returned by the public API.
This response always includes `dataType` and never includes an internal output-definition `version`.
Legacy stored evaluator definitions are normalized into this shape before they are returned.
Use this response shape when deciding how to interpret future evaluation scores:
- `NUMERIC`: expect numeric score values
- `BOOLEAN`: expect `true` / `false`
- `CATEGORICAL`: expect one or more values from `score.categories`
discriminant: dataType
union:
NUMERIC:
type: PublicNumericEvaluatorOutputDefinition
BOOLEAN:
type: PublicBooleanEvaluatorOutputDefinition
CATEGORICAL:
type: PublicCategoricalEvaluatorOutputDefinition
examples:
- name: PublicNumeric
value:
dataType: NUMERIC
reasoning:
description: Explain why the answer is correct or incorrect.
score:
description: Return a score between 0 and 1.
- name: PublicCategorical
value:
dataType: CATEGORICAL
reasoning:
description: Explain which label best fits the output.
score:
description: Choose the best label.
categories:
- correct
- partially_correct
- incorrect
shouldAllowMultipleMatches: false
EvaluationRuleStringFilterOperator:
enum:
- name: Equals
value: "="
- name: Contains
value: contains
- name: DoesNotContain
value: does not contain
- name: StartsWith
value: starts with
- name: EndsWith
value: ends with
EvaluationRuleNumberFilterOperator:
enum:
- name: Equals
value: "="
- name: GreaterThan
value: ">"
- name: LessThan
value: "<"
- name: GreaterThanOrEqual
value: ">="
- name: LessThanOrEqual
value: "<="
EvaluationRuleOptionsFilterOperator:
enum:
- name: AnyOf
value: any of
- name: NoneOf
value: none of
EvaluationRuleArrayOptionsFilterOperator:
enum:
- name: AnyOf
value: any of
- name: NoneOf
value: none of
- name: AllOf
value: all of
EvaluationRuleBooleanFilterOperator:
enum:
- name: Equals
value: "="
- name: NotEquals
value: "<>"
EvaluationRuleNullFilterOperator:
enum:
- name: IsNull
value: is null
- name: IsNotNull
value: is not null
DateTimeEvaluationRuleFilter:
properties:
column:
type: string
docs: Column to filter on.
operator:
type: EvaluationRuleNumberFilterOperator
docs: Comparison operator for datetime values.
value:
type: datetime
docs: Datetime value to compare against.
StringEvaluationRuleFilter:
properties:
column:
type: string
docs: Column to filter on.
operator:
type: EvaluationRuleStringFilterOperator
value:
type: string
NumberEvaluationRuleFilter:
properties:
column:
type: string
docs: Column to filter on.
operator:
type: EvaluationRuleNumberFilterOperator
value:
type: double
StringOptionsEvaluationRuleFilter:
properties:
column:
type: string
docs: Column to filter on.
operator:
type: EvaluationRuleOptionsFilterOperator
value:
type: list<string>
docs: One or more allowed string values.
ArrayOptionsEvaluationRuleFilter:
properties:
column:
type: string
docs: Column to filter on.
operator:
type: EvaluationRuleArrayOptionsFilterOperator
value:
type: list<string>
docs: One or more array elements to match.
StringObjectEvaluationRuleFilter:
properties:
column:
type: string
docs: Object-valued column to filter on. In the unstable public API this is currently `metadata`.
key:
type: string
docs: Top-level key inside the object-valued column to filter on.
operator:
type: EvaluationRuleStringFilterOperator
value:
type: string
NumberObjectEvaluationRuleFilter:
properties:
column:
type: string
docs: Object-valued column to filter on.
key:
type: string
docs: Key inside the object-valued column to filter on.
operator:
type: EvaluationRuleNumberFilterOperator
value:
type: double
CategoryOptionsEvaluationRuleFilter:
properties:
column:
type: string
docs: Object-valued column to filter on.
key:
type: string
docs: Key inside the object-valued column to filter on.
operator:
type: EvaluationRuleOptionsFilterOperator
value:
type: list<string>
BooleanEvaluationRuleFilter:
properties:
column:
type: string
docs: Column to filter on.
operator:
type: EvaluationRuleBooleanFilterOperator
value:
type: boolean
NullEvaluationRuleFilter:
properties:
column:
type: string
docs: Column to filter on. In the unstable public API this is currently `parentObservationId`.
operator:
type: EvaluationRuleNullFilterOperator
value:
type: optional<string>
docs: Ignored placeholder value. Clients may omit it or send an empty string.
EvaluationRuleMapping:
docs: |
Maps one evaluator prompt variable to one source field from the target object.
How to build a valid mapping list:
1. Create the evaluator or fetch it with `GET /evaluators/{id}`.
2. Read the evaluator `variables` array.
3. Add exactly one mapping object for each variable in that array.
4. Use the variable name exactly as returned, without braces such as `{{` or `}}`.
5. Choose a `source` that is valid for the selected `target`.
`jsonPath` is optional. Use it only when the selected source is a JSON object and you want to extract one nested field before inserting it into the evaluator prompt.
Recovery guidance:
- `invalid_variable_mapping`: the variable name is unknown for this evaluator, or the selected `source` is not valid for the chosen `target`
- `missing_variable_mapping`: one or more evaluator variables are not mapped yet
- `duplicate_variable_mapping`: the same evaluator variable appears more than once
- `invalid_json_path`: the JSONPath expression is malformed. Remove it or correct it.
properties:
variable:
type: string
docs: |
Prompt variable name without braces.
Example: for the prompt `Judge {{input}} against {{output}}`, use `input` and `output`.
source:
type: EvaluationRuleMappingSource
docs: |
Source field that should populate the prompt variable.
Quick reference:
- `target=observation`: `input`, `output`, `metadata`
- `target=experiment`: `input`, `output`, `metadata`, `expected_output`
jsonPath:
type: optional<string>
docs: |
Optional JSONPath selector applied to the selected source before it is passed to the evaluator prompt.
Requirements:
- Must start with `$`
- Must be a syntactically valid JSONPath expression
- Most useful with `source=metadata`
examples:
- name: BasicObservationMapping
value:
variable: input
source: input
- name: MetadataProjectionMapping
value:
variable: customer_tier
source: metadata
jsonPath: "$.customer.tier"
- name: ExperimentExpectedOutputMapping
value:
variable: expected_output
source: expected_output
EvaluationRuleFilter:
docs: |
One filter condition used to decide whether a live-ingested target should be evaluated.
An evaluation rule can include zero or more filter objects. All filters must be satisfied for the target to run.
How to build a valid filter object:
- Pick the `target` first, because it changes the supported columns.
- Pick the filter `type`. That determines which fields are required.
- Use `key` only for object filters such as `metadata`.
- Use the correct `value` shape for the chosen filter `type`.
Operator quick reference by filter `type`:
- `string`: `"="`, `contains`, `does not contain`, `starts with`, `ends with`
- `number`: `"="`, `">"`, `"<"`, `">="`, `"<="`
- `datetime`: `"="`, `">"`, `"<"`, `">="`, `"<="`
- `stringOptions`: `any of`, `none of`
- `arrayOptions`: `any of`, `none of`, `all of`
- `stringObject`: same operators as `string`
- `null`: `is null`, `is not null`
Supported columns by target:
- `target=observation`
- `type`: `stringOptions`, operators `any of` / `none of`, values `GENERATION`, `SPAN`, `EVENT`
- `name`: `stringOptions`, operators `any of` / `none of`
- `environment`: `stringOptions`, operators `any of` / `none of`
- `level`: `stringOptions`, operators `any of` / `none of`, values `DEBUG`, `DEFAULT`, `WARNING`, `ERROR`
- `version`: `string`
- `traceName`: `stringOptions`, operators `any of` / `none of`
- `userId`: `string`
- `sessionId`: `string`
- `tags`: `arrayOptions`, operators `any of` / `none of` / `all of`
- `metadata`: `stringObject` with `key`
- `parentObservationId`: `null`, operators `is null` / `is not null`
- `calledToolNames`: `arrayOptions`, operators `any of` / `none of` / `all of`
- `toolCalls`: `number`
- `target=experiment`
- `datasetId`: `stringOptions`, operators `any of` / `none of`
Use dataset `id` values from `GET /api/public/v2/datasets`, not dataset names.
Recovery guidance:
- `invalid_filter_value` with `details.column` but no `invalidValues`: the selected `column` is not supported for the chosen `target`
- `invalid_filter_value` with `details.invalidValues`: the selected values are not allowed for that column. Replace them with one of `details.allowedValues` when provided.
- `invalid_filter_value` for `column=datasetId`: call `GET /api/public/v2/datasets`, then retry with dataset `id` values from that response.
discriminant: type
union:
"datetime":
type: DateTimeEvaluationRuleFilter
"string":
type: StringEvaluationRuleFilter
"number":
type: NumberEvaluationRuleFilter
"stringOptions":
type: StringOptionsEvaluationRuleFilter
"categoryOptions":
type: CategoryOptionsEvaluationRuleFilter
"arrayOptions":
type: ArrayOptionsEvaluationRuleFilter
"stringObject":
type: StringObjectEvaluationRuleFilter
"numberObject":
type: NumberObjectEvaluationRuleFilter
"boolean":
type: BooleanEvaluationRuleFilter
"null":
type: NullEvaluationRuleFilter
examples:
- name: ObservationTypeFilter
value:
type: stringOptions
column: type
operator: any of
value:
- GENERATION
- name: ObservationMetadataFilter
value:
type: stringObject
column: metadata
key: customerTier
operator: "="
value: enterprise
- name: ObservationRootOnlyFilter
value:
type: "null"
column: parentObservationId
operator: is null
- name: ObservationTagsFilter
value:
type: arrayOptions
column: tags
operator: any of
value:
- production
- name: ExperimentDatasetFilter
value:
type: stringOptions
column: datasetId
operator: any of
value:
- "550e8400-e29b-41d4-a716-446655440000"
@@ -0,0 +1,269 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/fern-api/fern/main/fern.schema.json
types:
PublicApiErrorCode:
docs: |
Machine-readable error code returned by the unstable evaluators API.
SDKs, CLIs, and agents should branch on `code` rather than parsing the human-readable `message`.
The HTTP status still indicates the broad error class, while `code` gives the specific failure reason.
enum:
- authentication_failed
- access_denied
- invalid_request
- invalid_query
- invalid_body
- invalid_filter_value
- invalid_json_path
- invalid_variable_mapping
- missing_variable_mapping
- duplicate_variable_mapping
- resource_not_found
- name_conflict
- evaluator_preflight_failed
- conflict
- unprocessable_content
- rate_limited
- method_not_allowed
- internal_error
PublicApiValidationIssue:
docs: |
One validation issue returned for malformed request bodies or query parameters.
This mirrors the most important parts of a Zod issue: a machine-readable `code`,
a human-readable `message`, and a structured `path`.
properties:
code:
type: string
docs: Machine-readable validation issue code emitted by the server validator.
message:
type: string
docs: Human-readable explanation of the validation failure.
path:
type: list<unknown>
docs: Path to the invalid field, for example `["mapping", 0, "jsonPath"]`.
PublicApiErrorDetails:
docs: |
Optional structured context attached to an unstable-evals error.
The populated fields depend on the error `code`:
- request parsing failures populate `issues`
- filter validation failures populate `field`, `column`, `invalidValues`, and `allowedValues`
- variable mapping failures populate `field`, `variable`, or `variables`
- JSONPath validation failures populate `field`, `variable`, and `value`
- evaluator preflight failures populate `evaluatorName`, `provider`, and `model`
- rate limiting populates `retryAfterSeconds`, `limit`, `remaining`, and `resetAt`
properties:
issues:
type: optional<list<PublicApiValidationIssue>>
docs: Validation issues for malformed request bodies or query parameters.
field:
type: optional<string>
docs: Path-like reference to the failing field, for example `mapping[1].jsonPath`.
column:
type: optional<string>
docs: Filter column that failed validation.
invalidValues:
type: optional<list<string>>
docs: Unsupported values supplied by the caller.
allowedValues:
type: optional<list<string>>
docs: Allowed values for the failing filter column.
variable:
type: optional<string>
docs: Evaluator variable involved in the failure.
variables:
type: optional<list<string>>
docs: Multiple evaluator variables involved in the failure, for example missing mappings.
value:
type: optional<string>
docs: Raw invalid value supplied by the caller.
evaluatorName:
type: optional<string>
docs: Evaluator name used during preflight validation.
provider:
type: optional<nullable<string>>
docs: Provider resolved during evaluator preflight, if any.
model:
type: optional<nullable<string>>
docs: Model resolved during evaluator preflight, if any.
retryAfterSeconds:
type: optional<integer>
docs: Suggested retry delay for rate-limited requests.
limit:
type: optional<integer>
docs: Numeric limit associated with the failure, for example the active evaluation-rule cap or the current rate-limit window.
remaining:
type: optional<integer>
docs: Remaining requests in the current rate-limit window.
resetAt:
type: optional<string>
docs: ISO-8601 timestamp when the current rate-limit window resets.
PublicApiError:
docs: |
Standard error envelope for the unstable evaluators API.
Response handling guidance:
- Use the HTTP status code for the broad class of failure.
- Use `code` for precise branching in SDKs, CLIs, or agents.
- Inspect `details` for field-level validation context such as invalid filter values, malformed JSONPath expressions, or missing variable mappings.
- Retry only after fixing the specific issue described by `code` and `details`.
properties:
message:
type: string
docs: Human-readable description of the failure.
code:
type: PublicApiErrorCode
docs: Stable machine-readable error code.
details:
type: optional<PublicApiErrorDetails>
docs: Optional structured error context. Inspect the populated fields based on `code`.
examples:
- name: InvalidFilterValue
value:
message: 'Filter column "type" contains unsupported value(s): INVALID'
code: invalid_filter_value
details:
field: filter[0].value
column: type
invalidValues:
- INVALID
allowedValues:
- GENERATION
- SPAN
- EVENT
- name: InvalidExperimentDatasetFilterValue
value:
message: 'Filter column "datasetId" contains dataset id(s) that do not exist in this project: dataset-prod'
code: invalid_filter_value
details:
field: filter[0].value
column: datasetId
invalidValues:
- dataset-prod
- name: EvaluatorPreflightFailed
value:
message: 'No valid LLM model found for evaluator "answer-correctness". No default model or custom model configured for project project_123'
code: evaluator_preflight_failed
details:
evaluatorName: answer-correctness
provider: null
model: null
- name: MissingVariableMapping
value:
message: "Missing mappings for evaluator variable(s): output"
code: missing_variable_mapping
details:
field: mapping
variables:
- output
- name: InvalidJsonPath
value:
message: 'Mapping for variable "customer_tier" has an invalid jsonPath "$[". JSONPath expressions must use balanced quotes, brackets, and parentheses.'
code: invalid_json_path
details:
field: mapping[0].jsonPath
variable: customer_tier
value: "$["
- name: NameConflict
value:
message: 'An evaluation rule named "answer-quality-live" already exists in this project. Use PATCH /api/public/unstable/evaluation-rules/erule_123 to update it instead of creating a duplicate.'
code: name_conflict
details:
field: name
- name: ActiveEvaluationRuleLimit
value:
message: This project already has the maximum number of active evaluation rules (50). Disable an existing active evaluation rule before enabling another one.
code: conflict
details:
limit: 50
- name: RateLimited
value:
message: Rate limit exceeded
code: rate_limited
details:
retryAfterSeconds: 60
limit: 1000
remaining: 0
resetAt: "2026-03-30T10:00:00.000Z"
errors:
BadRequestError:
docs: |
Request parsing or validation failed.
Typical unstable-eval examples:
- malformed request body or query parameters
- unsupported filter value
- invalid JSONPath selector
- missing or duplicate variable mappings
Recovery guidance:
- read `details.issues` for malformed bodies or queries
- read `details.column`, `details.invalidValues`, and `details.allowedValues` for filter problems
- for `details.column=datasetId`, call `GET /api/public/v2/datasets` and retry with dataset `id` values from that response
- read `details.variable` or `details.variables` for mapping problems
status-code: 400
type: PublicApiError
UnauthorizedError:
docs: |
Authentication failed.
Typical unstable-eval examples:
- the API key or secret key is invalid
- the request uses the wrong host or stale credentials
status-code: 401
type: PublicApiError
AccessDeniedError:
docs: |
Authenticated, but the caller is not allowed to access the requested resource.
Typical unstable-eval examples:
- using an organization-scoped key against project-scoped evaluator endpoints
- using a valid key that lacks the required access level for this resource
status-code: 403
type: PublicApiError
NotFoundError:
docs: The requested evaluator or evaluation rule does not exist within the authorized project.
status-code: 404
type: PublicApiError
MethodNotAllowedError:
docs: The HTTP method is not supported for this endpoint.
status-code: 405
type: PublicApiError
ConflictError:
docs: |
The request conflicts with existing state.
Typical unstable-eval examples:
- evaluator version changed during concurrent creation
- an evaluation rule name already exists in the project, so the caller should PATCH the existing resource instead
- enabling another evaluation rule would exceed the project limit of 50 active evaluation rules
- the underlying state changed between read and write operations, so the client should retry
Recovery guidance:
- for `name_conflict`, PATCH the existing evaluation rule instead of creating another one
- for the active-limit conflict, disable another active evaluation rule before enabling a new one
status-code: 409
type: PublicApiError
UnprocessableContentError:
docs: |
The request is syntactically valid, but Langfuse cannot accept it in its current form.
The most important unstable-eval case is `code=evaluator_preflight_failed`, which means the evaluator cannot currently run with the resolved model configuration.
Recovery guidance:
- configure the project's default evaluation model, or send a valid explicit evaluator `modelConfig`
- once the model configuration is valid, retry the same request
status-code: 422
type: PublicApiError
TooManyRequestsError:
docs: The project is rate limited. Retry after the interval described in the response headers and error details.
status-code: 429
type: PublicApiError
InternalServerError:
docs: An unexpected server-side error occurred.
status-code: 500
type: PublicApiError
@@ -0,0 +1,494 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/fern-api/fern/main/fern.schema.json
imports:
commons: ./commons.yml
errors: ./errors.yml
pagination: ../utils/pagination.yml
service:
auth: true
base-path: /api/public/unstable
endpoints:
create:
docs: |
Create an evaluation rule.
An evaluation rule defines **what** incoming data should be evaluated and **how prompt variables should be populated** from that data.
Use this resource after choosing an evaluator from the evaluator endpoints.
Key rules:
- `name` must be unique within the project for public evaluation rules
- `target` must be `observation` or `experiment`
- `evaluator.name` + `evaluator.scope` must identify an existing evaluator family returned by the evaluator endpoints
- Langfuse resolves that family to its latest version before saving the evaluation rule
- for `target=experiment`, use dataset `id` values from `GET /api/public/v2/datasets` when filtering by `datasetId`
- every evaluator prompt variable must be mapped exactly once
- `expected_output` mappings are only valid for `target=experiment`
- if `enabled=true`, Langfuse validates that the referenced evaluator can currently run
- at most 50 evaluation rules can be effectively active in one project at the same time
If an evaluation rule with the same `name` already exists in the project, the API returns `409`.
In that case, update the existing resource with `PATCH /api/public/unstable/evaluation-rules/{evaluationRuleId}` instead of creating a second one.
If enabling this resource would exceed the 50-active limit, the API also returns `409`.
In that case, disable or pause another active evaluation rule before enabling a new one.
Current scope:
- evaluation rules are live-ingestion rules only
- they do not trigger historical backfills
Recovery guidance:
- `400 invalid_filter_value`: fix the filter `column` or `value` using `details.column`, `details.invalidValues`, and `details.allowedValues`
- `400 invalid_filter_value` with `details.column=datasetId`: call `GET /api/public/v2/datasets`, then retry with dataset `id` values from that response
- `400 missing_variable_mapping`: fetch the evaluator again and make sure every variable in `variables` appears exactly once in `mapping`
- `400 duplicate_variable_mapping`: remove repeated mappings for the same variable
- `400 invalid_variable_mapping`: switch to a valid `source` for the selected `target`, or fix the variable name
- `400 invalid_json_path`: remove or correct the `jsonPath`
- `422 evaluator_preflight_failed`: the selected evaluator cannot run with the resolved model configuration. Fix the evaluator/default model setup, then retry the create request.
method: POST
path: /evaluation-rules
request: CreateEvaluationRuleRequest
response: EvaluationRule
errors:
- errors.BadRequestError
- errors.UnauthorizedError
- errors.AccessDeniedError
- errors.NotFoundError
- errors.ConflictError
- errors.MethodNotAllowedError
- errors.UnprocessableContentError
- errors.TooManyRequestsError
- errors.InternalServerError
examples:
- name: CreateObservationEvaluationRule
docs: Deploy an evaluator to score live generations only.
request:
name: answer-correctness-live
evaluator:
name: answer-correctness
scope: project
target: observation
enabled: true
sampling: 1
filter:
- type: stringOptions
column: type
operator: any of
value:
- GENERATION
mapping:
- variable: input
source: input
- variable: output
source: output
response:
body:
id: erule_123
name: answer-correctness-live
evaluator:
id: evaltmpl_123
name: answer-correctness
scope: project
target: observation
enabled: true
status: active
pausedReason: null
pausedMessage: null
sampling: 1
filter:
- type: stringOptions
column: type
operator: any of
value:
- GENERATION
mapping:
- variable: input
source: input
- variable: output
source: output
createdAt: "2026-03-30T09:20:00.000Z"
updatedAt: "2026-03-30T09:20:00.000Z"
- name: CreateExperimentEvaluationRule
docs: Deploy an evaluator to compare experiment outputs against expected outputs. Discover valid dataset IDs with `GET /api/public/v2/datasets` first.
request:
name: experiment-expected-output-match
evaluator:
name: expected-output-match
scope: project
target: experiment
enabled: true
sampling: 0.5
filter:
- type: stringOptions
column: datasetId
operator: any of
value:
- "550e8400-e29b-41d4-a716-446655440000"
mapping:
- variable: output
source: output
- variable: expected_output
source: expected_output
response:
body:
id: erule_456
name: experiment-expected-output-match
evaluator:
id: evaltmpl_456
name: expected-output-match
scope: project
target: experiment
enabled: true
status: active
pausedReason: null
pausedMessage: null
sampling: 0.5
filter:
- type: stringOptions
column: datasetId
operator: any of
value:
- "550e8400-e29b-41d4-a716-446655440000"
mapping:
- variable: output
source: output
- variable: expected_output
source: expected_output
createdAt: "2026-03-30T09:30:00.000Z"
updatedAt: "2026-03-30T09:30:00.000Z"
list:
docs: |
List evaluation rules in the authenticated project.
Each item describes one live evaluation rule and its effective runtime status.
method: GET
path: /evaluation-rules
request:
name: ListEvaluationRulesRequest
query-parameters:
page:
type: optional<integer>
docs: 1-based page number. Defaults to `1`.
limit:
type: optional<integer>
docs: Maximum number of items per page. Defaults to `50`.
response: EvaluationRules
errors:
- errors.BadRequestError
- errors.UnauthorizedError
- errors.AccessDeniedError
- errors.MethodNotAllowedError
- errors.TooManyRequestsError
- errors.InternalServerError
get:
docs: |
Get one evaluation rule by its identifier.
Use this endpoint to inspect the current evaluator, target, mapping, filters, and effective runtime status.
method: GET
path: /evaluation-rules/{evaluationRuleId}
path-parameters:
evaluationRuleId:
type: string
docs: Evaluation rule identifier returned by the evaluation rule endpoints.
response: EvaluationRule
errors:
- errors.BadRequestError
- errors.UnauthorizedError
- errors.AccessDeniedError
- errors.NotFoundError
- errors.MethodNotAllowedError
- errors.TooManyRequestsError
- errors.InternalServerError
update:
docs: |
Update an evaluation rule.
Typical uses:
- enable or disable live execution
- switch to another evaluator
- adjust sampling
- change filters
- update variable mappings
Important behavior:
- provide only the fields you want to change
- if you provide `evaluator`, Langfuse resolves that evaluator family to its latest version before saving
- changing `target`, `filter`, or `mapping` must still produce a valid target-specific configuration
- if you change `target`, also send a compatible `filter` and `mapping` in the same request unless the existing ones are still valid for the new target
- if the resulting config is enabled, Langfuse re-validates that the selected evaluator can run
- if the update would move a non-active evaluation rule into the active state and the project already has 50 active evaluation rules, the API returns `409`
Recovery guidance:
- if the update fails with `missing_variable_mapping` or `invalid_variable_mapping` after changing `evaluator` or `target`, resend the request with a complete new `mapping`
- if the update fails with `invalid_filter_value` after changing `target`, resend the request with a target-compatible `filter`
method: PATCH
path: /evaluation-rules/{evaluationRuleId}
path-parameters:
evaluationRuleId:
type: string
docs: Evaluation rule identifier.
request: UpdateEvaluationRuleRequest
response: EvaluationRule
errors:
- errors.BadRequestError
- errors.UnauthorizedError
- errors.AccessDeniedError
- errors.NotFoundError
- errors.MethodNotAllowedError
- errors.UnprocessableContentError
- errors.TooManyRequestsError
- errors.InternalServerError
delete:
docs: |
Delete an evaluation rule.
This removes the live-ingestion rule only. It does not delete the referenced evaluator.
method: DELETE
path: /evaluation-rules/{evaluationRuleId}
path-parameters:
evaluationRuleId:
type: string
docs: Evaluation rule identifier.
response: DeleteEvaluationRuleResponse
errors:
- errors.BadRequestError
- errors.UnauthorizedError
- errors.AccessDeniedError
- errors.NotFoundError
- errors.MethodNotAllowedError
- errors.TooManyRequestsError
- errors.InternalServerError
types:
EvaluationRule:
docs: |
Live evaluation rule for incoming data.
An evaluation rule answers:
- which evaluator should be used
- which target objects should trigger scoring
- how often scoring should run
- which target fields should populate each evaluator variable
- whether the deployment is active, inactive, or paused
Important status semantics:
- `enabled` is the desired on/off setting from the client
- `status` is the effective runtime state after Langfuse applies validation and blocking rules
- `enabled=true` with `status=paused` means the rule should run, but Langfuse has paused it until the underlying problem is fixed
properties:
id:
type: string
docs: Stable evaluation rule identifier.
name:
type: string
docs: Human-readable deployment name. This is independent from the evaluator name.
evaluator:
type: EvaluationRuleEvaluator
docs: |
Evaluator currently used by this rule.
`name` and `scope` identify the evaluator family conceptually.
`id` is the currently active evaluator version in that family.
If you create a newer project version with the same evaluator name later, existing evaluation rules are moved to it automatically.
target:
type: commons.EvaluationRuleTarget
docs: Target object type that should trigger scoring.
enabled:
type: boolean
docs: Desired enabled state configured by the client.
status:
type: commons.EvaluationRuleStatus
docs: Effective runtime status after Langfuse applies validation and blocking rules.
pausedReason:
type: nullable<string>
docs: Machine-readable reason when `status=paused`, otherwise `null`.
pausedMessage:
type: nullable<string>
docs: Human-readable explanation when `status=paused`, otherwise `null`.
sampling:
type: double
docs: |
Fraction of matching target objects that should be evaluated.
Must be greater than `0` and less than or equal to `1`.
- `1` means evaluate every matching target.
- `0.25` means evaluate approximately 25% of matching targets.
filter:
type: list<commons.EvaluationRuleFilter>
docs: List of filter conditions used to decide whether a target should be evaluated.
mapping:
type: list<commons.EvaluationRuleMapping>
docs: Variable mappings used to populate the evaluator prompt from the live target object.
createdAt:
type: datetime
docs: Timestamp when the evaluation rule was created.
updatedAt:
type: datetime
docs: Timestamp when the evaluation rule was last updated.
examples:
- value:
id: erule_123
name: answer-correctness-live
evaluator:
id: evaltmpl_123
name: answer-correctness
scope: project
target: observation
enabled: true
status: active
pausedReason: null
pausedMessage: null
sampling: 1
filter:
- type: stringOptions
column: type
operator: any of
value:
- GENERATION
mapping:
- variable: input
source: input
- variable: output
source: output
createdAt: "2026-03-30T09:20:00.000Z"
updatedAt: "2026-03-30T09:20:00.000Z"
EvaluationRules:
docs: Paginated list of evaluation rules.
properties:
data:
type: list<EvaluationRule>
docs: Evaluation rules in the current page.
meta:
type: pagination.MetaResponse
docs: Standard pagination metadata.
CreateEvaluationRuleRequest:
docs: |
Request body for creating an evaluation rule.
Checklist for agents and SDK clients:
- reference an existing evaluator family by `evaluator.name` and `evaluator.scope`
- choose `target=observation` or `target=experiment`
- if `target=experiment` and you want a dataset filter, call `GET /api/public/v2/datasets` first and use dataset `id` values in `filter[].value`
- fetch or inspect the evaluator first, then provide a complete variable mapping for every evaluator variable listed in `variables`
- optionally narrow execution with `filter`
- set `enabled=true` only when you want live execution immediately
properties:
name:
type: string
docs: Human-readable deployment name.
evaluator:
type: EvaluationRuleEvaluatorReference
docs: |
Evaluator family to use.
Use `name` and `scope` from the evaluator endpoints.
Langfuse resolves that family to its latest version before saving the rule.
target:
type: commons.EvaluationRuleTarget
docs: Target object type to evaluate.
enabled:
type: boolean
docs: Whether the deployment should be active immediately after creation.
sampling:
type: optional<double>
docs: Optional sampling fraction. Defaults to `1`.
filter:
type: optional<list<commons.EvaluationRuleFilter>>
docs: |
Optional filter list.
Omit or pass an empty list to evaluate all matching targets for the selected `target`.
Each filter object must use a column that is valid for that `target`.
For `target=experiment`, `column=datasetId` expects dataset `id` values from `GET /api/public/v2/datasets`, not dataset names.
mapping:
type: list<commons.EvaluationRuleMapping>
docs: |
Required variable mappings.
Every evaluator variable must appear exactly once.
Build this list from the evaluator `variables` array returned by the evaluator endpoints.
UpdateEvaluationRuleRequest:
docs: |
Partial update body for an evaluation rule.
Provide only the fields you want to change.
An empty body is rejected.
Practical guidance:
- If you only want to rename the rule or change sampling, send just those fields.
- If you change `evaluator`, send a fresh `mapping` unless you are certain the existing mapping still matches the evaluator variables.
- If you change `target`, usually send both `filter` and `mapping` in the same request.
- If you change an experiment `datasetId` filter, call `GET /api/public/v2/datasets` and use dataset `id` values from that response.
properties:
name:
type: optional<string>
docs: Updated deployment name.
evaluator:
type: optional<EvaluationRuleEvaluatorReference>
docs: |
Updated evaluator family.
Langfuse resolves the provided evaluator family to its latest version before saving the rule.
target:
type: optional<commons.EvaluationRuleTarget>
docs: Updated target object type.
enabled:
type: optional<boolean>
docs: Updated desired enabled state.
sampling:
type: optional<double>
docs: Updated sampling fraction.
filter:
type: optional<list<commons.EvaluationRuleFilter>>
docs: |
Updated filter list.
For `target=experiment`, `column=datasetId` expects dataset `id` values from `GET /api/public/v2/datasets`, not dataset names.
mapping:
type: optional<list<commons.EvaluationRuleMapping>>
docs: Updated variable mappings.
DeleteEvaluationRuleResponse:
docs: Confirmation response returned after successful deletion.
properties:
message:
type: string
docs: Always `Evaluation rule successfully deleted`.
EvaluationRuleEvaluatorReference:
docs: |
Evaluator family reference used when creating or updating an evaluation rule.
`name` and `scope` are enough to identify the evaluator family in the authenticated project context.
properties:
name:
type: string
docs: Evaluator family name.
scope:
type: commons.EvaluatorScope
docs: Whether the evaluator family is project-owned or Langfuse-managed.
EvaluationRuleEvaluator:
docs: |
Resolved evaluator currently used by the evaluation rule.
`id` is the exact active evaluator version.
`name` and `scope` identify the evaluator family conceptually.
properties:
id:
type: string
docs: Identifier of the exact evaluator version currently used by the rule.
name:
type: string
docs: Evaluator family name.
scope:
type: commons.EvaluatorScope
docs: Whether the evaluator family is project-owned or Langfuse-managed.
@@ -0,0 +1,250 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/fern-api/fern/main/fern.schema.json
imports:
commons: ./commons.yml
errors: ./errors.yml
pagination: ../utils/pagination.yml
service:
auth: true
base-path: /api/public/unstable
endpoints:
create:
docs: |
Create an evaluator in the authenticated project.
Use evaluators to define **how** Langfuse should score data: the prompt, the expected structured output, and the optional model configuration.
Naming behavior:
- If this is a new evaluator name in your project, Langfuse creates version `1`.
- If the name already exists in your project, Langfuse creates the next version and returns it.
- When a new project version is created, existing evaluation rules in that project automatically move to the newest version for that evaluator name.
Recommended workflow:
1. Create the evaluator.
2. Read the returned `variables` array.
3. Read the returned `outputDefinition.dataType` so the client knows whether future scores will be numeric, boolean, or categorical.
4. Create one or more evaluation rules that reference the returned evaluator family using `name` and `scope`.
Recovery guidance:
- `422` with `code=evaluator_preflight_failed`: the evaluator cannot run with the resolved model configuration. Add a valid explicit `modelConfig`, or configure the project's default evaluation model, then retry the same request.
- `400` with `code=invalid_body`: the request shape is malformed. Use the structured `details.issues` array to fix the specific fields and retry.
- `400` with `code=invalid_body` on `outputDefinition`: send `dataType`, `reasoning.description`, and `score.description`. Do not send `version`; it is not part of the public request shape.
Unstable API note:
- This surface may evolve while the underlying evaluation data model is being redesigned.
method: POST
path: /evaluators
request: CreateEvaluatorRequest
response: Evaluator
errors:
- errors.BadRequestError
- errors.UnauthorizedError
- errors.AccessDeniedError
- errors.MethodNotAllowedError
- errors.ConflictError
- errors.UnprocessableContentError
- errors.TooManyRequestsError
- errors.InternalServerError
examples:
- name: CreateEvaluatorVersion
docs: Create a new version of an evaluator named `answer-correctness`.
request:
name: answer-correctness
prompt: |
You are grading an answer.
Input:
{{input}}
Output:
{{output}}
Return a score between 0 and 1.
outputDefinition:
dataType: NUMERIC
reasoning:
description: Explain why the score was assigned.
score:
description: Correctness score between 0 and 1.
modelConfig:
provider: openai
model: gpt-4.1-mini
response:
body:
id: evaltmpl_123
name: answer-correctness
version: 2
scope: project
type: llm_as_judge
prompt: |
You are grading an answer.
Input:
{{input}}
Output:
{{output}}
Return a score between 0 and 1.
variables:
- input
- output
outputDefinition:
dataType: NUMERIC
reasoning:
description: Explain why the score was assigned.
score:
description: Correctness score between 0 and 1.
modelConfig:
provider: openai
model: gpt-4.1-mini
evaluationRuleCount: 0
createdAt: "2026-03-30T09:00:00.000Z"
updatedAt: "2026-03-30T09:00:00.000Z"
list:
docs: |
List the evaluators available to the authenticated project.
Important behavior:
- This endpoint returns the latest version of each available evaluator.
- Results can include evaluators from your project and Langfuse-managed evaluators.
- If the same evaluator name exists in both places, both are returned as separate items with different `scope` values.
method: GET
path: /evaluators
request:
name: ListEvaluatorsRequest
query-parameters:
page:
type: optional<integer>
docs: 1-based page number. Defaults to `1`.
limit:
type: optional<integer>
docs: Maximum number of items per page. Defaults to `50`.
response: Evaluators
errors:
- errors.BadRequestError
- errors.UnauthorizedError
- errors.AccessDeniedError
- errors.MethodNotAllowedError
- errors.TooManyRequestsError
- errors.InternalServerError
get:
docs: |
Get one evaluator by `id`.
Use this endpoint when you want the prompt, output definition, model configuration, and derived variables for the evaluator you plan to use in an evaluation rule.
method: GET
path: /evaluators/{evaluatorId}
path-parameters:
evaluatorId:
type: string
docs: Evaluator identifier returned by the evaluator endpoints.
response: Evaluator
errors:
- errors.BadRequestError
- errors.UnauthorizedError
- errors.AccessDeniedError
- errors.NotFoundError
- errors.MethodNotAllowedError
- errors.TooManyRequestsError
- errors.InternalServerError
types:
Evaluator:
docs: |
One evaluator that can be used for scoring.
An evaluator describes **how** to score data:
- prompt
- extracted prompt variables
- output schema
- optional explicit model configuration
It does not define **which** live objects are evaluated. That is the job of `evaluation-rules`.
For agent clients, the most important fields are:
- `variables`: use these exact names when building the evaluation-rule `mapping` array
- `outputDefinition`: tells you the expected score type and the evaluator's response instructions
- `modelConfig`: tells you whether the evaluator uses the project default model (`null`) or an explicit provider/model
Versioning behavior:
- `GET /evaluators` returns the latest version of each available evaluator.
- `GET /evaluators/{id}` can return an older version.
- Evaluation rules always run against the latest version for the selected evaluator name within the same source (`project` or `managed`).
properties:
id:
type: string
docs: Identifier of this evaluator.
name:
type: string
docs: Evaluator name.
version:
type: integer
docs: Version number of this evaluator.
scope:
type: commons.EvaluatorScope
docs: "Where this evaluator comes from: your project or Langfuse-managed defaults."
type:
type: commons.EvaluatorType
docs: Evaluator engine type. Currently always `llm_as_judge`.
prompt:
type: string
docs: Prompt template used during evaluation.
variables:
type: list<string>
docs: |
Variables extracted from the evaluator prompt.
Every variable in this list must be mapped exactly once when creating an evaluation rule.
outputDefinition:
type: commons.PublicEvaluatorOutputDefinition
docs: |
Structured output schema returned by this evaluator.
Responses always include `dataType` and omit the internal output-definition `version`.
Use `dataType` to decide how future scores should be interpreted.
modelConfig:
type: nullable<commons.EvaluatorModelConfig>
docs: Explicit model configuration, or `null` when the project default evaluation model is used.
evaluationRuleCount:
type: integer
docs: Number of evaluation rules in the project that currently use this evaluator version.
createdAt:
type: datetime
docs: Timestamp when this evaluator was created.
updatedAt:
type: datetime
docs: Timestamp when this evaluator was last updated.
Evaluators:
properties:
data:
type: list<Evaluator>
meta:
type: pagination.MetaResponse
CreateEvaluatorRequest:
docs: |
Request body for creating an evaluator.
If the same `name` already exists in your project, Langfuse creates the next version and returns it.
Existing evaluation rules in the same project are then moved to that new latest version automatically.
properties:
name:
type: string
docs: Evaluator name within the authenticated project.
prompt:
type: string
docs: Prompt template used by the evaluator.
outputDefinition:
type: commons.EvaluatorOutputDefinition
docs: |
Structured output schema the evaluator must return.
Always send `dataType`.
Do not send `version`; it is an internal storage detail and not part of the public request contract.
modelConfig:
type: optional<nullable<commons.EvaluatorModelConfig>>
docs: Optional explicit model configuration. Omit or set to `null` to use the project default evaluation model.
+6 -4
View File
@@ -1,6 +1,6 @@
{
"name": "langfuse",
"version": "3.167.4",
"version": "3.174.0",
"author": "engineering@langfuse.com",
"license": "MIT",
"private": true,
@@ -46,9 +46,9 @@
"braces": "3.0.3",
"dotenv-cli": "^7.4.4",
"husky": "^9.1.7",
"prettier": "^3.8.1",
"prettier": "^3.8.3",
"release-it": "^19.2.4",
"turbo": "2.9.5"
"turbo": "2.9.12"
},
"release-it": {
"git": {
@@ -107,9 +107,11 @@
"rollup@^4.0.0": "^4.22.4",
"@types/node-fetch": "^2.6.13",
"@types/react-dom": "19.2.3",
"fast-xml-builder": "1.1.7",
"glob": "^10.5.0",
"qs": "6.14.1",
"path-to-regexp@0.1.12": "0.1.13"
"path-to-regexp@0.1.12": "0.1.13",
"ip-address@10.1.0": "10.2.0"
},
"patchedDependencies": {
"next-auth@4.24.13": "patches/next-auth@4.24.13.patch"
+16
View File
@@ -3,6 +3,7 @@ import globals from "globals";
import tseslint from "typescript-eslint";
import turboConfig from "eslint-config-turbo/flat";
import eslintPluginPrettierRecommended from "eslint-plugin-prettier/recommended";
import langfusePlugin from "@repo/eslint-plugin";
import "eslint-plugin-only-warn";
export default tseslint.config(
@@ -59,6 +60,9 @@ export default tseslint.config(
},
languageOptions: {
parser: tseslint.parser,
parserOptions: {
projectService: true,
},
},
rules: {
"no-undef": "off", // TypeScript handles this
@@ -82,6 +86,18 @@ export default tseslint.config(
ignoreRestSiblings: true,
},
],
"@typescript-eslint/no-deprecated": "warn",
},
},
// Vitest in-source testing should only be used while developing, not in committed code.
{
name: "langfuse/no-in-source-vitest",
plugins: {
"@repo": langfusePlugin,
},
rules: {
"@repo/no-in-source-vitest": "warn",
},
},
);
+21
View File
@@ -2,6 +2,7 @@ import nextCoreWebVitals from "eslint-config-next/core-web-vitals";
import eslintPluginPrettierRecommended from "eslint-plugin-prettier/recommended";
import turboConfig from "eslint-config-turbo/flat";
import "eslint-plugin-only-warn";
import langfusePlugin from "@repo/eslint-plugin";
export default [
// Global ignores - include config files
@@ -62,6 +63,9 @@ export default [
name: "langfuse/next/typescript",
files: ["**/*.ts", "**/*.tsx"],
languageOptions: {
parserOptions: {
projectService: true,
},
globals: {
React: "readonly",
JSX: "readonly",
@@ -74,8 +78,12 @@ export default [
},
},
},
plugins: {
"@repo": langfusePlugin,
},
rules: {
"no-unused-vars": "off", // Use @typescript-eslint/no-unused-vars instead
"@repo/no-tailwind-overflow-scroll": "warn",
// Custom rules from old config
"@typescript-eslint/consistent-type-imports": [
"warn",
@@ -94,7 +102,20 @@ export default [
ignoreRestSiblings: true,
},
],
"@typescript-eslint/no-deprecated": "warn",
"react/jsx-key": ["error", { warnOnDuplicates: true }],
"react/no-unused-prop-types": "warn",
},
},
// Vitest in-source testing should only be used while developing, not in committed code.
{
name: "langfuse/no-in-source-vitest",
plugins: {
"@repo": langfusePlugin,
},
rules: {
"@repo/no-in-source-vitest": "warn",
},
},
];
+3 -2
View File
@@ -16,9 +16,10 @@
],
"dependencies": {
"@eslint/js": "^9.39.2",
"eslint-config-next": "16.2.3",
"@repo/eslint-plugin": "workspace:*",
"eslint-config-next": "16.2.6",
"eslint-config-prettier": "^10.1.8",
"eslint-config-turbo": "2.9.5",
"eslint-config-turbo": "2.9.12",
"eslint-plugin-only-warn": "^1.1.0",
"eslint-plugin-prettier": "^5.5.4",
"globals": "^16.0.0",
-1
View File
@@ -27,7 +27,6 @@
"isolatedModules": true,
"jsx": "preserve",
"types": [
"jest",
"node"
],
},
+21
View File
@@ -0,0 +1,21 @@
{
"name": "@repo/eslint-plugin",
"version": "0.0.0",
"license": "MIT",
"private": true,
"type": "module",
"main": "dist/index.js",
"scripts": {
"test": "vitest run",
"build": "tsc"
},
"devDependencies": {
"@repo/typescript-config": "workspace:*",
"@types/node": "^24.3.0",
"@typescript-eslint/parser": "^8.59.0",
"@typescript-eslint/rule-tester": "^8.59.0",
"@typescript-eslint/utils": "^8.59.0",
"typescript": "^5.9.2",
"vitest": "^4.1.4"
}
}
+11
View File
@@ -0,0 +1,11 @@
import { default as noTailwindOverflowScroll } from "./rules/no-tailwind-overflow-scroll.js";
import { default as noInSourceVitest } from "./rules/no-in-source-vitest.js";
export const plugin = {
rules: {
"no-in-source-vitest": noInSourceVitest,
"no-tailwind-overflow-scroll": noTailwindOverflowScroll,
},
};
export default plugin;
@@ -0,0 +1,36 @@
import { RuleTester } from "@typescript-eslint/rule-tester";
import * as typescriptEslintParser from "@typescript-eslint/parser";
import rule from "./no-in-source-vitest.js";
const ruleTester = new RuleTester({
languageOptions: {
parser: typescriptEslintParser,
parserOptions: {
ecmaVersion: 2022,
sourceType: "module",
ecmaFeatures: {
jsx: true,
},
},
},
});
ruleTester.run("no-in-source-vitest", rule, {
valid: [
{
code: `export const value = 42;`,
filename: "/repo/web/src/features/example/value.ts",
},
{
code: `if (import.meta.env.DEV) console.log("dev");`,
filename: "/repo/web/src/features/example/value.ts",
},
],
invalid: [
{
code: `if (import.meta.vitest) {}`,
filename: "/repo/web/src/features/example/value.ts",
errors: [{ messageId: "unexpected" }],
},
],
});
@@ -0,0 +1,29 @@
import { createRule } from "../util.js";
const rule = createRule({
name: "no-in-source-vitest",
meta: {
type: "suggestion",
docs: {
description:
"Vitest in-source testing should only be used while developing, not in committed code.",
},
messages: {
unexpected:
"Vitest in-source testing should only be used while developing, not in committed code.",
},
schema: [],
},
defaultOptions: [],
create(context) {
return {
'MemberExpression[computed=false][property.name="vitest"][object.type="MetaProperty"][object.meta.name="import"][object.property.name="meta"]'(
node,
) {
context.report({ node, messageId: "unexpected" });
},
};
},
});
export default rule;
@@ -0,0 +1,93 @@
import { RuleTester } from "@typescript-eslint/rule-tester";
import * as typescriptEslintParser from "@typescript-eslint/parser";
import { describe, expect, it, vi } from "vitest";
import rule from "./no-tailwind-overflow-scroll.js";
const ruleTester = new RuleTester({
languageOptions: {
parser: typescriptEslintParser,
parserOptions: {
ecmaFeatures: {
jsx: true,
},
},
},
});
ruleTester.run("no-tailwind-overflow-scroll", rule, {
valid: [
`<div className="button" />`,
`<div className="overflow-auto" />`,
`<div className="overflow-x-auto" />`,
`<div className="overflow-y-auto" />`,
`<div className="overflow-scrollbar" />`,
`const value = 42;`,
],
invalid: [
{
code: `<div className="overflow-scroll" />`,
errors: [{ messageId: "unexpected" }],
},
{
code: `<div className="overflow-y-scroll" />`,
errors: [{ messageId: "unexpected" }],
},
{
code: `<div className="overflow-x-scroll" />`,
errors: [{ messageId: "unexpected" }],
},
{
code: `<div className="flex h-full overflow-x-scroll" />`,
errors: [{ messageId: "unexpected" }],
},
{
code: `const className = "flex h-full overflow-y-scroll";`,
errors: [{ messageId: "unexpected" }],
},
{
code: `const className = cn("text-muted-foreground flex h-full overflow-x-scroll", className);`,
errors: [{ messageId: "unexpected" }],
},
{
code: `<div className="md:overflow-scroll" />`,
errors: [{ messageId: "unexpected" }],
},
{
code: `<div className={\`flex overflow-x-scroll\`} />`,
errors: [{ messageId: "unexpected" }],
},
{
code: `<div className="!overflow-scroll" />`,
errors: [{ messageId: "unexpected" }],
},
{
code: `<div className="overflow-scroll!" />`,
errors: [{ messageId: "unexpected" }],
},
{
code: `<div className="md:!overflow-scroll" />`,
errors: [{ messageId: "unexpected" }],
},
{
code: `<div className="md:overflow-scroll!" />`,
errors: [{ messageId: "unexpected" }],
},
],
});
describe("no-tailwind-overflow-scroll", () => {
it("ignores template elements with non-string raw values", () => {
const report = vi.fn();
const listeners = rule.create({
report,
} as never);
listeners.TemplateElement?.({
value: {
raw: null,
},
} as never);
expect(report).not.toHaveBeenCalled();
});
});
@@ -0,0 +1,75 @@
import { createRule } from "../util.js";
const FORBIDDEN_TAILWIND_UTILITIES = new Set([
"overflow-scroll",
"overflow-x-scroll",
"overflow-y-scroll",
]);
// Tailwind important modifiers can wrap a utility token in either position,
// for example `!overflow-scroll` or `overflow-scroll!`.
function normalizeTailwindToken(token: string): string {
return token.replace(/^!|!$/g, "");
}
// Yield normalized utility tokens, for example `md:overflow-scroll!` ->
// `overflow-scroll`.
function* extractTailwindUtilityTokens(value: string): Generator<string> {
for (const match of value.matchAll(/\S+/g)) {
const normalizedToken = normalizeTailwindToken(match[0]);
const variantSeparatorIndex = normalizedToken.lastIndexOf(":");
yield variantSeparatorIndex === -1
? normalizedToken
: normalizeTailwindToken(
normalizedToken.slice(variantSeparatorIndex + 1),
);
}
}
const rule = createRule({
name: "no-tailwind-overflow-scroll",
meta: {
type: "suggestion",
docs: {
description:
"Avoid Tailwind's forced scrollbars (`overflow-scroll`, `overflow-x-scroll`, `overflow-y-scroll`). Use `overflow-auto`, `overflow-x-auto`, `overflow-y-auto` instead to show scrollbars only when necessary.",
},
schema: [],
messages: {
unexpected:
"Avoid Tailwind's forced scrollbars (`overflow-scroll`, `overflow-x-scroll`, `overflow-y-scroll`). Use `overflow-auto`, `overflow-x-auto`, `overflow-y-auto` instead to show scrollbars only when necessary.",
},
},
defaultOptions: [],
create(context) {
return {
// Define AST listeners here
Literal(node) {
if (typeof node.value === "string") {
for (const candidateToken of extractTailwindUtilityTokens(
node.value,
)) {
if (FORBIDDEN_TAILWIND_UTILITIES.has(candidateToken)) {
context.report({ node, messageId: "unexpected" });
break;
}
}
}
},
TemplateElement(node) {
if (typeof node.value.raw === "string") {
for (const candidateToken of extractTailwindUtilityTokens(
node.value.raw,
)) {
if (FORBIDDEN_TAILWIND_UTILITIES.has(candidateToken)) {
context.report({ node, messageId: "unexpected" });
break;
}
}
}
},
};
},
});
export default rule;
+4
View File
@@ -0,0 +1,4 @@
import { ESLintUtils } from "@typescript-eslint/utils";
// There is no hosted documentation for our internal rules
export const createRule = ESLintUtils.RuleCreator((name) => `#${name}`);
+14
View File
@@ -0,0 +1,14 @@
{
"extends": "@repo/typescript-config/base",
"compilerOptions": {
"declaration": false,
"declarationMap": false,
"rootDir": "src",
"outDir": "dist",
"target": "ES2022",
"strictNullChecks": true,
"types": ["node", "vitest/globals"]
},
"exclude": ["node_modules"],
"include": ["src"]
}
+19
View File
@@ -0,0 +1,19 @@
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
setupFiles: ["./vitest.setup.ts"],
coverage: {
enabled: true,
provider: "v8",
include: ["src/**/*.ts"],
exclude: ["src/**/*.test.ts", "src/index.ts"],
thresholds: {
statements: 100,
branches: 100,
functions: 100,
lines: 100,
},
},
},
});
+8
View File
@@ -0,0 +1,8 @@
import { RuleTester } from "@typescript-eslint/rule-tester";
import { afterAll, describe, it } from "vitest";
// Required for RuleTester to work with Vitest
RuleTester.afterAll = afterAll;
RuleTester.it = it;
RuleTester.itOnly = it.only;
RuleTester.describe = describe;
+4
View File
@@ -25,11 +25,15 @@ Use root [AGENTS.md](../../AGENTS.md) for monorepo-level rules.
- Main exports: `src/index.ts`
- DB clients and types: `src/db.ts`
- Server exports: `src/server/index.ts`
- Server cache utilities: `src/server/cache/*`
- Domain model types: `src/domain/*`
- Repository layer: `src/server/repositories/*`
- Queue payload schemas: `src/server/queues.ts`
- Queue helpers: `src/server/redis/*`
- Postgres schema: `prisma/schema.prisma`
- For unstable public eval APIs, the public `evaluatorId` is currently the
exact `EvalTemplate.id`. Latest-version family grouping is derived from
`(projectId, name)` rather than stored on extra evaluator identity fields.
- Prisma migrations: `prisma/migrations/*`
- ClickHouse migrations: `clickhouse/migrations/{clustered,unclustered}/*`
- Seeder and support scripts: `scripts/seeder/*`, `clickhouse/scripts/*`
@@ -1,2 +1,2 @@
ALTER TABLE traces ON CLUSTER default ADD INDEX IF NOT EXISTS idx_session_id session_id TYPE bloom_filter() GRANULARITY 1;
ALTER TABLE traces ON CLUSTER default MATERIALIZE INDEX IF EXISTS idx_session_id;
ALTER TABLE traces ON CLUSTER default ADD INDEX IF NOT EXISTS idx_session_id session_id TYPE bloom_filter() GRANULARITY 1 SETTINGS alter_sync = 2;
ALTER TABLE traces ON CLUSTER default MATERIALIZE INDEX IF EXISTS idx_session_id SETTINGS mutations_sync = 2;
@@ -1,2 +1,2 @@
ALTER TABLE traces ON CLUSTER default ADD INDEX IF NOT EXISTS idx_user_id user_id TYPE bloom_filter() GRANULARITY 1;
ALTER TABLE traces ON CLUSTER default MATERIALIZE INDEX IF EXISTS idx_user_id;
ALTER TABLE traces ON CLUSTER default ADD INDEX IF NOT EXISTS idx_user_id user_id TYPE bloom_filter() GRANULARITY 1 SETTINGS alter_sync = 2;
ALTER TABLE traces ON CLUSTER default MATERIALIZE INDEX IF EXISTS idx_user_id SETTINGS mutations_sync = 2;
@@ -1,3 +1,3 @@
ALTER TABLE traces ON CLUSTER default DROP COLUMN IF EXISTS environment;
ALTER TABLE observations ON CLUSTER default DROP COLUMN IF EXISTS environment;
ALTER TABLE scores ON CLUSTER default DROP COLUMN IF EXISTS environment;
ALTER TABLE traces ON CLUSTER default DROP COLUMN IF EXISTS environment SETTINGS alter_sync = 2;
ALTER TABLE observations ON CLUSTER default DROP COLUMN IF EXISTS environment SETTINGS alter_sync = 2;
ALTER TABLE scores ON CLUSTER default DROP COLUMN IF EXISTS environment SETTINGS alter_sync = 2;
@@ -1,3 +1,3 @@
ALTER TABLE traces ON CLUSTER default ADD COLUMN environment LowCardinality(String) DEFAULT 'default' AFTER project_id;
ALTER TABLE observations ON CLUSTER default ADD COLUMN environment LowCardinality(String) DEFAULT 'default' AFTER project_id;
ALTER TABLE scores ON CLUSTER default ADD COLUMN environment LowCardinality(String) DEFAULT 'default' AFTER project_id;
ALTER TABLE traces ON CLUSTER default ADD COLUMN environment LowCardinality(String) DEFAULT 'default' AFTER project_id SETTINGS alter_sync = 2;
ALTER TABLE observations ON CLUSTER default ADD COLUMN environment LowCardinality(String) DEFAULT 'default' AFTER project_id SETTINGS alter_sync = 2;
ALTER TABLE scores ON CLUSTER default ADD COLUMN environment LowCardinality(String) DEFAULT 'default' AFTER project_id SETTINGS alter_sync = 2;
@@ -1,2 +1,2 @@
ALTER TABLE scores ON CLUSTER default ADD INDEX IF NOT EXISTS idx_project_trace_observation (project_id, trace_id, observation_id) TYPE bloom_filter(0.001) GRANULARITY 1 SETTINGS mutations_sync = 2;
ALTER TABLE scores ON CLUSTER default ADD INDEX IF NOT EXISTS idx_project_trace_observation (project_id, trace_id, observation_id) TYPE bloom_filter(0.001) GRANULARITY 1 SETTINGS alter_sync = 2;
ALTER TABLE scores ON CLUSTER default MATERIALIZE INDEX IF EXISTS idx_project_trace_observation SETTINGS mutations_sync = 2;
@@ -1,2 +1,2 @@
ALTER TABLE scores ON CLUSTER default ADD INDEX IF NOT EXISTS idx_project_trace_observation (project_id, trace_id, observation_id) TYPE bloom_filter(0.001) GRANULARITY 1 SETTINGS mutations_sync = 2;
ALTER TABLE scores ON CLUSTER default ADD INDEX IF NOT EXISTS idx_project_trace_observation (project_id, trace_id, observation_id) TYPE bloom_filter(0.001) GRANULARITY 1 SETTINGS alter_sync = 2;
ALTER TABLE scores ON CLUSTER default MATERIALIZE INDEX IF EXISTS idx_project_trace_observation SETTINGS mutations_sync = 2;
@@ -1,2 +1,2 @@
ALTER TABLE scores ON CLUSTER default ADD INDEX IF NOT EXISTS idx_project_session (project_id, session_id) TYPE bloom_filter(0.001) GRANULARITY 1 SETTINGS mutations_sync = 2;
ALTER TABLE scores ON CLUSTER default ADD INDEX IF NOT EXISTS idx_project_session (project_id, session_id) TYPE bloom_filter(0.001) GRANULARITY 1 SETTINGS alter_sync = 2;
ALTER TABLE scores ON CLUSTER default MATERIALIZE INDEX IF EXISTS idx_project_session SETTINGS mutations_sync = 2;
@@ -1,2 +1,2 @@
ALTER TABLE scores ON CLUSTER default ADD INDEX IF NOT EXISTS idx_project_dataset_run (project_id, dataset_run_id) TYPE bloom_filter(0.001) GRANULARITY 1 SETTINGS mutations_sync = 2;
ALTER TABLE scores ON CLUSTER default ADD INDEX IF NOT EXISTS idx_project_dataset_run (project_id, dataset_run_id) TYPE bloom_filter(0.001) GRANULARITY 1 SETTINGS alter_sync = 2;
ALTER TABLE scores ON CLUSTER default MATERIALIZE INDEX IF EXISTS idx_project_dataset_run SETTINGS mutations_sync = 2;
@@ -1,2 +1,2 @@
ALTER TABLE observations ON CLUSTER default DROP INDEX IF EXISTS idx_res_metadata_value;
ALTER TABLE observations ON CLUSTER default DROP INDEX IF EXISTS idx_res_metadata_key;
ALTER TABLE observations ON CLUSTER default DROP INDEX IF EXISTS idx_res_metadata_value SETTINGS alter_sync = 2;
ALTER TABLE observations ON CLUSTER default DROP INDEX IF EXISTS idx_res_metadata_key SETTINGS alter_sync = 2;
@@ -1,4 +1,4 @@
ALTER TABLE observations ON CLUSTER default ADD INDEX IF NOT EXISTS idx_res_metadata_key mapKeys(metadata) TYPE bloom_filter(0.01) GRANULARITY 1;
ALTER TABLE observations ON CLUSTER default ADD INDEX IF NOT EXISTS idx_res_metadata_value mapValues(metadata) TYPE bloom_filter(0.01) GRANULARITY 1;
ALTER TABLE observations ON CLUSTER default MATERIALIZE INDEX IF EXISTS idx_res_metadata_key;
ALTER TABLE observations ON CLUSTER default MATERIALIZE INDEX IF EXISTS idx_res_metadata_value;
ALTER TABLE observations ON CLUSTER default ADD INDEX IF NOT EXISTS idx_res_metadata_key mapKeys(metadata) TYPE bloom_filter(0.01) GRANULARITY 1 SETTINGS alter_sync = 2;
ALTER TABLE observations ON CLUSTER default ADD INDEX IF NOT EXISTS idx_res_metadata_value mapValues(metadata) TYPE bloom_filter(0.01) GRANULARITY 1 SETTINGS alter_sync = 2;
ALTER TABLE observations ON CLUSTER default MATERIALIZE INDEX IF EXISTS idx_res_metadata_key SETTINGS mutations_sync = 2;
ALTER TABLE observations ON CLUSTER default MATERIALIZE INDEX IF EXISTS idx_res_metadata_value SETTINGS mutations_sync = 2;
@@ -1,2 +1,2 @@
ALTER TABLE dataset_run_items_rmt ON CLUSTER default ADD INDEX IF NOT EXISTS idx_trace_id trace_id TYPE bloom_filter(0.001) GRANULARITY 1;
ALTER TABLE dataset_run_items_rmt ON CLUSTER default MATERIALIZE INDEX IF EXISTS idx_trace_id;
ALTER TABLE dataset_run_items_rmt ON CLUSTER default ADD INDEX IF NOT EXISTS idx_trace_id trace_id TYPE bloom_filter(0.001) GRANULARITY 1 SETTINGS alter_sync = 2;
ALTER TABLE dataset_run_items_rmt ON CLUSTER default MATERIALIZE INDEX IF EXISTS idx_trace_id SETTINGS mutations_sync = 2;
@@ -1,2 +1,2 @@
ALTER TABLE observations ON CLUSTER default DROP COLUMN IF EXISTS usage_pricing_tier_name;
ALTER TABLE observations ON CLUSTER default DROP COLUMN IF EXISTS usage_pricing_tier_id;
ALTER TABLE observations ON CLUSTER default DROP COLUMN IF EXISTS usage_pricing_tier_name SETTINGS alter_sync = 2;
ALTER TABLE observations ON CLUSTER default DROP COLUMN IF EXISTS usage_pricing_tier_id SETTINGS alter_sync = 2;
@@ -1,2 +1,2 @@
ALTER TABLE observations ON CLUSTER default ADD COLUMN usage_pricing_tier_id Nullable(String);
ALTER TABLE observations ON CLUSTER default ADD COLUMN usage_pricing_tier_name Nullable(String);
ALTER TABLE observations ON CLUSTER default ADD COLUMN usage_pricing_tier_id Nullable(String) SETTINGS alter_sync = 2;
ALTER TABLE observations ON CLUSTER default ADD COLUMN usage_pricing_tier_name Nullable(String) SETTINGS alter_sync = 2;

Some files were not shown because too many files have changed in this diff Show More