Compare commits

...
566 Commits
Author SHA1 Message Date
Ben BachemandGitHub 2834d618ca feat(agent): Add context about current user location (#14121) 2026-06-10 17:58:32 +00:00
3900a904f8 feat(monitors): added additional no-data resolution modes (#14171)
* feat(monitors): add no-data resolution modes

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

* feat(monitors): refine no-data modes and dropdown UI

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

* refactor(monitors): reference schema enums instead of string literals

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

* test(monitors): add triggerIds to base monitor service test input

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

* refactor(monitors): merge resolveNoDataSeverity into computeSeverity

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

* style(monitors): restyle no-data SEVERITY badge to slate

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

* fix(monitors): drop unreachable fallthrough in matches() to keep exhaustiveness guard

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 17:34:16 +00:00
Tobias WochingerandGitHub 0667150d73 ci: shorten pipeline critical path in tests-web jobs (#14138)
* ci: cut pipeline critical path in test and docker build jobs

The pipeline wall clock is gated by the slowest tests-web matrix job,
with test-docker-build second. All changes are measured on PR #14138.

tests-web (slowest matrix job 285s -> ~215s):
- vitest --maxWorkers=8: the server tests are I/O-bound, so extra
  workers overlap DB waits (run tests 76-144s -> 64-86s)
- start dev containers and the golang-migrate download in the
  background, overlapped by the build
- shim the clickhouse client to the binary already inside the dev
  container instead of downloading the ~300MB package (~9s)
- drop the Next.js build cache: ~30s/job restore+save for ~40s of
  build savings, while ~1GB churn per commit evicted caches that pay
- scope turbo cache keys by deploy mode (turbo hashes .env, which
  differs per mode, so only the save-race winner ever replayed) and
  prune entries older than three days to stop the archive snowballing

test-docker-build (~230s -> ~150s):
- healthcheck start_interval: containers are ready in seconds but the
  first probe fired only after the 30s interval (compose wait 45s->12s)
- start dependency containers during the image build
- skip the Next.js type check inside the smoke-test image build (the
  lint job type-checks already); release builds are unaffected

* test(web): run state-clean server tests without worker isolation

The web server suite spends more CI time importing the @langfuse/shared
module graph per test file than running tests (371s cumulative import
vs 264s tests). Split the server vitest project:

- "server" runs with isolate: false so each worker imports the module
  graph once; its per-file teardown no longer disconnects the shared
  redis/ClickHouse singletons (workers are reaped at run end)
- "server-isolated" keeps per-file isolation for files that touch
  process-global state: module mocks, spies, fake timers, process.env
  writes, and connection lifecycle calls (redis.disconnect,
  disconnectQueues). Classification is content-based at config load so
  new test files sort themselves.

Raise the teardown hook timeout to 30s: it imports the heavy shared
module graph, which can exceed the 10s default on a loaded runner.

CI effect on the gating tests-web jobs: run tests 64-86s -> 41-56s.

* ci: stop gating jobs on the duplicate-run check for non-push events

Every job waited for pre-job (runner pickup + check + dispatch), which
cost 45-100s of wall clock on every run. The duplicate-tree skip only
fires on push events in practice - 8 of the last 12 main-push runs
skipped (the merge queue had just tested the identical tree), while
pull_request and merge_group trees are almost always new.

pre-job therefore only runs for push events now; everywhere else it
resolves as skipped at run creation and jobs start immediately. The
llm-connections paths filter moves to its own non-gating job since the
filter is still needed for pull requests.

Behavior changes: merge_group runs no longer skip duplicate trees
(rare, ~1 in 10), and a pre-job API failure now fails open instead of
silently skipping all jobs.

* test(web): prebundle shared deps for the shared-context server project

vitest's ssr optimizer collapses the @langfuse/shared module graph into
prebundled chunks: cumulative import time drops 29.3s -> 0.8s locally.
Only enabled for the "server" project, whose files by definition do not
mock modules (module mocking is what prebundling can break).

* Revert "test(web): prebundle shared deps for the shared-context server project"

This reverts commit b58fefaa70c40a051949428c6c518995928a8211.

* ci: overlap setup downloads and container startup in e2e jobs

Apply the same treatment the tests-web jobs got: the golang-migrate
download, dev container startup, and (for browser e2e) the playwright
chromium install have no dependency on the build, so they run in the
background overlapped by it. The e2e jobs are co-critical with
tests-web (~170-210s) since jobs stopped waiting for pre-job.

* ci: address review findings on background-start observability and env classifier

- catch `delete process.env.X` and bracket-notation env assignments in
  the global-state classifier (future-proofing; no current file affected)
- surface the docker build smoke test's background dependency startup
  log and exit code instead of discarding them

* ci: fix background-start failure capture and turbo key prefix collision

Review findings from the Claude review:
- background subshells inherited the runner shell's errexit, so a
  failing command aborted the subshell before the exit-code marker was
  written and the waiting step burned its full timeout; set +e inside
  the capture subshells
- the default deploy-mode's turbo restore prefix (mode-) also matched
  the -azure and -redis-cluster keys; give the default mode an explicit
  "-default" segment

* ci: fail wait steps explicitly when background markers never appear

Review follow-up: the || true + exit "$(cat marker)" shape produced a
cryptic "numeric argument required" when the outer poll itself timed
out. Emit a real error with the captured log instead, and size the
budgets for what they actually wait on (image pulls are not covered by
compose --wait-timeout; the playwright install has no inner timeout).

* test(web): catch remaining state-mutating vitest APIs in the classifier

Review follow-up: vi.resetModules, vi.setSystemTime, and the unstub
helpers also mutate worker-global state; no current file in scope is
affected.

* test(web): extend dataset run deletion propagation window

The deletion travels through the worker queue and a ClickHouse
mutation; 4x30s polls all timed out on a loaded azure-mode runner.

* test(web): isolate files that mutate the t3-env singleton

Review finding: `(env as any).X = value` mutates the env object from
env.mjs, which does not proxy process.env and so bypassed the
classifier. Three in-scope files used it while running in the shared
context; they now route to server-isolated.
2026-06-10 18:24:25 +02:00
NimarandGitHub c6f9aff77c feat(trace): send web callouts from a trace & session (#13731) 2026-06-10 17:54:00 +02:00
Nimar 484e8358af chore: release v3.182.0 2026-06-10 17:35:08 +02:00
Nikita KabardinandGitHub 480028c60d feat(ui): unify keyboard shortcut keycaps (#14164)
* feat(ui): unify keyboard shortcut keycaps

* fix(ui): enforce keyboard shortcut content props
2026-06-10 14:32:07 +00:00
845137bf1b fix(fern/scores): split scores v3 back into scores-v3.yml to avoid breaking SDK groups (#14162)
* fix(fern/scores): move v3 scores definition back to scores-v3.yml

Promoting v3 to the canonical scores.yml (#14126) renamed the SDK's
scores group, which would force a major SDK release. Move the v3
definition back to its own file so it generates as a separate group.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(fern/scores): restore scores v2 as canonical scores.yml

Moves the v2 GET endpoints out of legacy/ back to the default scores
group so the generated SDK keeps its existing scores.* methods and type
names. Import paths adjusted for the new location. The deprecation
notes pointing to GET /api/public/v3/scores are kept.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(fern/scores): restore V3-suffixed identifiers in scores-v3.yml

Reverts the identifier renames from #14126 so generated names match the
pre-rename SDK surface: getMany → getManyV3, GetScoresRequest →
GetScoresV3Request, GetScoresResponse → GetScoresV3Response,
GetScoresMeta → GetScoresV3Meta, and all ScoreSubject* types →
ScoreSubject*V3. This also resolves the name collisions with the
restored v2 scores.yml, which declares GetScoresRequest and
GetScoresResponse.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* chore(generated): regenerate OpenAPI spec after scores file split

Schema names, operationIds, and SDK groups now match the pre-#14126
state (scores = v2, scoresV3 = v3); only documentation text differs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 13:34:23 +00:00
6aac9e72b3 fix(scim): stop PUT role downgrade and audit SCIM membership mutations (#14083)
* fix(scim): stop PUT role downgrade and audit membership mutations (INT-1249)

SCIM `active: true` (PUT and PATCH) is an idempotent "ensure provisioned"
signal, not a role assignment. The PUT handler previously upserted the
membership with `update: { role }`, where `role` defaulted to NONE whenever the
request omitted a parseable `roles` array. A periodic IdP full-sync (e.g. Okta)
that re-sends every user with `active: true` and no roles therefore overwrote
existing members' roles with NONE, silently stripping their permissions.

Changes:
- PUT/PATCH `active: true` now only create a membership (with the default NONE
  role) when one does not already exist, and make no change when it does. Any
  `roles` in the payload are ignored on PUT; roles are set via the create (POST)
  flow or the in-app UI. Concurrent provisioning races are treated as
  idempotent no-ops (P2002).
- Add audit_logs entries for SCIM membership mutations that actually change
  state: a `create` entry when provisioning adds a membership, and a `delete`
  entry when deprovisioning (PUT/PATCH active:false, DELETE) removes one. No-ops
  do not write audit entries. This closes INT-1249, where PUT/PATCH/DELETE
  membership changes bypassed the audit log entirely.

Adds regression tests asserting an existing member's role is preserved on
`active: true`, and that create/delete audit entries are written only on real
changes.

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

* fix(scim): honor explicit PUT roles, preserve role when roles omitted

Refine the PUT provisioning behaviour per review: an explicit `roles` value is
honoured again (the membership is created or updated to that role), and only a
request that omits `roles` leaves an existing membership untouched (creating a
default NONE membership when none exists). This keeps a roleless IdP full-sync
from resetting a member's role to NONE while still letting SCIM set roles
explicitly.

- Replace `provisionMembershipIfMissing` with `provisionMembership`, which takes
  an optional role: create-or-update to the role when provided, else create NONE
  if missing / no-op if present.
- Audit a `create` on provisioning and an `update` (with before/after) when an
  explicit role actually changes an existing membership; no audit on no-ops.
- PATCH still passes no role (PatchOp values only carry `active`).

Updates the regression tests accordingly: explicit role honoured on create and
on update (with update audit), and no role downgrade when `roles` is omitted.

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

* chore: test cleanup

* fix(scim): include ProjectMemberships in deprovision audit before payload

The SCIM deprovision path (PUT/PATCH active:false, DELETE) deletes the
OrganizationMembership, which cascade-deletes its ProjectMembership rows
(ProjectMembership.organizationMembership is onDelete: Cascade) without emitting
any separate audit entry. The delete audit's `before` is the only place those
rows can be preserved, but `deprovisionOrReject` fetched the membership without
the relation, so a compliance reviewer could not reconstruct which projects a
deprovisioned user had access to.

Add `include: { ProjectMemberships: true }` to the findUnique inside the
deprovision transaction, matching the tRPC deleteMembership audit entry
(membersRouter.ts). The role-update path is intentionally left as-is: it mirrors
the tRPC updateOrgRole path and does not delete project memberships, so they
remain queryable and need not be captured.

Adds a regression test asserting the cascaded project membership is gone from
the DB after a SCIM DELETE but recoverable from the audit before payload.

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

* fix(scim): guard last-OWNER demotion on PUT role update

Honoring an explicit role on PUT (commit acf4bf2) reintroduced a path that could
demote the sole OWNER of an organization: applyToExisting updated the role with
no owner-count check, so `PUT /Users/{ownerId}` with `roles:['MEMBER']` against
the last OWNER left the org with zero owners and unmanageable from the UI. Every
sibling path guards this (deprovisionOrReject, tRPC updateOrgMembership) — the
PUT role-update path did not.

Wrap the role update in a Serializable transaction that, when demoting an OWNER
(existing OWNER → non-OWNER), counts remaining OWNERs and rejects with 403 and
the same detail string as the deprovision guard when none would remain. Two
concurrent demotions cannot both pass (serialization failures surface as 409 for
client retry). provisionMembership now returns null when it has already written
a rejection response, and the PUT/PATCH handlers short-circuit on it.

Adds last-OWNER tests for the PUT role-update vector: rejects demoting the only
OWNER (403), and allows demotion when another OWNER remains.

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

* fix(scim): read membership inside the role-update transaction

The last-OWNER guard in provisionMembership decided "is this a demotion" from a
membership row read OUTSIDE the Serializable transaction. Postgres SSI only
protects reads made within the transaction, so a stale read could skip the
guard: with X read as MEMBER, a concurrent promotion of X to OWNER plus a
demotion of the previous OWNER could interleave such that the unguarded update
left the org with zero owners. The same stale read meant a concurrent
deprovision between read and update surfaced as an unhandled P2025 → 500.

Re-read the membership inside the transaction and base the no-op check, the
OWNER guard, and the update on that snapshot. When the row vanished before the
transaction (concurrent deprovision), fall through to the create path instead
of erroring, preserving the "ensure provisioned" semantics. The P2002
concurrent-create fallback now reuses the same in-transaction path.

No behavioral change for sequential requests; audit writes stay post-commit per
the established pattern.

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

* chore: typo

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 13:09:38 +00:00
e5c1a451c5 fix(api): clarify v3 scores field groups and move v4 removal note to v2 endpoints (#14161)
- Document which response fields each v3 scores field group adds (core
  fields always returned; details, subject, annotation additive). The
  "core" token remains accepted by the schema for compatibility but is
  not documented, since passing it has no effect
- Replace the incorrect "requires Langfuse v4" note on the v3 endpoint
  with removal notices on the deprecated v2 GET endpoints, including the
  id-filter migration path for get-by-id
- Regenerate OpenAPI spec (docs-only; Python/TS SDK outputs unchanged)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 12:51:00 +00:00
d955c5eb47 fix(monitors): coerce string-typed metric values in live preview chart (#14159)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 12:18:41 +00:00
1abd0f57ae feat(traces): Add trace delete button to events table (#14079)
* feat(traces): Add trace delete button to events table

* Update web/src/features/events/components/EventsTable.tsx

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

---------

Co-authored-by: Nimar <l.nimar.b@gmail.com>
2026-06-10 11:29:02 +00:00
996e111253 fix(monitors): correct Slack alert formatting (#14132)
* fix(monitors): correct Slack alert formatting

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

* test(monitors): update slack-processor tests for attachment fallback

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

* refactor(monitors): extract buildMonitorAlertSlackMessage into shared

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

* fix(worker): correct 'scaleable' typo in webhooks comment

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

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 09:51:31 +00:00
Tobias WochingerandGitHub 88f7b0a829 chore: update license copyright year to include 2026 (#14151) 2026-06-10 09:51:00 +00:00
Hassieb PakzadandGitHub d96b1cc5a2 fix(evals): scope observation eval jobs by trace (#14124) 2026-06-10 11:49:45 +02:00
Ben BachemandGitHub 5283301c89 fix(agent): Recent conversations styling issues (#14148) 2026-06-10 09:19:22 +00:00
Hassieb PakzadandGitHub 7c7853a4a1 fix(media): harden media database writes (#14137) 2026-06-10 11:18:11 +02:00
Steffen SchmitzandGitHub 8031619e67 chore: remove unnecessary score log line (#14147)
* chore: remove unnecessary score log line

* chore: remove prompt logging

* chore: lint

* chore: lint
2026-06-10 09:13:03 +00:00
6c5f0aa81d feat(mcp): expose unstable evaluator and evaluation-rule tools (#14113)
* feat(mcp): expose unstable evaluator and evaluation-rule tools

Add an `evals` MCP feature with eight tools wrapping the unstable public
evaluator and evaluation-rule APIs: list/get/create evaluators and
list/get/create/update/delete evaluation rules.

Tool runtime validation reuses the public-API contract schemas directly;
the discovery-facing base schemas flatten the contract's union schemas
(evaluator type, rule target, output definition, filters) where the MCP
layer forbids union JSON schemas. Reads are exposed to in-app agent keys;
mutating tools are marked destructive.

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

* test(mcp): cover code evaluators; clarify mapping source description

Address PR review: broaden RuleMappingBaseSchema description to surface
experiment-only sources (expected_output, experiment_item_metadata), and
add code-evaluator coverage — a code evaluator + managed-mapping rule plus
a check that supplying a mapping on a code rule is rejected.

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

* refactor(mcp): share advanced-filter base schema

scores, metrics, and evaluation-rule tools each defined an identical loose
{column, operator, value, type, key?} filter base for MCP discovery. Extract
it to core/filter-schema.ts (McpAdvancedFilterBaseSchema) and reuse it.
listObservations keeps its narrower variant (type inferred from the column).

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

* refactor(evals): centralize public eval audit logging

* refactor(mcp): rename createEvaluator to upsertEvaluator; consolidate eval tests

- Rename the createEvaluator MCP tool to upsertEvaluator: creating with an
  existing name versions the evaluator and migrates its rules, so it is not a
  plain create (mirrors the upsertDataset convention).
- Move eval tool coverage into the shared mcp-tools-{read,write} servertests and
  mock the evaluator preflight instead of provisioning a default eval model.
- Keep one happy path per union; drop cross-project and redundant negative tests.

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 09:11:00 +00:00
Nikita Kabardin 18500296fe chore: release v3.181.0 2026-06-10 10:51:50 +02:00
cfcdc838c0 fix(ui): sessions view gets into infinite rerendering loop while translated with google translate (#14036)
* fix(sessions): stabilize virtualized session rows

* refactor(sessions): extract session JSON download action

* fix(annotation-queues): restore session item type contract

* fix(sessions): clear stale virtual row height

* refactor(sessions): isolate virtual row measurement state

* fix(sessions): release stale virtual row height freeze

* fix(session): lazy initialize virtual row measurement state

* fix(session): small polishments to sessions virtualization

* fix(session): keep virtual row freeze active during oscillation

* fix(session): keep virtual row freeze active after scroll

---------

Co-authored-by: Nimar <l.nimar.b@gmail.com>
2026-06-10 08:37:21 +00:00
Ben BachemandGitHub ad480f4040 chore: Allow skipping lint in pre-commit hook (#14118) 2026-06-10 08:14:07 +00:00
Steffen SchmitzandGitHub b152638ec1 chore: move MCP info logs to debug (#14146) 2026-06-10 08:00:06 +00:00
Hassieb PakzadandGitHub 823740664f feat(models): add Claude Fable 5 and Mythos 5 (#14136) 2026-06-09 22:41:18 +02:00
Ben BachemandGitHub 31344c2ece refactor(agent): Move in-app agent feature to EE (#14127)
* refactor(agent): Move in-app agent feature to EE

* feat(agent): Introduce in-app-agent entitlement
2026-06-09 16:30:37 +00:00
Nikita KabardinandGitHub 267dd829d1 fix(ui): improve dialog overlays and command menu motion (#14129) 2026-06-09 15:59:57 +00:00
Niklas Semmler ccd2e93e3f chore: release v3.180.0 2026-06-09 18:04:02 +02:00
Nikita KabardinandGitHub d1c90ed117 docs: preserve README image aspect ratios (#14131) 2026-06-09 15:53:37 +00:00
d60e520707 feat(fern/scores): demote scores v2 to legacy, promote v3 to canonical Scores section (#14126)
* feat(fern/scores): move v2 GET endpoints to legacy/scores-v2.yml

Copies the scores v2 service definition into the legacy/ directory to
reflect that it is superseded by v3. Import paths adjusted for the new
location (../utils/pagination.yml, ../commons.yml).

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

* feat(fern/scores): mark legacy scores v2 endpoints as deprecated

Points consumers to GET /api/public/v3/scores in the endpoint docs.

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

* feat(fern/scores): promote scores-v3.yml to canonical scores.yml

Replaces scores.yml (v2 GET endpoints, now in legacy/) with the v3
definition. Deletes the superseded scores-v3.yml.

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

* feat(fern/scores): rename V3-suffixed identifiers in canonical scores.yml

Now that scores.yml is the primary definition, drop the V3 suffix from
the endpoint (getManyV3 → getMany), the request/response wrappers
(GetScoresV3Request → GetScoresRequest, GetScoresV3Response →
GetScoresResponse, GetScoresV3Meta → GetScoresMeta), and all ScoreSubject
types (ScoreSubjectV3 → ScoreSubject, etc.).

The concrete score model types (ScoreV3, NumericScoreV3, etc.) keep their
V3 suffix because commons.yml already declares Score, NumericScore, etc.
for the v1/v2 API surface — Fern uses a flat global namespace so those
names are taken.

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

* chore(generated): regenerate OpenAPI spec after scores section rename

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

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 15:31:27 +00:00
Tobias WochingerandGitHub 76d737114a fix(evals): preserve unsafe dataset integers (#14119)
* fix(evals): preserve unsafe dataset integers

Preserve unsafe JSON number literals as strings when normalizing dataset items, extracting eval JSON selectors, and importing CSV dataset values.

* fix(datasets): preserve unsafe CSV decimals
2026-06-09 14:40:44 +00:00
NimarandGitHub ec2138d5c2 fix(playground): parse pipecat tools (#14120) 2026-06-09 16:47:25 +02:00
a9678ad9c1 feat(monitors): metric alerting - scheduler, processor, alert dispatch, and management ui (#13874)
* feat(automations): narrow getAutomations by eventSource and matches

* feat(monitors): list page

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

* feat(monitors): create and edit monitors

* feat(monitors): prefill create-automation form from monitor draft

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

* feat(monitors): polish create/edit form UX

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

* feat(monitors): toggle automations from the monitor form

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

* refactor(automations): drop unused matches arg from getAutomations

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

* feat(monitors): gate page entry on feature flag and rbac scope

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

* fix(monitors): highlight automations matching an untagged monitor

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

* refactor(monitors): apply style guide to existing components

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

* feat(automations): gate Monitor event source on monitors flag

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

* fix(monitors): point breadcrumb at the monitors list, not the project root

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

* refactor: tighten jsdoc and inline comments per style guide

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

* refactor(monitors): extract updateSeverityForStatus and lock in transition tests

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

* refactor(monitors): move FilterState mapping out of the service layer

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

* refactor(monitors): introduce getValidMonitorAggregationsForMeasure and structural isValidThresholdOrder

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

* refactor(query): drop formatAggregation, reuse widget form startCase mapping

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

* refactor(tag): make TagManager a controlled component and rename alignPopover

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

* fix(monitors): make none-of automation rows inert in the panel

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

* fix(monitors): ignore stale form status on save so pause/resume sticks

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

* fix(automations): UTF-8-safe prefill encoder so non-ASCII tags don't throw

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

* fix(query): drop aggs.agg pin from getValidAggregationsForMeasure, keep it in monitors wrapper

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

* refactor(monitors): typed ListMonitorFilter as a singleFilter[] subset

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

* refactor(monitors): fold projectId into toPrismaWhere and let updateSeverityForStatus take an optional current

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

* fix(mcp): use the new getValidAggregationsForMeasure

* refactor(query): restore getValidAggregationsForMeasureType(measureType)

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

* refactor(widgets): restore WidgetForm.tsx from main

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

* refactor(tag): trim TagManager diff to additive triggerButton/alignPopover and reintroduce mutate-on-close with a downward initialTags sync

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

* feat(monitors): gate monitors to Langfuse Cloud deployments only

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

* feat(monitors): add MonitorScheduler with deterministic next_run_at and run_at queue payload

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

* test(monitors): scaffold MonitorScheduler integration test with table-driven runner

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

* fix(monitors): treat NULL last_completed_run_at as never-completed in runIsPending

The runIsPending negation introduced a three-valued-logic bug: when
last_published_run_at is set but last_completed_run_at is NULL (worker has
the first job but hasn't finished), last_completed < last_published is NULL,
the AND chain short-circuits to NULL, and the CASE stamps a duplicate run.
Treating NULL as "never completed" closes the gap.

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

* test(monitors): cover advance/publish branches for MonitorScheduler

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

* test(monitors): cover next_run_at determinism and cadence-aligned boundary math

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

* refactor(monitors): lowerCamelCase scheduler test constants, group by role

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

* fix(monitors): typos in scheduler SQL comments

* fix(monitors): clear lifecycle stamps when MonitorService.update changes schedulerBatchId

When the query shape changes (filters/view/window), the previous publish's
lifecycle stamps refer to the OLD batch. Leaving them would suppress the next
publish via runIsPending for up to monitorProcessorTtl. Cosmetic edits
(name/tags/threshold) preserve the stamps so an in-flight worker on the same
batch still dedups.

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

* feat(monitors): add monitors.last_claimed_run_at lifecycle column

MonitorProcessor.claim writes this when a worker takes ownership of a
published run. The CAS in claim uses last_published_run_at as the TTL
anchor; this column is the per-run claim discriminator so duplicate
BullMQ deliveries become no-ops.

* feat(monitors): add MonitorProcessor.claim with TTL CAS

Replaces the Redis lock at step 1 of the RFC §Monitor Queue Processor.
The claim CAS uses three guards: last_published_run_at = event.runAt
rejects stale republishes, last_completed_run_at < last_published_run_at
rejects BullMQ replays of completed runs, and a TTL branch on
last_claimed_run_at allows reclaim after a dead worker. TTL constant is
shared with the scheduler's runIsPending gate.

Servertest covers the three success branches (fresh, prior-run, TTL),
five denial branches (in-flight within TTL, TTL boundary exact,
completed, stale event, never-published), two multi-monitor batches,
empty/missing inputs, and projectId mismatch.

* feat(monitors): add computeSeverity threshold check

Pure mapping of (metric value, operator, alert/warning thresholds) to
NO_DATA / OK / WARNING / ALERT. Alert is checked before warning so the
more-severe outcome wins when both match (relevant for NEQ overlaps).

Table-driven test covers null values, every operator branch, both
boundary cases (strict GT/LT do not match at threshold; GTE/LTE do),
and missing-warning-band fallthrough.

* feat(monitors): add applyStateMachine for severity transitions

Encodes the RFC §Severity State Machine table: given a transition from
prev to computed severity plus noData/renotify config, returns the emit
flag and the next lifecycle stamps (severity, severityChangedAt,
alertedAt).

Notable interpretations:
- The RFC text writes the cooldown gate as `alertedAt + interval >
  scheduledAt`, which inverts the natural "wait long enough" intent;
  implemented here as `scheduledAt - prevAlertedAt >= interval`.
- NULL prevAlertedAt on a noData escalation fires immediately (no prior
  emit to cool down from); on a renotify self-loop it stays silent
  (renotify needs a baseline to re-emit from).

28-case table-driven test covers every row of the RFC table plus the
cooldown / NULL edges and the OK-self-loop renotify exemption.

* feat(monitors): add MonitorProcessor.complete bulk lifecycle UPDATE

Lands the post-evaluation lifecycle stamps for an entire batch in one
statement via a VALUES-join: last_completed_run_at, severity,
severity_changed_at, alerted_at. The caller passes pre-computed values
from applyStateMachine; complete just writes them.

Servertest covers seven row-shape cases (no-change, severity-only,
emit-only, both, multi-monitor mixed, empty no-op, missing-id ignored)
and a project-scoping case proving a completion against a row in
another project leaves it untouched.

* feat(monitors): MonitorProcessor.process shell — claim, CH query, state machine, complete

Wires the four pure units (claim, computeSeverity, applyStateMachine,
complete) to real I/O on the project's ClickHouse and Postgres. Adds a
MonitorPublisher seam to the constructor as a placeholder; the
trigger-filter and publisher-emit branches land in the follow-up commit.

Per RFC step 9, the publish-before-commit ordering will go between the
per-row state machine loop and the complete() call.

Servertest covers four orchestration shapes:
- empty claim (event with no monitors) → no CH query, no row write
- OK → OK steady state (CH count 50, threshold 100) → only last_completed_run_at advances
- cold-start UNKNOWN → ALERT (CH count 142) → state machine emits, alertedAt advances
- partial claim (1 of 2 already completed) → only the claimable row updates

NOTE: NO_DATA semantics replaced with cold-start ALERT in case E because
count() returns 0 (not NULL) on empty ClickHouse results. NO_DATA returns
as a follow-up once we wire a NULL-returning aggregation.

* feat(monitors): MonitorProcessor.process emits alerts via publisher

Wires the trigger-filter and publisher-emit branches of process().
Builds a MonitorAlert per state-machine emit, projects (severity, tags,
monitorId, monitorName) into the trigger-filter shape, calls publisher
once per surviving monitor (RFC step 9: not once per matching trigger),
then commits.

Alert message body distinguishes the kind of transition:
- NO_DATA              → "<agg>(<view>.<measure>) has no data over the last <window>"
- NO_DATA → OK         → "<agg>(<view>.<measure>) has data again"
- WARN/ALERT/NO_DATA → OK → "<agg>(<view>.<measure>) is back within threshold"
- WARN/ALERT (any other transition or self-loop renotify)
                       → "<agg>(<view>.<measure>) is <above/below/...> <threshold>"

Servertest covers four publisher-side cases:
- ALERT + matching severity trigger → publish called once with the expected payload shape
- ALERT but trigger filter matches WARNING → publish NOT called; row still written per state machine
- tags-based trigger filter matches → publish called once
- multiple triggers, one matches → publish called ONCE (per-monitor, not per-trigger)

* test(monitors): rewrite MonitorProcessor.process servertest as branch table; inject CH+trigger seams

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

* refactor(monitors): rename lifecycle columns to wallclock semantics

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

* feat(monitors): scheduler rescue branch + wallclock publish stamps

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

* feat(monitors): add publishedAt wallclock to MonitorQueueEvent

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

* feat(monitors): processor claim CAS keyed on event.publishedAt; complete CAS owner key

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

* feat(monitors): wire MonitorScheduler + MonitorProcessor into BullMQ

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

* test(monitors): add publishedAt to MonitorQueueEvent type fixtures

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

* fix(monitors): PAUSED guard in state machine + accept path-only permalink

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

* refactor(monitors): replace BullMQ scheduler queue with PeriodicExclusiveRunner shards

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

* refactor(monitors): rename MonitorProcessorQueue→MonitorQueue, MonitorSchedulerRunner→MonitorRunner; hoist scheduler into constructor; fail-fast on missing queue

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

* fix(monitors): replace stalledInterval:0 with maxStalledCount:0 on MonitorQueue worker

BullMQ 5 rejects stalledInterval:0; use maxStalledCount:0 to achieve the same intent (no stalled-job redelivery) without violating the constraint.

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

* fix(monitors): emit wire shape from scheduler to keep BullMQ JSON-safe

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

* feat(automations): allow monitor in AvailableWebhookApiSchema

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

* feat(monitors): MonitorWebhookQueueEventSchema gains id+timestamp; rename version→apiVersion

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

* refactor(webhooks): WebhookOutboundEnvelopeSchema discriminates on type

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

* feat(automations): expose automations[] on TriggerDomainWithActions

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

* feat(monitors): processor fans alerts out to WebhookQueue envelopes

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

* chore(worker): add slackify-markdown dependency

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

* feat(slack): SlackMessageParams accepts optional attachments

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

* feat(slack): buildMonitorMessage with severity emoji + color attachment

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

* feat(webhooks): dispatch monitor-alert envelopes with redis-backed auto-disable

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

* feat(monitors): publish MonitorAlerts to WebhookQueue

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

* test(monitors): e2e scheduler → dispatcher → webhook URL

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

* feat(monitors): add triggerIds column

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

* feat(monitors): add triggerIds to MonitorSchema

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

* fix(monitors): backfill triggerIds in test fixture after schema change

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

* feat(automations): inject synthetic triggerIds clause

* feat(monitors): persist triggerIds on create and update

* refactor(automations): drop filter UI from monitor-source trigger form

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

* fix(automations): align monitor trigger eventActions with plan (created/updated/deleted)

* refactor(monitors): switch automations panel from tag-matching to triggerIds

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

* feat(monitors): wire triggerIds through MonitorForm; move TagManager under name

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

* revert(test-utils): unrelated stash-pop content accidentally bundled with monitor work

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

* refactor(monitors): restore automations-panel card UX (drop tag pills) and trim TagManager chrome

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

* refactor(monitors): polish trigger UI - info card, no event actions, hide execution history

To achieve this we:
- Replaced the monitor-source eventAction picker with an Alert info card linking to the create-monitors page; the picker (with created/updated/deleted options) and its setValue side-effects are gone.
- Hid the Execution History tab for monitor-source automations on the detail page; the form renders directly with no TabsBar.
- Dropped the actionType parameter from the automations create deep-link and collapsed the type-specific dropdown items into a single "New automation" link.
- Skipped name auto-fill on create and added autoFocus to the name input so the cursor lands there when the form opens.
- Bumped per-row padding on the automations panel from px-2 py-1 to p-2.

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

* revert(monitors): restore per-action-type entries in automations dropdown

Restores the dropdown menu with Webhook / Slack / GitHub Dispatch entries on top of the generic "New automation" link, and re-adds the actionType arg to the create deep-link prefill. Walks back the over-broad collapse from ca7060610 - the dropdown UX was the right shape; only the tag-related prefill needed stripping.

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

* fix(monitors): normalize filter columns in resolver so view validation passes

The InlineFilterBuilder writes UI-table display labels (e.g. "Environment") into form state, while CreateMonitorSchema.validateMonitorQuery validates against view-space dimension keys (e.g. "environment"). With mode: "onChange" the validator was rejecting valid filters before the submit handler could map them.

To achieve this we:
- Wrapped zodResolver so it maps filters via mapWidgetUiTableFilterToView before delegating to the schema; form state itself stays in UI-table-space so the builder still renders display labels.
- Added the inline "Send Alerts to Slack, Webhooks, and GitHub Actions." explainer under the Automations heading.

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

* feat(entitlements): add monitor-count limit at 10 across all plans

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

* test(monitors): cover monitor-count limit enforcement and count query

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

* feat(monitors): gate create at monitor-count limit and expose org-wide count

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

* feat(monitors): show at-limit state on the New monitor action button

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

* feat(monitors): add onboarding splash for empty monitors list

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

* feat(automations): support redirectUrl in create-form deep links

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

* fix(automations): fire pending redirect when secret dialog is dismissed via X or Escape

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

* refactor(automations): consolidate post-flow redirect into a single finishFlow helper

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

* feat(monitors): shift CH evaluation window back by 30s for ingestion lag

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

* refactor(monitors): narrow isValidQuery to a disallow-list

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

* fix(automations): scope triggerIds clause to monitors; preserve triggerIds on toolbar pause

- matchesTriggerFilter only appends the synthetic `triggerIds any-of [id]`
  clause for monitor-source triggers. Prompt-source events don't carry a
  triggerIds field, so the unconditional clause broke every existing prompt
  automation in the worker's promptVersionProcessor.
- EditMonitorPage's toolbar Pause/Resume payload was missing `triggerIds`;
  zod's `.default([])` filled in `[]` server-side and the service wrote it,
  silently wiping linked automations on every toggle.

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

* refactor(automations): gate synthetic triggerIds clause on data presence, not source enum

Drops the eventSource conditional in matchesTriggerFilter. The clause now
appends when the event data carries a triggerIds array, which is the actual
opt-in signal — monitor events publish the field, prompt-version events do
not. New sources opt in by including the field, no matcher change required.

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

* fix(monitors): 0-indexed list pagination, redirect URL on add-automation, EQ threshold rendering, empty-state flash

- MonitorService.list was 1-indexed (`(page - 1) * limit`) while every other
  router and the MonitorsTable call site send a 0-indexed page. Aligns the
  service with the codebase convention and updates the servertest.
- MonitorAutomationsPanel's "+ Automation" dropdown now threads
  `router.asPath` into `automationCreateHref` so saving an automation returns
  the user to the monitor form they were editing instead of dumping them on
  the automations list.
- ThresholdOverlay adds an EQ arm (thin band centered on the value, mirroring
  NEQ) and floors `bandEpsilon` at 1 so a `threshold.value === 0` while data
  loads doesn't collapse the band to zero height.
- MonitorAutomationsPanel gates the empty splash on
  `automations.isSuccess && data.length === 0` so loading and error states no
  longer flash "Set up automations" over a monitor that already has them.

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

* fix(monitors): hold list-page render until hasAny resolves to drop empty-state flicker

When the project has no monitors, the page was rendering the table branch
(loading skeleton) while monitors.hasAny was in flight, then swapping to the
onboarding splash. Now gates the splash-vs-table choice on hasAny.isSuccess
and renders a minimal page chrome during the loading window so the table
never mounts on the empty path.

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

* fix(monitors): MonitorsTable pageIndex+1 for 1-indexed service, extract toPrismaOrderBy mapper

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

* fix(monitors): display ListMonitorsPage if hasAny fails

* fix(monitors): monitors test start paging at 1

* fix(monitors): gate onboarding splash CTA on monitors:CUD

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

* improvements(monitors): better help description

* refactor(table-controls): template filter empty-state copy with tableName

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

* fix(monitors): pass hasCUDAccess in MonitorsOnboarding client tests

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

* fix(monitors): release entitlement for projects pending deletion

* fix(monitors): status severity consistant across create and update operations

* fix(monitors): status severity consistant across create and update operations

* ci(codespell): ignore false-positive usera

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

* test(monitors): rename userA to sessionUser to satisfy codespell

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

* fix(monitors): processor filters by triggerIds

* fix(monitors): update not found error

* improvement(monitors): take placeholder name as the default name

* fix(monitors): prevent monitor edits from toggeling status

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

* fix(monitors): removed dead isPinnedRight field

* feat(monitors): show preview query errors inline in live preview

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

* style(monitors): fill preview height with flex and shrink error overlay

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

* refactor(monitors): return claimed monitors directly from claim

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

* test(monitors): rename to monitors-e2e and link seeded trigger via triggerIds

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

* refactor(monitors): clean up processor and scheduler

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

* fix(monitors): monitor processor receives inputs instead of parsed domain model

* fix(monitors): correct stale fixture field names and typo to unblock CI

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

* fix(monitors): null lastClaimedAt on query-shape edit to void stale-completion CAS

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

* fix(monitors): NO_DATA self-loop honors noData SILENT instead of renotify

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

* fix(monitors): omit permalink at producer when NEXTAUTH_URL unset

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

* fix(monitors): parse monitor-alert payload in executeSlackAction to recover Dates

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

* fix(monitors): guard complete CAS on status=ACTIVE to not overwrite a post-claim PAUSED row

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

* fix(monitors): scope MonitorService.update pre-read by projectId

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

* fix(monitors): clear monitor-alert failure counter on auto-disable

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

* fix(monitors): guard complete CAS on publish identity to void scheduler-rescued stale completion

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

* fix(monitors): isolate success-path failure-counter reset so a Redis blip can't cascade into the failure branch

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

* fix(monitors): use alert title as Slack text fallback for monitor-alert push previews

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

* fix(monitors): gate claimMonitors on status=ACTIVE to close pause-then-resume PAUSED-stick race

* fix(monitors): cap Slack monitor-alert header at 150 chars and drop severity emoji

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

* test(monitors): drive no-AutomationExecution test through real dispatch path

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

* fix(monitors): hoist Slack config read out of failure-counting try

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

* fix(monitors): update stale Slack header emoji assertion to title-only

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

* fix(monitors): empty payload.filters in oversize GitHub-dispatch monitor-alert fallback

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

* fix(monitors): MIN-rollup scheduler run_at and reset stamps on resume

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

* fix(monitors): escape Slack text fallback for monitor-alert dispatches

* fix(monitors): MAX-rollup scheduler run_at so resumed siblings pull batch forward

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

* fix(monitors): NOTIFY no-data monitor alerts on sustained NO_DATA from cold start

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

* fix(monitors): reset alertedAt on PAUSED->ACTIVE resume

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

* fix(monitors): wrap failure-side resetAutomationFailures in try/catch

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

* docs(monitors): correct MonitorNoDataSchema SILENT/NOTIFY JSDoc

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

* fix(monitors): NO_DATA persistence emits when prior-stretch alert predates current stretch

* fix(monitors): gate NO_DATA on appended count_count so empty-window count/sum/min/max monitors report NO_DATA

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

* feat(monitors): list row actions, polling, and monitors/<id> route

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

* fix(monitors): minor ui refinements

* fix(monitors): theme-aware foreground ramp for row action icons

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

* feat(monitors): make monitor queue concurrency configurable via env

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

* improvement(monitors): removed ComputedSeverity enum

* test(monitors): null vs 0 coverage for scalar queries

* style(monitors): MonitorRunner log name clarity

* test(monitors): skip scalar query test when events_core is absent

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

* fix(monitors): flip monitor to ERROR_BAD_QUERY on permanent query-shape failure

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

* fix(monitors): scope ERROR_BAD_QUERY to query-shape failures only

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

* improvement(monitors): better error handling

* test(monitors): executeQuery error flips ERROR_BAD_QUERY, no throw

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

* feat(monitors): per-metric ErrorBadQuery validation, unify into complete

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

* refactor(monitors): rename toActve typo, inline ACTIVE transition

* fix(monitors): gate MonitorAutomationsPanel row toggles on hasAccess

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

* fix(automations): seed webhook apiVersion from trigger eventSource

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

* fix(monitors): gate AddAutomationDropdown trigger on hasAccess

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

* fix(monitors): log batch context when queryMetrics executeQuery rejects

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

* fix(nav): drop DEV region gate from cloudAdmin check

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

* feat(monitors): require at least one automation on create/update

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

* feat(monitors): make form validation errors visible on submit

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

* test(monitors): seed valid triggerIds in monitors servertest fixture

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

* fix(monitors): silent label on no-data is correct now

* fix(monitors): forward eventSource when switching action type to WEBHOOK

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

* fix(monitors): gate nav entry on isLangfuseCloud to match page gate

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-09 14:34:13 +00:00
c82119e266 feat(api): expose code evaluators in public endpoints (#14058)
* feat(api): expose code evaluators in public endpoints

* fix(api): correct code-evaluator error mapping and patch type handling

Address review findings on the public unstable evals API:

- Translate raw TRPCErrors from the shared code-eval preflight into the
  documented structured 4xx errors instead of a 500 (dispatcher/template/
  language failures previously fell through to internal_error).
- Restore BAD_REQUEST for invalid code-evaluator targets in the tRPC path.
- Stop the PATCH evaluator reference from defaulting `type` to llm_as_judge;
  inherit the rule's current evaluator type when omitted so code rules are
  not silently retargeted.

Update Fern docs + regenerate OpenAPI, add regression tests.

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

* test(evals): update stale unstable evals assertions for evaluator type

- Drop the removed LLM_AS_JUDGE filter from the active-rule count expectation
  (the query now spans code evaluators too).
- Expect the evaluator `type` field now returned on evaluation-rule responses.

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

* fix(api): disallow changing an evaluation rule's evaluator type on patch

A PATCH that switched a code rule to an LLM evaluator inherited the
synthesized canonical code mapping and validated it against the LLM
evaluator, producing a 400 referencing fields the caller never sent.

Drop `type` from the patch evaluator reference so the rule always inherits
its current evaluator type; the family lookup stays scoped to that type, so
cross-type retargeting can no longer happen. Update Fern docs + regenerate
OpenAPI, and assert the stripped `type` in the contract test.

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

* refactor(evals): use typed domain errors for code-eval validation

Replace ad-hoc TRPCErrors in the code-eval test run with a typed
CodeEvalTestRunSetupError discriminated by `code`, and consolidate the
job-config preflight into a single CodeEvalJobConfigError carrying a
typed `code` (drops the redundant invalid-target class). Transport
mapping (tRPC + unstable public API) now lives at each boundary with
exhaustive switches. Also simplifies the evaluator-type contract to
literal<"code"> and reuses PublicEvaluatorTypeType.

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

* fix(evals): align code-eval job-config tRPC error codes and fixtures

Map CodeEvalJobConfigError to granular tRPC codes (resource_not_found →
NOT_FOUND, invalid_request → BAD_REQUEST, preflight_failed →
PRECONDITION_FAILED) via a dedicated helper, restoring the pre-existing
404/400 responses on the create/update job-config paths and keeping the
tRPC boundary consistent with the public API.

Also fixes test fixtures left structurally invalid by the widened
StoredPublicEvaluatorTemplate / narrowed evalTemplate Pick types: add
type/sourceCode/sourceCodeLanguage to the LLM fixtures, add type and
drop excess vars/prompt from the rule-config fixture, pass evaluator
type on the query fixtures, and narrow the discriminated evaluator
record before asserting outputDefinition.

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

* docs(api): note code-eval source is validated at rule-link time

Clarify in the create-evaluator endpoint docs that a code evaluator's
sourceCode is not executed at creation; it is first preflight-tested
when linked to an evaluation rule, so runtime errors surface there.
Regenerate the OpenAPI output accordingly.

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

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 13:44:35 +00:00
e94a792ecf fix(scores): qualify scores columns (#13882)
* fix(scores): qualify scores columns

* chore: retrigger ci

---------

Co-authored-by: Nimar <l.nimar.b@gmail.com>
Co-authored-by: Steffen Schmitz <steffen@langfuse.com>
2026-06-09 13:28:59 +00:00
633c603a91 feat(support): Team/Enterprise urgent (Sev-1) support requests (#14094)
* refactor(web): create support threads only in Pylon

The support form previously created threads in both Pylon and Plain. As
Plain is being deprecated, this removes all Plain thread-creation code so
new support threads are created only in Pylon.

- Replace plainRouter with a Pylon-only supportRouter; drop the Plain-only
  prepareAttachmentUploads procedure and all Plain calls (customer upsert,
  tenant/tier sync, thread + thread-event creation, presigned S3 uploads).
  Org/project validation and plan derivation are kept for Pylon
  priority/severity/tier mapping. A missing PYLON_API_KEY now surfaces as
  pylonIssueFailed instead of a silent no-op.
- Update SupportFormSection to upload attachments only to Pylon and call
  api.supportRouter.createSupportThread.
- Delete the support-chat/plain directory and add pylonConstants.ts for the
  attachment size limit (10MB, matching the Pylon upload endpoint).
- Remove the now-unused PLAIN_API_KEY env var from env.mjs and
  .env.prod.example.

PLAIN_AUTHENTICATION_SECRET / createSupportEmailHash (Plain dashboard login
via auth) are left untouched as they are out of scope.

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

* fix(web): preserve support form state when Pylon submission fails

Now that Pylon is the sole destination for support threads,
pylonIssueFailed=true means no ticket was created anywhere. The previous
onSuccess handler reset the form (message, topic, severity, attachments)
before checking pylonIssueFailed, so a failed submission wiped everything
the user typed and left them no way to retry.

Move the reset/clear into the success-only path and early-return after
showing the error toast on failure, keeping the form state intact for retry.

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

* fix(web): prevent silent attachment drops on support form

Two regressions from removing Plain as a support destination:

1. The per-file cap was raised to 10MB but maxCombinedBytes stayed at 50MB.
   Attachments are POSTed to /api/support/upload-attachments as base64 JSON
   (~33% larger), and that endpoint caps the body at 50MB, so a valid <50MB
   raw selection could exceed the body limit and be rejected. Lower
   maxCombinedBytes to 35MB (~47MB once base64-encoded) for headroom.

2. uploadFilesToPylon is now the sole attachment path but was still wrapped
   in a .catch that returned [], silently dropping the user's files while the
   thread was still created with a success toast. Remove the swallow so upload
   errors propagate to the outer try/catch (form.setError), letting the user
   retry instead of losing attachments.

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

* refactor(web): use shared PYLON_MAX_FILE_SIZE_BYTES in upload endpoint

The upload-attachments endpoint hardcoded its own 10MB per-file limit while
pylonConstants.ts claimed to be the single source of truth. Import the shared
constant so the endpoint and the form stay in sync and a future bump only
needs to change one place.

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

* feat(support): allow Team/Enterprise to flag urgent (Sev-1) requests

Add an "Urgent (Sev-1)" checkbox next to the severity dropdown in the
support form, shown only to Team/Enterprise plans. When checked, the
Pylon issue is created with case_severity "Sev-1" and priority "urgent".

The flag is enforced server-side: non-high-tier plans cannot force Sev-1
even if the input is supplied. An info tooltip explains intended use.

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

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 12:36:35 +00:00
Ben BachemandGitHub ec5ac58183 feat(agent): Improve in-app agent UI (#14027)
* feat(agent): Improve in-app agent UI

* Restyle conversation history UI

* fixup! feat(agent): Improve in-app agent UI

* Update markdown styling

* More styling improvements

* Do not close trace pane when interacting with the in-app agent

* Increase in-app agent window

* Resolve PR comments
2026-06-09 12:02:27 +00:00
Ben BachemandGitHub 2cfac4aff2 feat(agent): Add Langfuse Docs MCP (#14116) 2026-06-09 12:02:02 +00:00
Valery MeleshkinandGitHub ee36a6aaf4 fix: Function 'hasAllTokens' supports a max of 64 search tokens (#14114) 2026-06-09 10:03:53 +00:00
2b7332104f feat(storage): add GOOGLE_CLOUD_UNIVERSE_DOMAIN configuration option for GCP blob storage (#13453)
* fix(Storage): use GOOGLE_CLOUD_UNIVERSE_DOMAIN

* feat(Storage): Set default value for GOOGLE_CLOUD_UNIVERSE_DOMAIN

* feat(Storage): Add universeDomain to others Storage instantiation

---------

Co-authored-by: Steffen Schmitz <steffen@langfuse.com>
2026-06-09 09:53:12 +00:00
Ben BachemandGitHub d872b3383e chore: Enable eslint concurrency (#14115) 2026-06-09 09:33:35 +00:00
2aafc3a0d4 fix(email): pass SMTP_CONNECTION_URL through NextAuth instead of SES options (#14100)
Handing the nodemailer SES transport-options object (containing a live
SESv2Client) to NextAuth's EmailProvider `server` field crashed every
NextAuth-adjacent endpoint, including /api/auth/session, with
`RangeError: Maximum call stack size exceeded`. NextAuth's
`parseProviders` deep-merges provider options on every request, and the
recursion follows circular references inside the AWS SDK client.

Pass the connection URL string to EmailProvider instead and let the
custom `sendResetPasswordVerificationRequest` build the transport via
the existing `createMailTransport` helper. The now-unused
`buildMailServerConfig` helper and `MailServerConfig` type are removed.

Adds a regression test that replays NextAuth's deep-merge algorithm
against each supported SMTP_CONNECTION_URL scheme (smtp://, smtps://,
ses://) and asserts none of them blow the stack.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Steffen Schmitz <steffen@langfuse.com>
2026-06-09 09:31:42 +00:00
d32b8521a3 feat(scores): release v3 scores API (#14099)
Promotes the v3 scores list endpoint from preview to general availability.

- Move Fern source from `_drafts/scores-v3.yml` to `definition/scores-v3.yml`
  so the endpoint appears in the published OpenAPI spec and generated SDKs.
- Remove the `LANGFUSE_ENABLE_SCORES_V3_API` feature flag from `env.mjs`
  and both `.env.*.example` files. The handler no longer 404s when the
  flag is unset — the endpoint is now reachable on every deployment.
- Regenerate `web/public/generated/api/openapi.yml` to include
  `/api/public/v3/scores`.

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-09 07:18:57 +00:00
971284cd0d chore: remove foreign keys from project-media (#14105)
* chore: remove foreign keys from project-media

* chore: cleanup

* Update migration.sql

---------

Co-authored-by: Steffen Schmitz <steffenschmitz@hotmail.de>
2026-06-08 19:36:06 +00:00
Tobias WochingerandGitHub f03a02fd8a feat(evals): allowlist more TypeScript evaluator helpers (#14097)
* feat(evals): allowlist more TypeScript evaluator helpers

* fix(evals): type evaluator iterators accurately
2026-06-08 17:07:29 +00:00
Max DeichmannandGitHub f34b83dbdc docs(skills): require valid markdown output (#14091) 2026-06-08 17:49:45 +02:00
Ben BachemandGitHub ff1be63a01 feat(agent): Show tool calls in the UI (#14086) 2026-06-08 14:26:48 +00:00
Niklas SemmlerandGitHub 0721ac483c feat(scores): reject unknown query params on v3 list endpoint (#14093) 2026-06-08 13:26:26 +00:00
Niklas SemmlerandGitHub 5f43038c13 feat(scores): accept lowercase source and dataType on v3 list endpoint (#14092) 2026-06-08 13:26:14 +00:00
eea4629c3f feat(scores): flatten v3 response shape to match proposal (#14088)
* feat(scores): flatten v3 response shape to match proposal

Hoist `comment`, `configId`, `metadata`, `authorUserId`, `queueId` from
the `details` / `annotation` wrappers to top-level fields on the v3
score response. Matches the canonical JSON in the Scores v3 API proposal.
`subject` stays nested (discriminated union; flattening would lose the
`kind`/`traceId` type relationship and collide with the top-level `id`).

`fields=` syntax and gating semantics unchanged — group names still
control which fields appear, only the wrapping changes. The v3 endpoint
is flag-gated (LANGFUSE_ENABLE_SCORES_V3_API=false by default), so no
production callers are affected.

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

* docs(scores): describe field + group dependency in v3 schema

Pair each hoisted field's docs with a "Present when <group> is included"
clause so SDK readers see the field-group gating alongside the
description. Replace ScoreSubjectV3's gating-only type-level docs with a
description of the discriminated union itself.

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

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-08 13:00:54 +00:00
2e10e7a45c feat(scores): version v3 cursor with v: 1 for forward migration (#14084)
* feat(scores): version v3 cursor with v: 1 for forward migration

* remove comment

* fix(scores): encode cursor version from cursor.v instead of hardcoded literal

encodeCursorV3 hardcoded v: 1 in the serialized payload, ignoring cursor.v.
This undermined the discriminated-union forward-compat contract: a future
v: N arm sharing lastTimestamp/lastId would silently serialize as v: 1.
Use cursor.v so the encoded payload always reflects the cursor's version.

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

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-08 12:59:50 +00:00
352adf9993 docs(agents): add security-review skill for SSRF and recurring findings (#14087)
External security reports keep landing on the same shape — a user-supplied URL
or endpoint that the server fetches without going through outbound-URL
validation. Add a shared `.agents/skills/security-review` skill so agents are
prompted to catch the pattern during PR review and during plan/design mode,
not after the fact.

The skill is intentionally extensible: SKILL.md is a slim entrypoint,
`references/checklist.md` is the mental sweep, and each recurring finding
class becomes its own `references/<topic>.md`. SSRF/outbound URL validation
ships today, anchored in the existing canonical helpers
(`validateLlmConnectionBaseURL`, `validateWebhookURL`,
`validateBlobStorageEndpoint`, `validateOutboundUrlHost`,
`fetchWithSecureRedirects`) so reviewers point authors at known-good call
sites rather than re-deriving fixes.

Wire it into the existing routers so it loads at the right moments:
- root `.agents/AGENTS.md` Start Here entry
- `.agents/skills/README.md` registration
- `code-review/SKILL.md` + `code-review/references/review-checklist.md`
- `backend-dev-guidelines/SKILL.md` + `backend-dev-guidelines/AGENTS.md`
  anti-pattern bullet for raw `fetch(<userUrl>)`

Verified with `pnpm run agents:sync` and `pnpm run agents:check`; the new
skill is linked into `.claude/skills/security-review` and surfaces in the
available-skills listing.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-08 12:16:58 +00:00
f2c9642e77 fix(api): return 503 for public auth db failures (#13866)
* fix(api): return 503 for public auth db failures

* fix(api): trace public auth db failures

---------

Co-authored-by: Steffen Schmitz <steffen@langfuse.com>
2026-06-08 11:49:25 +00:00
Max DeichmannandGitHub e912555b2b fix: suppress network connectivity error toasts (#14054) 2026-06-08 11:39:50 +00:00
Ben BachemandGitHub fcf7142f0e feat(agent): Trace in-app agent in langfuse (#13748) 2026-06-08 11:29:32 +00:00
Ben BachemandGitHub 414b4ebb09 fix(web): Margins on project overview (#14078) 2026-06-08 11:28:04 +00:00
Steffen Schmitz 3cb6514396 chore: release v3.179.1 2026-06-08 11:56:07 +02:00
34786499a5 fix(worker): bump decommissioned gemini-2.0-flash to gemini-2.5-flash in VertexAI tests (#14085)
* fix(worker): bump decommissioned gemini-2.0-flash to gemini-2.5-flash in VertexAI tests
* fix(worker): use gemini-2.5-flash-lite for VertexAI tests to avoid thinking-token starvation
---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 11:51:19 +02:00
Steffen Schmitz 30cb63ff9b chore: release v3.179.0 2026-06-08 11:38:35 +02:00
fdf8ab5925 refactor(web): remove support thread creation in Plain (#14081)
* refactor(web): create support threads only in Pylon

The support form previously created threads in both Pylon and Plain. As
Plain is being deprecated, this removes all Plain thread-creation code so
new support threads are created only in Pylon.

- Replace plainRouter with a Pylon-only supportRouter; drop the Plain-only
  prepareAttachmentUploads procedure and all Plain calls (customer upsert,
  tenant/tier sync, thread + thread-event creation, presigned S3 uploads).
  Org/project validation and plan derivation are kept for Pylon
  priority/severity/tier mapping. A missing PYLON_API_KEY now surfaces as
  pylonIssueFailed instead of a silent no-op.
- Update SupportFormSection to upload attachments only to Pylon and call
  api.supportRouter.createSupportThread.
- Delete the support-chat/plain directory and add pylonConstants.ts for the
  attachment size limit (10MB, matching the Pylon upload endpoint).
- Remove the now-unused PLAIN_API_KEY env var from env.mjs and
  .env.prod.example.

PLAIN_AUTHENTICATION_SECRET / createSupportEmailHash (Plain dashboard login
via auth) are left untouched as they are out of scope.

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

* fix(web): preserve support form state when Pylon submission fails

Now that Pylon is the sole destination for support threads,
pylonIssueFailed=true means no ticket was created anywhere. The previous
onSuccess handler reset the form (message, topic, severity, attachments)
before checking pylonIssueFailed, so a failed submission wiped everything
the user typed and left them no way to retry.

Move the reset/clear into the success-only path and early-return after
showing the error toast on failure, keeping the form state intact for retry.

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

* fix(web): prevent silent attachment drops on support form

Two regressions from removing Plain as a support destination:

1. The per-file cap was raised to 10MB but maxCombinedBytes stayed at 50MB.
   Attachments are POSTed to /api/support/upload-attachments as base64 JSON
   (~33% larger), and that endpoint caps the body at 50MB, so a valid <50MB
   raw selection could exceed the body limit and be rejected. Lower
   maxCombinedBytes to 35MB (~47MB once base64-encoded) for headroom.

2. uploadFilesToPylon is now the sole attachment path but was still wrapped
   in a .catch that returned [], silently dropping the user's files while the
   thread was still created with a success toast. Remove the swallow so upload
   errors propagate to the outer try/catch (form.setError), letting the user
   retry instead of losing attachments.

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

* refactor(web): use shared PYLON_MAX_FILE_SIZE_BYTES in upload endpoint

The upload-attachments endpoint hardcoded its own 10MB per-file limit while
pylonConstants.ts claimed to be the single source of truth. Import the shared
constant so the endpoint and the form stay in sync and a future bump only
needs to change one place.

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

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 09:13:47 +00:00
Niklas SemmlerandGitHub 0219baba50 docs(agents): document versioned API type location convention (#14080)
## What does this PR do?

> PR title must follow Conventional Commits, for example `feat(web): add trace filters` or `fix: handle empty dataset names`.

<!-- Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change. -->

Fixes # (issue)

<!-- Please provide a loom video for visual changes to speed up reviews
 Loom Video: https://www.loom.com/
-->

## Type of change

<!-- Please delete bullets that are not relevant. -->

- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] 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 (restructures existing code without changing behavior, e.g. simplify logic, split modules, reduce duplication)
- [ ] This change requires a documentation update

## Mandatory Tasks

- [ ] Make sure you have self-reviewed the code. A decent size PR without self-review might be rejected.

## Checklist

<!-- Remove bullet points below that don't apply to you -->

- I haven't read the [contributing guide](https://github.com/langfuse/langfuse/blob/main/CONTRIBUTING.md)
- My code doesn't follow the style guidelines of this project (`pnpm run format`)
- I haven't commented my code, particularly in hard-to-understand areas
- I haven't checked if my PR needs changes to the documentation
- I haven't checked if my changes generate no new warnings (`npm run lint`)
- I haven't added tests that prove my fix is effective or that my feature works
- I haven't checked if new and existing unit tests pass locally with my changes

<!-- greptile_comment -->

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

This PR adds a new "Versioned API Type Location" subsection to the backend dev guidelines, documenting the convention for placing versioned public API types under `packages/shared/src/features/<domain>/interfaces/api/v{N}/`. The described directory structure and canonical example (the `scores` feature) accurately match the current repository layout.

- Adds directory-tree illustration and three recommended files per version (`schemas.ts`, `endpoints.ts`, `validation.ts`), matching what already exists for `scores` v1–v3.
- Documents two anti-patterns to avoid: flat versioned files and placing types in `web/src/features/public-api/types/`.
- One minor cross-reference error: the text says "example below" but the cited code block appears above the new section.
</details>

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

Documentation-only change that accurately describes an existing pattern already used across the scores feature. Safe to merge.

The added section correctly reflects the actual directory structure in the repo (verified against packages/shared/src/features/scores/interfaces/api/). The only issue is a minor directional error in a cross-reference ('below' vs 'above') that does not affect the correctness of the convention being documented.

The single changed file .agents/skills/backend-dev-guidelines/references/routing-and-controllers.md contains a misdirected cross-reference on line 384 worth a quick fix before merge.
</details>

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

```mermaid
%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[New versioned API feature] --> B{Where to place types?}
    B -->|Correct| C["packages/shared/src/features/domain/interfaces/api/vN/"]
    C --> D["schemas.ts — Zod request/response shapes"]
    C --> E["endpoints.ts — Composed types"]
    C --> F["validation.ts — Cross-field helpers"]
    B -->|Anti-pattern 1| G[" Flat file: domain-api-v2.ts"]
    B -->|Anti-pattern 2| H[" web/src/features/public-api/types/"]
    C --> I["Export via @langfuse/shared"]
    I --> J["Consumed by web/src/pages/api/public/..."]
```
</details>

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

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

---

### Issue 1 of 1
.agents/skills/backend-dev-guidelines/references/routing-and-controllers.md:383-385
**Stale cross-reference direction**

The text says "in the example below", but the code block that uses `GetScoresQueryV1` / `GetScoresResponseV1` (imported from `@langfuse/shared`) lives in the **REST API Pattern** section immediately *above* this new section, not below it. Readers following the pointer downward will find nothing.

Change "example below" → "example above".

```suggestion
The scores feature (`packages/shared/src/features/scores/interfaces/api/`) is
the canonical example. The `GetScoresQueryV1` / `GetScoresResponseV1` symbols
imported from `@langfuse/shared` in the example above originate there.
```

`````

</details>

<sub>Reviews (1): Last reviewed commit: ["docs(agents): document versioned API typ..."](https://github.com/langfuse/langfuse/commit/f0ff59bd10074b5d86f5c84106a1972c53f99d87) | [Re-trigger Greptile](https://app.greptile.com/api/retrigger?id=36165686)</sub>

> Greptile also left **1 inline comment** on this PR.

<!-- /greptile_comment -->
2026-06-08 11:03:37 +02:00
marliessophieandGitHub 9654566f75 feat(experiments): move ui tables to GA (#14057)
* feat(experiments): move ui tables to GA

* fix(experiments): enhance useExperimentAccess hook with initialization state
2026-06-08 08:28:51 +00:00
3fa569abff fix(security): enforce data-retention entitlement server-side in setRetention (#14071)
* fix(security): enforce data-retention entitlement server-side in setRetention

The data-retention entitlement was only enforced in the UI
(ConfigureRetention.tsx disables the input client-side). The backing
projects.setRetention tRPC mutation validated project membership and the
project:update RBAC scope but not the plan entitlement, so a project
OWNER/ADMIN on a plan without data-retention could call the mutation
directly and set a destructive retentionDays, triggering the worker
deletion scheduler despite the feature not being available for the plan.

Add a server-side throwIfNoEntitlement check scoped to the project, after
the existing RBAC check, matching the pattern used in other routers. Keep
the UI check as presentation only. Add a server-side regression test
covering both the non-entitled (FORBIDDEN, not persisted) and entitled
(succeeds) plans.

Fixes LFE-10054

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

* chore: feedback

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 12:27:00 +00:00
3fc77c40b6 fix(queue): make BullMQ Redis version check configurable (#13541)
Co-authored-by: Steffen Schmitz <steffen@langfuse.com>
2026-06-07 11:29:56 +00:00
Niklas SemmlerandGitHub 9eec10511f feat(scores): add all filter params to v3 scores API (Phase 4) (#14005)
## Summary

Phase 4 of [LFE-9539](https://linear.app/langfuse/issue/LFE-9539) — adds all filter query parameters to \`GET /api/public/v3/scores\`. All filters are optional.

Stacked on #14001 (Phase 3).

**New query params:**

| Param | Type | Notes |
|---|---|---|
| \`id\` | comma-separated | OR within, AND across filters |
| \`name\` | comma-separated | |
| \`source\` | comma-separated enum | \`API\`, \`EVAL\`, \`ANNOTATION\` — invalid values → 400 |
| \`dataType\` | comma-separated enum | \`NUMERIC\`, \`BOOLEAN\`, \`CATEGORICAL\`, \`TEXT\`, \`CORRECTION\` — invalid values → 400 |
| \`environment\` | comma-separated | |
| \`configId\` / \`queueId\` / \`authorUserId\` | comma-separated | |
| \`traceId\` / \`sessionId\` / \`observationId\` / \`experimentId\` | comma-separated | mutually exclusive |
| \`value\` | comma-separated | requires single \`dataType\` from NUMERIC / BOOLEAN / CATEGORICAL |
| \`valueMin\` / \`valueMax\` | number | requires \`dataType=NUMERIC\` |
| \`fromTimestamp\` / \`toTimestamp\` | datetime | optional |
| \`userId\` / \`traceTags\` | — | always → 400 (require trace JOIN, not supported in v3) |

### Validation details

- \`source\` and \`dataType\` validated against their enum allowlists at parse time
- Bare params (e.g. \`?id=\`) produce empty arrays — not treated as present
- \`?value=\` (empty/comma-only) → treated as absent, not a 400
- \`?traceId=&sessionId=<uuid>\` → empty \`traceId\` ignored, no false mutual-exclusion 400
- \`value=abc&dataType=NUMERIC\` → 400 (non-numeric, using \`!isFinite\` to also catch Infinity)
- \`value=true&dataType=BOOLEAN\` valid; \`value=1&dataType=BOOLEAN\` → 400
- \`transformBooleanValueForFilter\` parameter narrowed to \`"true" | "false"\`

### Impacted packages

- \`@langfuse/shared\` — \`GetScoresV3\` extended with all filter params; \`csvStringParam\` (blank-segment filtered); \`csvEnumParam\` for validated enum params
- \`web\` — \`buildDynamicFilters\` wires params to \`FilterList\`; \`GetScoresV3Query\` superRefine in handler; \`transformBooleanValueForFilter\` helper; tests

## Test plan

- [x] \`pnpm --filter web run lint\` — clean
- [x] \`pnpm --filter @langfuse/shared run typecheck\` — clean
- [x] 12 unit tests pass (polymorphicValue, transformBooleanValueForFilter)
- [ ] Integration tests — require live ClickHouse + \`LANGFUSE_ENABLE_SCORES_V3_API=true\`
  - 400 validation: userId, traceTags, entity mutual exclusion, source=INVALID, dataType=INVALID, value type/format checks, empty value= → 200
  - filter correctness: id, name, source, dataType, environment, configId, queueId, authorUserId, traceId, sessionId, observationId, experimentId, value NUMERIC/CATEGORICAL/BOOLEAN, valueMin/valueMax, toTimestamp

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-06-05 17:31:30 +02:00
Niklas SemmlerandGitHub fae4acd8f0 feat(scores): add field groups to v3 scores API (Phase 3) (#14001)
## What does this PR do?

Phase 3 of [LFE-9539](https://linear.app/langfuse/issue/LFE-9539) — adds optional `fields=` query parameter to `GET /api/public/v3/scores`. Default is `core` when omitted; callers opt in to additional groups:

- **`details`** — `comment`, `configId`, `metadata` (always an object, never null)
- **`subject`** — `kind` (trace/observation/session/experiment), entity-specific `id`, and `traceId` (observation-kind only; absent, not null, when missing)
- **`annotation`** — `authorUserId`, `queueId`

Unknown group names → 400. `fields=trace` is reserved → 400.

### Design notes

- `deriveSubject` throws `InternalServerError` when a score reaches the trace branch with a null `traceId` — surfaces data integrity issues instead of silently returning a wrong ID; the row-drop handler in `listScoresV3ForPublicApi` catches it so callers never see a 500
- `ScoreDetailsV3.metadata` is non-nullable (implementation always coalesces to `{}`); OpenAPI and Zod schema aligned accordingly
- `nullable: true` is not emitted for `traceId`, `details`, `subject`, or `annotation` in the Fern draft — these fields are either present or absent, never null; optionality is expressed via absence from the `required` array. The generated `openapi.yml` is **not** updated in this PR because the endpoint lives in `fern/apis/server/_drafts/` and is excluded from Fern's discovery path; it will be regenerated when the draft is promoted to `definition/`

Stacked on #13996 (Phase 2).

### Impacted packages

- `@langfuse/shared` — `SCORE_FIELD_GROUPS_V3`, `fieldsParam` Zod transform, `ScoreDetailsV3` / `ScoreSubjectV3` / `ScoreAnnotationV3` schemas
- `web` — `deriveSubject` helper, `domainToV3` accepts `fields`, handler passes `fields` through, tests (`createSessionScore` / `createDatasetRunScore` imported for kind coverage)
- `fern/` — `fields` param and group types added to draft spec (`_drafts/scores-v3.yml`); generated `openapi.yml` unchanged (draft not yet promoted)

## Type of change

- [x] New feature (non-breaking change which adds functionality)

## Mandatory Tasks

- [x] Make sure you have self-reviewed the code. A decent size PR without self-review might be rejected.

## Checklist

- [x] My code follows the style guidelines of this project (`pnpm run format`)
- [x] I have commented my code where needed (particularly `deriveSubject` priority order and the draft-vs-definition OAS note)
- [x] My changes generate no new warnings (`pnpm run lint`)
- [x] I have added tests that prove my feature works (all four `subject.kind` values, field group combinations, reserved/unknown group rejection)
- [x] New and existing tests pass locally with my changes
2026-06-05 17:25:50 +02:00
Niklas SemmlerandGitHub 36e2085cf5 feat(scores): add cursor pagination to v3 scores API (Phase 2) (#13996)
## Summary

Phase 2 of [LFE-9539](https://linear.app/langfuse/issue/LFE-9539) — adds cursor-based pagination to \`GET /api/public/v3/scores\`.

Stacked on #13995 (Phase 1).

- **Cursor tuple**: \`(timestamp, id)\` — base64url-encoded JSON, matches the leading sort columns
- **\`meta.cursor\`** is present when more pages exist; absent on the final page — no COUNT query needed
- **Invalid cursor → 400** — decoded via Zod transform; bad base64 or wrong-shape JSON surfaces as \`InvalidRequestError("Invalid cursor format")\`
- **Fetch \`limit + 1\`** trick: service fetches one extra row; if it arrives, trim it and emit cursor from the last kept row
- **Cursor seek**: \`WHERE (s.timestamp, s.id) < (...)\` — matches the \`ORDER BY timestamp DESC, id DESC\` prefix exactly
- **\`s.event_ts\` removed from SELECT** — no longer consumed after cursor simplification; ORDER BY on \`event_ts\` still works without it in SELECT

### Why \`(timestamp, id)\` and not \`(timestamp, event_ts, id)\`?

The \`ORDER BY\` is \`timestamp DESC, id DESC, event_ts DESC\`. \`id\` is the stable second sort key across scores — putting it second in the cursor means the seek predicate \`(timestamp, id) < (...)\` exactly matches the sort prefix. \`event_ts\` is only a tiebreaker for \`LIMIT 1 BY\` deduplication of multi-version rows; after deduplication each \`(id, project_id)\` pair appears at most once, so \`event_ts\` never determines inter-score ordering. Using it in the cursor would cause page boundary instability when concurrent writes produce scores with the same timestamp but inverse \`event_ts\`/\`id\` orderings.

### Impacted packages

- \`web\` — handler, service, new \`types/scores.ts\` cursor encode/decode, tests
- \`@langfuse/shared\` — \`GetScoresV3\` gains \`cursor?: string\`; \`GetScoresResponseV3\` meta gains \`cursor?: string\`
- \`fern/\` + \`web/public/generated/api/openapi.yml\` — cursor added to request params and response meta

## Test plan

- [x] \`pnpm --filter web run lint\` — passed
- [x] \`pnpm --filter web run typecheck\` — passed
- [x] Fern generation — all checks passed
- [ ] Integration tests with \`LANGFUSE_ENABLE_SCORES_V3_API=true\` — require live ClickHouse
  - cursor present when more pages exist, absent on final page
  - full pagination without duplicates or skips
  - invalid cursor → 400
  - stale cursor → empty page, no cursor

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-06-05 17:08:31 +02:00
4134aabe35 feat(scores): add v3 scores API — polymorphic value, flag-gated (Phase 1) (#13995)
* feat(scores): add v3 scores API phase 1 — polymorphic value, flag-gated

Introduces GET /api/public/v3/scores and GET /api/public/v3/scores/{scoreId}
behind LANGFUSE_ENABLE_SCORES_V3_API feature flag. Phase 1 delivers the
polymorphic `value` field (number/boolean/string/null by dataType), a hard
limit of 100, and the core response shape — no cursor or filters yet.

New files:
- packages/shared/src/features/scores/interfaces/api/scores-v3.ts: GetScoresV3,
  GetScoreV3, GetScoresResponseV3, GetScoreResponseV3, APIScoreSchemaV3 Zod
- web/src/features/public-api/server/scores-api-v3.ts: standalone v3 service
  (listScoresV3ForPublicApi, getScoreV3ForPublicApi, polymorphicValue). Does not
  import v1/v2 service helpers.
- web/src/__tests__/server/scores-api-v3.servertest.ts: polymorphicValue unit
  tests always run; integration tests gate on LANGFUSE_ENABLE_SCORES_V3_API=true

Fern update deferred to follow-on task (endpoint is flag-off by default).

Part of LFE-9539 / LFE-9926.

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

* feat(scores): add Fern v3 scores service definition and regenerate OpenAPI

Phase 1 of LFE-9539: adds fern/apis/server/definition/scores-v3.yml with
GET /v3/scores (limit-only) and GET /v3/scores/{scoreId} endpoints, using
a discriminated union on dataType for the polymorphic value field.
Regenerated openapi.yml via `npx fern generate --api server --group local`.

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

* fix(scores): use z.discriminatedUnion for v3 response schema and align CORRECTION value

- Replace flat APIScoreSchemaV3 z.object with z.discriminatedUnion keyed on
  dataType, matching the Fern/OpenAPI contract — invalid pairs like
  { dataType: 'NUMERIC', value: 'oops' } now fail Zod validation
- Remove unused ScoreDataTypeDomain import
- Fix CORRECTION polymorphicValue: || null → ?? null so empty string is
  preserved rather than coerced to null
- Add symmetric "longStringValue" in score guard to match existing stringValue
  guard in domainToV3
- Cast construction result as APIScoreV3 since ScoreDomain is flat and
  TypeScript cannot verify the dataType/value pair statically

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

* test(scores): add CORRECTION round-trip integration test

Covers the full GET /v3/scores path for a CORRECTION score with a
non-empty longStringValue, closing the coverage gap alongside the
existing NUMERIC, BOOLEAN, CATEGORICAL, and TEXT integration tests.

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

* chore: remove self-evident comment from env var declaration

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

* test(scores): use it.skipIf for feature-flag-off test

Replaces the early-return guard with it.skipIf so Vitest reports a
real skip rather than a zero-assertion pass when the flag is enabled
in CI.

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

* fix(scores): drop tautological in-guards in domainToV3

ScoreDomain always has both stringValue and longStringValue as own keys,
so the 'key in score' checks and their null fallbacks were unreachable.
Replace with direct property access.

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

* fix(scores): route v3 single-score GET to ReadOnly ClickHouse cluster

getScoreV3ForPublicApi was calling the getScoreById wrapper which does
not forward preferredClickhouseService, causing single-score reads to
hit the primary cluster. Switch to _handleGetScoreById directly with
scoreScope="all" and preferredClickhouseService="ReadOnly", mirroring
ScoresApiService.getScoreById in v1/v2.

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

* fix(scores): use nullable<string> for CATEGORICAL/TEXT/CORRECTION value in Fern

optional<string> means the field may be absent; nullable<string> means
the field is always present but can be null — which matches the wire
format and the Zod schema (z.string().nullable()).

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

* fix(scores): make v3 string value non-nullable for CATEGORICAL/TEXT/CORRECTION

The discriminated value is always a string for these data types: the
domain model guarantees stringValue is set for CATEGORICAL/TEXT, and
CORRECTION reads longStringValue which defaults to "". value can never
be null, so the contract should be string, not nullable<string>.

Switch Fern types to string and Zod to z.string(), and update the
now-inaccurate "or null" docs.

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

* fix(scores): throw on impossible null value in polymorphicValue

value is non-nullable for every v3 dataType, so coalescing a missing
string to null produced an invalid object that would only fail later
during response validation with an opaque error. Throw
InternalServerError at the source instead — for a missing stringValue
(CATEGORICAL/TEXT), a missing longStringValue (CORRECTION), or an
unknown dataType — and drop null from the return type.

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

* chore(scores): regenerate OpenAPI for non-nullable v3 string value

Regenerated web/public/generated/api/openapi.yml via
`fern generate --api server --group local` to reflect the
non-nullable CATEGORICAL/TEXT/CORRECTION value.

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

* fix(scores): filter is_deleted=0 in v3 list query and add dev env flag

The v3ListQuery was missing the `is_deleted = 0` filter, causing soft-deleted
scores to be returned in the GET /v3/scores response. Also selects
`is_deleted` to satisfy the ScoreRecordReadType schema. Adds the env var to
.env.dev.example alongside the other events-table feature flags.

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

* remove(scores): drop GET /v3/scores/{scoreId} endpoint

Scores can be retrieved by ID via GET /v3/scores?id={id}, making a
dedicated single-resource route redundant. Removes the route handler,
GetScoreV3/GetScoreResponseV3 schemas, getScoreV3ForPublicApi service
function, getByIdV3 Fern endpoint, and all associated tests. Regenerates
the OpenAPI spec. Also adds .playwright-mcp/ to .gitignore.

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

* fix(scores): remove dormant is_deleted filter from v3 list query

Per backend-dev-guidelines database-patterns doc: is_deleted on scores
is dormant — no write path sets it to 1 and all deletes use lightweight
DELETE. The filter was dead weight and an odd-one-out vs every other
score read in the codebase.

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

* refactor(scores): reorder ORDER BY to timestamp, id, event_ts in v3 list query

Puts id before event_ts so the 2-tuple cursor (timestamp, id) matches the
sort prefix exactly. event_ts remains as the final tiebreaker for LIMIT 1 BY
deduplication of multi-version rows. Eliminates same-millisecond page
boundary instability.

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

* chore(scores): move v3 Fern spec to _drafts so it stays out of public OpenAPI

Relocates fern/apis/server/definition/scores-v3.yml to
fern/apis/server/_drafts/scores-v3.yml. Fern auto-discovers every yml under
definition/; moving the file out keeps the v3 endpoint and schemas out of the
generated OpenAPI spec and downstream SDKs while the runtime endpoint behind
LANGFUSE_ENABLE_SCORES_V3_API is unchanged. Reinstate later with a git mv back
to definition/ and revert the import path one line.

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

* docs(scores): describe v3 availability without naming the env flag

Replaces the LANGFUSE_ENABLE_SCORES_V3_API reference in the v3 endpoint
docs with a consumer-facing description of preview availability. Customer-
facing Fern docs blocks should not name internal env vars or feature flags;
they describe behavior from the consumer's perspective.

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

* refactor(scores): split v3 schemas into folder matching v1/v2 layout

Replaces the flat packages/shared/src/features/scores/interfaces/api/scores-v3.ts
with a v3/ folder containing schemas.ts and endpoints.ts, matching the v1/ and
v2/ layout already in this directory. Renames GetScoresV3 to GetScoresQueryV3
for naming parity with GetScoresQueryV1 and GetScoresQueryV2. The private
foundation schema becomes ScoreFoundationSchemaV3 (was ScoreBaseV3) for the same
reason.

No behavior change; consumers using the @langfuse/shared barrel continue to
resolve the public symbols (APIScoreV3, APIScoreSchemaV3, GetScoresResponseV3)
unchanged.

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

* feat(scores): add v3 row-level response validation matching v2 pattern

Adds filterAndValidateV3GetScoreList in v3/validation.ts and wires it into
listScoresV3ForPublicApi so the v3 list endpoint mirrors the v1/v2 contract:
malformed rows are logged and dropped per row rather than crashing the whole
response with a 500. createAuthedProjectAPIRoute only validates the response
schema in development, so v3 had no production-side row validation before this
change.

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

* refactor(scores): tighten polymorphicValue dataType + add exhaustive check

Replaces the loose dataType: string parameter with the discriminated union
ScoreDataTypeType. With the tightened input, the default branch's
const _exhaustiveCheck: never = score.dataType assignment now does
compile-time work: if a new score dataType is added to the union, this line
fails to compile and points the developer at the missing case.

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

* refactor(scores): prune unused is_deleted and wire v3 row-drop observability

Removes `s.is_deleted` from the v3 list query SELECT — nothing in the read
path consumes it, and ClickHouse is columnar so the column read was wasted
I/O. Also routes filterAndValidateV3GetScoreList parse errors through
@langfuse/shared logger so silent row drops become observable in Datadog
instead of getting buried in stdout via console.error.

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

* test(scores): tighten v3 input tests and tidy off-path coverage

Genericizes the v3-disabled 404 message to "Not Found" so the disabled state
no longer discloses the feature flag's existence to unauthenticated probes.
Adds limit=0, limit=-1, limit=abc → 400 tests to lock the Zod query schema
contract. Drops the CORRECTION-with-null-longStringValue unit test because
the production read path coerces longStringValue to "" via ScoreDomain's
schema default — the live code can never reach the guard.

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

* chore(env): document LANGFUSE_ENABLE_SCORES_V3_API in prod example

Adds a commented optional entry in .env.prod.example matching .env.dev.example's
flag. Defaults to unset on self-hosted; self-hosters can enable explicitly once
the v3 API graduates from preview.

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

* refactor(scores): contain per-row errors in v3 list so one bad row drops alone

Wraps the records.map(domainToV3, ...) call in a try/catch loop so a throw
inside polymorphicValue (e.g. a CATEGORICAL/TEXT row with stringValue null
or a CORRECTION row with longStringValue null) log-drops the single row
instead of crashing the entire listing with 500. Mirrors the row-level
graceful-drop semantics that filterAndValidateV3GetScoreList provides for
Zod failures one layer down.

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

* chore: remove stray .playwright-mcp/ from .gitignore

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

* ci: enable LANGFUSE_ENABLE_SCORES_V3_API in server test environment

Without this flag set, all v3 scores integration tests were silently
skipped in CI via the `maybe` = describe.skip pattern.

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

* test(scores): refactor v3 servertest — drop feature-flag guard, extract polymorphicValue unit tests, compress tabular cases with it.each

- Remove the `maybe` / `it.skipIf` / `env` pattern; the flag is now set
  unconditionally in CI so tests always run.
- Extract `polymorphicValue` unit tests out of the servertest (they need
  no DB/ClickHouse) into `server/unit/polymorphicValue.servertest.ts`,
  which runs under the lighter server-unit Vitest project.
- Compress the four limit-validation cases and five data-type value cases
  into `it.each` tables.

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

* remove duplicate error line

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-05 14:51:44 +00:00
Ben BachemandGitHub 1127567daa refactor(agent): Switch agent framework (#14018)
* refactor(agent): Switch agent framework

* Resolve PR comments
2026-06-05 14:34:00 +00:00
a6e3613fcc feat(worker): V4 self-hosted historic backfill chain (#13623)
* feat(worker): add V4 self-hosted historic backfill chain

Ships the events_full / events_core ClickHouse tables and a 5-step
background-migration chain (M1-M5) that backfills them from existing
traces, observations, and dataset run items. All steps are dormant
behind env gates so v3 self-hosters upgrade without data movement
until they opt in.

Refs: LFE-8833

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

* feat(clickhouse): promote observations_batch_staging to production migration

Adds CH migration 0037 (clustered + unclustered) that ships the
staging table the dual-write pipeline relies on. With this in place,
a self-hoster can flip LANGFUSE_EXPERIMENT_INSERT_INTO_EVENTS_TABLE=true
after the V4 historic backfill chain completes and the existing
event-propagation job will populate events_full from the staging
table — no longer dev-only.

TTL is set to 48h (vs. 12h used internally) so self-hosters get a
multi-day recovery window if the propagation job stops catching up.

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

* chore: remove unnecessary env var

* chore: drop unused env flag

* chore: drop unused env flag

* chore: update env variable setup

* chore(clickhouse): split events_core MV into its own migration and add IF NOT EXISTS guards

Move the events_core_mv definition out of 0036 into a dedicated 0037 so the
materialized view can evolve independently of the table. Renumber the
observations_batch_staging migration from 0037 to 0038 to keep ordering
sequential. Add IF NOT EXISTS to every new CREATE TABLE / CREATE MATERIALIZED
VIEW so the migrations are idempotent on environments where the objects were
provisioned out-of-band (e.g. Langfuse Cloud).

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

* chore(clickhouse): use DROP VIEW for events_core_mv down migration

DROP TABLE works on materialized views in ClickHouse, but DROP VIEW is the
canonical form and validates that the object is actually a view, per the
ClickHouse docs: "DROP VIEW checks that [db.]name is a view."

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

* chore(clickhouse): align unclustered events_full and observations_batch_staging comments with clustered

Mirror the trailing `index_granularity_bytes` rationale onto the unclustered
events_full migration and drop the LFE-7122 reference from the unclustered
observations_batch_staging migration so the two variants only differ by
ON CLUSTER + engine replication, as intended.

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

* chore: drop comment

* chore(prisma): rebase V4 background-migration timestamps from 20260509 to 20260521

Rename the five Prisma migration directories that register the V4 backfill
rows and update the matching name prefix used in each row plus the
self-registering upsert in the worker. The migrations were originally
authored on 2026-05-09; bumping to 2026-05-21 keeps them as the
last-applied migrations on rollout.

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

* fix(worker): prevent dormant background migrations from head-of-line blocking

The envGate check ran after lock acquisition and aborted the run loop on
the first dormant row, so any later un-gated migration (or one whose gate
was on) never got a chance. Push the gate check into the findFirst
predicate via Prisma JSON path equality so dormant rows are invisible to
the manager and unrelated successors run normally.

Also rename gate env vars to a discoverable LANGFUSE_BACKGROUND_MIGRATION_
prefix, register them in the worker EnvSchema (default "false"), and
document the envGate mechanism in worker/src/backgroundMigrations/README.md.

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

* chore: update background migration sqls

* chore: cleanup backfillBase

* chore: refactor partition discovery

* chore: clean up observations pid tid rewrite

* chore: cleanup drop pid tid tables validation

* chore: use parts based observations migration

* chore: confirm column match-ups

* chore: cleanup

* chore: validation changes

* chore: default the event prop queue to on

* chore: add legacy write path skipping behaviour

* chore: remove useEventsTable fallbacks

* chore: move FTS indexes to dev-tables.sh

* chore(clickhouse): move v4 events tables back to dev-tables.sh

Self-hosters on ClickHouse < 24.5 cannot use `enable_block_number_column`
/ `enable_block_offset_column`, and `text` indexes need >= 25.x. Keeping
the v4 events_full / events_core / events_core_mv / observations_batch_staging
DDLs as migrations would block those upgrades, so move them back into the
dev-only script until v4 becomes mainline and we can require 25.12+.

FTS indexes and enable_full_text_index are inlined into the table
definitions instead of being applied via separate ALTER statements.

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

* chore: wording and formatting

* chore: extend eval tests

* chore: patch partition id

* chore: only stop merges on observations_pid_tid_sorting if no errors

* chore: corrections

* chore: address dependency chain in background migrations

* chore: add database filters

* chore: upgrade events_only opt-in message

* chore: patch ingestion api log message

* chore: only push bookmarked on root spans

* chore: stop backfill on data integrity issues

* chore: fallback to events table for getTraceById lookups in events_only mode

* chore: handle descendant limit exceptions with flushing

* chore: remove concurrency

* chore: patch tests and add observations by id wrapper

* chore: select an empty map as metadata

* chore: make test conditional

* chore: feedback

* chore(worker): extract V4 background migration chain into stacked branch

Moves the V4 historic backfill background migrations into a dedicated
stacked branch (feat/v4-historic-backfill-migrations) so the data-movement
logic can be reviewed independently from the events_only read-path changes.

Extracted (now lives in the stacked branch):
- Prisma migrations registering the V4 backfill chain
- createRootSpansFromTraces / rewriteObservationsToPidTidSorting /
  backfillEventsFullFromObservations / backfillEventsFullFromDatasetRunItems /
  dropPidTidSortingTables and utils/backfillBase
- BackgroundMigrationManager env-gate logic + README docs
- LANGFUSE_BACKGROUND_MIGRATION_* env gates

Retained here: removals of the old backfill scripts and all
LANGFUSE_MIGRATION_V4_WRITE_MODE / preview-opt-in read-path changes.

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

* chore: update doc strings and change session mode

* chore: toggle default for lazy materialization disablement

* chore: revert default for lazy materialization

* chore: feedback

* chore: adjust comment reference checks and health check

* chore: configure opt-in flags for fast (preview)

* chore: build shared for lint

* chore: lint

* chore: lint

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-05 13:51:06 +00:00
38cf286ea2 feat(experiments): support charting in experiment list (#13686)
* feat(dashboarding): support experiment filters, dimensions in observations view via preset

* feat(widget-form): render experiment as view rather than preset

* Revert "feat(widget-form): render experiment as view rather than preset"

This reverts commit 841d8437c85b27897e57ccd97b3d12118856713f.

* refactor: handle metadata filter appropriately

* chore: push

* chore: push

* feat(tests): add v2-only experiment filters for observations in dashboard widget tests

* feat: support experiment_name as entity dimension

* feat: enhance experiment widget configuration with new score types and filters

* feat: add hook and API for fetching experiment item filter options

* feat: implement experiment run score filtering and chart grid component

* refactor: move datasetRunId to scoresV2 dimension

* fix: add query parameter to include entity dimension relation tables in QueryBuilder

* refactor: remove TODO comments related to queryId and validation in InlineWidget component

* feat: enhance experiment score widgets with timestamp metrics for ordering

* fix: ensure toTimestamp is correctly set in ExperimentsTable component

* fix: enforce required schedulerId for query scheduling in InlineWidget component

* fix: add score name filter and improve experiment charts UX

- Add missing score name filter to createScoreWidgetConfig for proper
  score filtering (fixes empty observation-level score charts)
- Extract chart constants, types, and utils to separate modular files
- Add horizontal scroll layout for charts grid on smaller screens
- Show "No data available" state for stale metric selections
- Display metric label in dropdown even when not in available options

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

* test(experiments): add tests for getExperimentItemsFilterOptions

Add tests covering:
- Empty experiment IDs returns empty arrays
- Non-existent experiments returns empty arrays
- Trace-level numeric and categorical scores
- Observation-level numeric and categorical scores
- Multiple experiments aggregation
- Categorical value aggregation across experiments

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

* feat(experiments): enhance score filter options with source and column definitions

- Added 'source' field to score filter options for both observation-level and trace-level scores.
- Updated the `buildScoreFilterOptionsQuery` to include 'source' in the selection and grouping.
- Enhanced `processScoreFilterOptionsResults` to track unique score columns by name, source, and data type.
- Modified `useExperimentItemsFilterOptions` to return full score column definitions for better integration with the ExperimentItemsTable.
- Refactored `ExperimentItemsTable` to utilize the new score column definitions for improved column visibility and filtering.

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

* fix(experiments): optimize score filter option query

* refactor(experiments): remove experimentMetadata from data model and related components

* feat(query): add highCardinality property to experiment score dimensions for improved data handling

* chore: push

* chore: push

* chore: push

* chore: push

* chore: push

* feat(query): implement high cardinality entity dimension validation in query processing

- Introduced a set of allowed high cardinality entity dimensions to enhance validation logic.
- Updated `getHighCardinalityDimensions` to include checks for entity dimensions.
- Added tests to ensure proper validation behavior for both allowlisted and non-allowlisted entity dimensions in various scenarios.

* feat(query): extend high cardinality entity dimensions and update related components

* chore: push

* fix(query): Remove unsupported aggregation types for datetime measure

* fix(query): expose datetime measures

* fixup: revert changes to queryBuilder

* feat(query): Implement bucket dimension handling in QueryBuilder

* feat(query): Enhance query validation for entity dimensions and high cardinality checks

* refactor(experiments): pass experiment id and name filters to comply with entity dimension requirements

* test(query): Update validation tests for high cardinality entity dimensions and refine query structure

* chore: push

* chore: push

* feat(experiments): add entity dimension label mapping to ExperimentChartSlot and InlineWidget for improved data presentation

* fix(charts): change order direction of min_timestamp in BASE_SCORE_CHART_CONFIG from descending to ascending

* fix(charts): update order direction of min_startTime and min_timestamp in chart configurations from ascending to descending

* fix(query): enforce entityDimension support only for v2 queries and update validation tests

* refactor(query): rename AppliedBucketDimension to AppliedBucketingDimension for consistency and clarity

* refactor(query): remove timestamp aggregation from scores and events observations views, update related tests and configurations

* fix(experiments): handle metric availability and enablement in ExperimentChartSlot component

* refactor(tests): remove max aggregation tests for scores and observations from queryBuilder tests

* feat(widget): enhance InlineWidget to order x-axis based on entity dimension label mapping for improved chart alignment

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-06-05 13:44:13 +00:00
Valery MeleshkinandGitHub e690319135 fix: make delete mask cleaner wait for submitted mutations to appear (#14056) 2026-06-05 13:25:15 +00:00
Max DeichmannandGitHub 8596942000 docs(agents): clarify weekly production review tables (#14055) 2026-06-05 12:51:19 +00:00
Nikita KabardinandGitHub f89273cd8a docs(agents): add frontend feature architecture skill (#14039)
* docs(agents): add frontend feature architecture skill

* docs(agents): refine large frontend migration guidance

* docs(agents): tighten frontend feature architecture skill
2026-06-05 12:37:28 +00:00
NimarandGitHub bb65af40f2 chore(env): default use haiku 4.5 on bedrock (#14051) 2026-06-05 12:10:36 +02:00
Valery MeleshkinandGitHub 7a9efdf788 fix: separate LANGFUSE_CLICKHOUSE_DELETED_MASK_CLEANER_CLUSTER_MODE_ENABLED toggle for delete mask cleaner (#14050) 2026-06-05 09:42:16 +00:00
NimarandGitHub e0d6be2a2b fix(agent): page and seed correctly (#14048) 2026-06-05 08:29:43 +00:00
f957e640b9 fix(otel): handle OTLP/JSON string-encoded numeric attribute values (#14047)
OTLP/JSON encodes int64 fields (e.g. attribute intValues) as decimal
strings per the spec, since JSON numbers cannot represent the full
int64 range. The attribute converter only handled the Long object
produced by the protobuf decoder and a plain number, so a JSON-encoded
intValue like "7" fell through to `high * 2^32 + low` with `high`
undefined and produced NaN. That NaN propagated into resourceAttributes
/ metadata and was rejected by ingestion validation
(invalid_union on body.metadata), so spans from OTLP/JSON exporters such
as the OpenTelemetry PHP SDK were dropped.

Centralize int parsing in convertOtelIntValue, which handles the Long
object, plain number, and decimal-string representations and returns
undefined (falling back to JSON.stringify) instead of NaN for
unparseable values. Also coerce string-encoded doubleValues, keeping
non-finite specials ("NaN"/"Infinity") as JSON-valid strings.

Adds otelMapping coverage for string-encoded int/double attributes and a
regression test for the reported process.pid resource attribute.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 07:39:08 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
29daa0f950 ci(deps): bump the github-actions group with 4 updates (#14045)
Bumps the github-actions group with 4 updates: [aws-actions/configure-aws-credentials](https://github.com/aws-actions/configure-aws-credentials), [github/codeql-action](https://github.com/github/codeql-action), [docker/login-action](https://github.com/docker/login-action) and [docker/metadata-action](https://github.com/docker/metadata-action).


Updates `aws-actions/configure-aws-credentials` from 6.1.1 to 6.1.2
- [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/d979d5b3a71173a29b74b5b88418bfda9437d885...acca2b1b2070338fb9fd1ca27ecee81d687e58e5)

Updates `github/codeql-action` from 4.35.5 to 4.36.0
- [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/9e0d7b8d25671d64c341c19c0152d693099fb5ba...7211b7c8077ea37d8641b6271f6a365a22a5fbfa)

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

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

---
updated-dependencies:
- dependency-name: aws-actions/configure-aws-credentials
  dependency-version: 6.1.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: github-actions
- dependency-name: github/codeql-action
  dependency-version: 4.36.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions
- dependency-name: docker/login-action
  dependency-version: 4.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions
- dependency-name: docker/metadata-action
  dependency-version: 6.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-05 07:10:57 +00:00
NimarandGitHub bcdfe596dc feat(agent): persist and restore agent sessions (#13720)
* feat(agent): persist and restore agent sessions

* simplify

* simplify

* simp

* no title

* fixes

* fix ordering

* delta sync

* move migration

* fix run ids

* fix: allow user to click and enable ai

* simplify after review

* move migration

* use events instead of messages

* simplify

* block stale

* ensure clean agent abortion

* clean

* proper titles

* fix

* simp error

* fix mession push

* fix unique

* show error

* feedback

* cleanup

* rename

* block non cloud

* simp

* leaner conversation create

* simplify schema

* add test

* simplify

* project scope

* review

* fix

* store less events

* stale

* fix

* buld

* more eventy

* add agents

* ids

* switch ids to cuid

* fix

* index for faster deletions

* also add seed data
2026-06-05 06:35:42 +00:00
NimarandGitHub e8e9fc6dfb chore(dx): ignore playwright mcp files (#14046) 2026-06-05 08:27:34 +02:00
1d863d41d9 feat(blob-storage): decouple export source visibility from V4 beta toggle (#14032)
Replace the per-user `isBetaEnabled` gate on the export source picker
and export field groups with a deployment-level `eventsExportAvailable`
check (`isEnrichedBlobExportAvailable(isLangfuseCloud)`), so all Cloud
users on pre-cutoff projects see consistent form behaviour regardless of
their personal beta flag.

Co-authored-by: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
2026-06-04 17:09:01 +00:00
Ben BachemandGitHub 9c866258c5 chore: Setup storybook branch deployments (#14030) 2026-06-04 15:34:10 +00:00
marliessophieandGitHub 4c89785249 fix(datasets): gate remote experiment config reads (#14009) 2026-06-04 15:21:38 +00:00
Valery MeleshkinandGitHub ed8123ab2d feat: introducing deletion mask cleaner (#14035) 2026-06-04 15:10:53 +00:00
Ben BachemandGitHub 8d20254255 docs(agents): Add Storybook skill (#13657) 2026-06-04 14:22:32 +00:00
Nikita KabardinandGitHub 00f541126c fix(ui): only mount right drawer layout when active (#14025)
* fix(ui): only mount right drawer layout when active

* fix(ui): avoid empty right drawer without remounting content
2026-06-04 12:35:11 +00:00
NimarandGitHub 2253190c06 chore(seeder): default ai features on in seeder (#14033) 2026-06-04 11:01:44 +02:00
Max DeichmannandGitHub a1884e7c0c docs(agents): lighten shared agent guidance (#14000)
* docs(agents): lighten shared agent guidance

* docs(agents): remove shared skills index

* push

* docs(agents): apply review feedback

* Revert "docs(agents): apply review feedback"

This reverts commit 6b6cac9df323a474b2082203f0b6a630ac803a4b.

* docs(agents): restore root guidance notes

* docs(agents): preserve skill entrypoint context

* docs(agents): remove duplicate skill references

* docs(agents): address skill docs review comments

* docs(agents): address follow-up review comments

* docs(agents): restore Fern API guidance
2026-06-03 18:44:03 +00:00
Nikita KabardinandGitHub ac9a416990 fix(web): avoid native title overlap on help tooltips (#14028) 2026-06-03 16:13:14 +00:00
Tobias WochingerandGitHub f1ac008239 chore(shared): trace code eval dispatches (#14024)
* feat(shared): trace code eval dispatches

* fix(shared): align code eval dispatch trace attributes
2026-06-03 14:27:43 +00:00
Ben BachemandGitHub f992ede78c chore: Add dark/light mode toggle to Storybook (#14023) 2026-06-03 12:21:58 +00:00
Tobias WochingerandGitHub 14aa2bb75a ci: limit SDK workflow write token exposure (#14008)
* ci: limit SDK workflow write token exposure

* ci: disable git hooks during SDK workflow pushes

* ci: harden SDK workflow push steps

* test(worker): allow Floci integration warmup

* ci: isolate SDK workflow push jobs

* ci: resolve SDK reviewer before push
2026-06-03 11:45:19 +02:00
Ben BachemandGitHub 212b09cc16 fix(agent): Pass redis to deleteApiKeyFromDb (#14020)
fix(agent): Pass redis to deleteApiKeyFromDb
2026-06-03 09:34:27 +00:00
Ben BachemandGitHub dc252fb4ca feat(mcp): Handle numeric colums in getObservationFilterValues (#13795) 2026-06-03 09:08:48 +00:00
NimarandGitHub 4f6381e8a5 chore(deps): bump pnpm to 11.4.0 (#14012) 2026-06-03 09:58:49 +02:00
Max DeichmannandGitHub 06e3422550 fix(auth): add public key to auth spans (#14011)
Add public key to auth span metadata
2026-06-02 17:28:43 +00:00
472b1df5fc feat(web): add MCP & CLI settings page and agent tools banner (#14007)
Adds a new "MCP & CLI" project settings page that introduces users to the
Langfuse Agent Skill, MCP server, and CLI. The page is content-only (no
functionality) with a short description, copyable install/usage snippets, and
links to the docs for each tool.

Also adds a dismissible informational banner on the organization overview page
highlighting that Langfuse works well with AI coding agents (Claude Code,
Codex, etc.) via the Agent Skill, MCP server, and CLI. The banner reuses the
existing Callout primitive (localStorage-backed dismissal with TTL).

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 16:44:46 +00:00
4170b025d5 fix(mixpanel): use empty distinct_id for events without a user (#14010)
* fix(mixpanel): use empty distinct_id for events without a user

Events from automated evaluators have no user context — the trace they
belong to genuinely has no user_id, not a missing one. Using the
per-event insert_id as distinct_id was counting each evaluation as a new
unique Mixpanel user, inflating MTU billing.

Mixpanel's documented approach for events not attributable to any user is
an empty string distinct_id, which it distributes across shards without
creating user profiles or incurring MTU cost. This avoids both the
billing spike and the hot-shard risk that a shared sentinel like
"langfuse_unknown_user" would carry at high event volume.

The langfuse_user_id property (distinct from distinct_id) is set to
"langfuse_unknown_user" to match the documented property value.

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

* refactor(mixpanel): drop langfuse_user_id property override

The property already flows through via ...otherProps unchanged — adding an
explicit override risked breaking customers who depend on the current null
value. Only distinct_id needed fixing.

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

---------

Co-authored-by: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
2026-06-02 16:21:14 +00:00
Jannik MaierhöferandGitHub d85a04c0c4 docs: update hero image in README.md 2026-06-02 16:50:16 +02:00
Tobias WochingerandGitHub 6d6d686399 ci: use Slack workflow webhooks for failure notifications (#14004)
* ci: use slack workflow webhooks for failure notifications

* ci: add temporary slack workflow webhook test

* ci: test slack webhook on pr synchronize

* ci: remove temporary slack webhook test
2026-06-02 16:15:48 +02:00
Tobias WochingerandGitHub 9ce4aa4752 feat(llm): support OpenAI Responses API connections (#14002)
* feat(llm): support OpenAI Responses API connections

Add OpenAI LLM connection config for routing compatible endpoints through LangChain's Responses API.

* fix(llm): preserve VertexAI config parsing

Keep VertexAI config ahead of OpenAI config so empty VertexAI config is not defaulted as OpenAI Responses API config.
2026-06-02 13:58:06 +00:00
Nikita Kabardin 0c62f7153c chore: release v3.178.0 2026-06-02 15:26:46 +02:00
Ben BachemandGitHub 2f738af2bb fix(annotation-queues): Race-safety of createAnnotationQueueForApi (#13971)
* fix: Race-safety of createAnnotationQueueForApi

* Remove duplicated tests
2026-06-02 12:55:10 +00:00
699e82b9fb fix(web): avoid duplicated basePath in Change Password link ; fixes #13736 (#13738)
* fix(web): avoid duplicated basePath in Change Password link ; fixes #13736

* chore: linting

---------

Co-authored-by: Steffen Schmitz <steffen@langfuse.com>
Co-authored-by: Steffen Schmitz <steffenschmitz@hotmail.de>
2026-06-02 12:33:26 +00:00
Ben BachemandGitHub 937e405915 feat(mcp): Add optional id to upsertDataset (#13946) 2026-06-02 12:29:28 +00:00
Ben BachemandGitHub f41085dd54 fix(datasets): Add callout for v4 if dataset run items are loading too long (#13970)
fix: Add callout for v4 if dataset run items are loading too long
2026-06-02 12:28:43 +00:00
Nikita KabardinandGitHub 663230dbba fix(ui): better trace detail header spacings (#13992)
* fix(ui): better trace detail header spacings

* Fix the icon shrinking

* fix(ui): make observation header title more tidy and in sync with traces
2026-06-02 12:07:58 +00:00
Ben BachemandGitHub 8003ff9060 refactor: Fix void operator linting issues (#13998) 2026-06-02 12:04:21 +00:00
Ben BachemandGitHub 2f4db330b9 refactor: Remove void operator usages (#13963) 2026-06-02 13:56:15 +02:00
Tobias WochingerandGitHub 7cd43cde7c feat(web): derive code eval support from dispatcher (#13979)
* feat(web): derive code eval support from dispatcher

Remove the public code eval UI flag and gate self-hosted code evaluator support from the configured dispatcher.

* fix(web): stabilize eval template validation dependencies

* fix(web): validate code eval table actions

* fix(web): repair code eval CI failures

* fix(web): stabilize code eval test run env

* fix(web): stabilize detail page list context

* test(worker): stabilize unrelated ingestion flake

Fix an unrelated flaky worker ingestion integration test that timed out in CI while validating code eval changes.
2026-06-02 13:31:57 +02:00
Ben BachemandGitHub 3bc8c6293e fix(agent): Remove explicit LANGFUSE_AWS_BEDROCK_REGION precondition (#13991) 2026-06-02 09:35:33 +00:00
Ben BachemandGitHub c1ab28d9be refactor(comments): Make comment TRPC routes read from events table (#13473)
* refactor(comments): Make comment TRPC routes read from events table

* Address PR comments
2026-06-02 09:05:46 +00:00
aa4cb4aa45 chore: point playwright folder to /tmp (#13860)
* avoid creating noise for intellij to pick up

* chore: standardize playwright-mcp output dir to /tmp/playwright-mcp

Update all references from the old `.playwright-mcp/` repo-local path to
`/tmp/playwright-mcp`, and remove the now-obsolete .gitignore entry.

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

---------

Co-authored-by: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
2026-06-02 08:28:28 +00:00
18c6d0f350 fix(security): enforce auditLogs:read and audit-logs entitlement for audit_logs batch exports (#13980)
* fix(security): enforce auditLogs:read and audit-logs entitlement for audit_logs batch exports

A project member with batchExports:create could request a batch export
of the audit_logs table, bypassing the stricter auditLogs:read RBAC scope
and audit-logs entitlement checks used by the normal audit log route.
The worker would then stream raw audit log rows to blob storage.

Add table-level authorization in batchExport.create: when tableName is
audit_logs, require both the audit-logs entitlement and auditLogs:read
project scope — mirroring the checks in auditLogs.allByProject.

Fixes LFE-10025.

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

* chore: remove comment from audit log batch export guard

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

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-02 08:28:05 +00:00
Hassieb PakzadandGitHub 03df79d528 chore(worker): add eval execution span attributes (#13961)
* chore(worker): add eval execution span attributes

* fix(worker): omit observation names from eval span attributes

* push

* push
2026-06-01 18:16:51 +00:00
8c6d09b91a feat(agent): Connect in-app agent to langfuse MCP (#13747)
Co-authored-by: Nimar <l.nimar.b@gmail.com>
2026-06-01 15:20:03 +00:00
Nimar 9bba37054c chore: release v3.177.1 2026-06-01 17:11:51 +02:00
Jannik MaierhöferandGitHub 4c8a23e64c docs: update readme banners (#13975)
Updated images and links in the README file, removed outdated references, and improved clarity of the Langfuse description.
2026-06-01 13:55:59 +00:00
Ben BachemandGitHub 0850ef3a6f refactor(datasets): Merge dataset services (#13962) 2026-06-01 11:58:43 +00:00
marliessophieandGitHub dc2f057ac4 refactor(code-evaluators): TS and python code formatting to auto-collapse comments (#13972)
* chore: update pnpm-lock and package.json for @codemirror/lang-javascript and adjust theme colors

* fix(evals): increase debounce time in useCodeEvalSourceValidation and simplify isValid calculation

* chore: push
2026-06-01 11:58:32 +00:00
Ben BachemandGitHub 3d7da7043b fix(mcp): Normalize fields in queryMetrics tool (#13942) 2026-06-01 11:58:15 +00:00
Ben BachemandGitHub 8145edffb5 fix(mcp): Update descriptions for createScore (#13945) 2026-06-01 11:58:01 +00:00
NimarandGitHub a15b92e4cc chore(deps): bump axios to 1.16.0 (#13973)
* chore(deps): bump axios to 1.16.0

* bump skill
2026-06-01 11:53:21 +00:00
Ben BachemandGitHub a8b69658f5 fix(mcp): Be more liberal in accepting dataset input and output schema (#13938) 2026-06-01 11:49:00 +00:00
Ben BachemandGitHub 736cdc4727 refactor(web): Align peek view detail components (#13964)
refactor: Align peek view detail components
2026-06-01 11:48:42 +00:00
Ben BachemandGitHub c113fbff77 fix(mcp): Prompt retrieval defaults to production (#13943)
fix(mcp): Prompt retrieval defaults to production when created only have latest
2026-06-01 11:45:32 +00:00
Ben BachemandGitHub 1795468dc5 fix(mcp): Update score tool descriptions (#13936) 2026-06-01 11:45:03 +00:00
Ben BachemandGitHub b7bf898d4a fix(mcp): Improve description for 'latest' label (#13937) 2026-06-01 11:44:55 +00:00
Niklas SemmlerandGitHub 55b28e57c8 docs(agents): warn against is_deleted filter on ClickHouse reads (#13965)
* docs(agents): warn against is_deleted filter on ClickHouse reads

* docs(agents): scope is_deleted guidance accurately, acknowledge legacy filters and blob_storage_file_log
2026-06-01 11:34:41 +00:00
Ben Bachem 6a26032b85 chore: release v3.177.0 2026-06-01 12:01:01 +02:00
Max DeichmannandGitHub edf8669719 fix(agents): require exhaustive datadog alert sweep (#13968) 2026-06-01 09:26:42 +00:00
Valery MeleshkinandGitHub 4f99e03cca chore: adding a few more tests after #13644 (#13967) 2026-06-01 09:16:40 +00:00
marliessophieandGitHub dc7727b08c fix(evals): hide duplicate SDK warning in fast preview (#13947)
* fix(evals): hide duplicate SDK warning in fast preview

* chore: push

* chore: push
2026-06-01 08:58:43 +00:00
cdcdbf8304 feat(ai): Add toggle for AI telemetry (#13939)
feat(ai): Add toggle for ai telemetry

Co-authored-by: Nimar <l.nimar.b@gmail.com>
2026-06-01 08:52:42 +00:00
Andrew HornandGitHub b6c2e91455 fix(search): match escaped unicode content in full-text search - Issue #11538 (#13644)
* test(search): add failing tests for non-English full-text search (issue #11538)

Reproduces GitHub issue #11538: full-text search over trace/observation input/output
returns nothing for non-ASCII text because the OpenTelemetry / Python-SDK ingestion path
stores I/O as JSON with ensure_ascii=True (so 你好 is persisted as the literal 你好),
while clickhouseSearchCondition only matches the raw query string.

- web/src/__tests__/server/multilingual-fulltext-search.servertest.ts: integration tests
  (via traces.all / generations.all tRPC + the ingestion API -> worker -> ClickHouse path)
  with one case per writing system used by >=1M people (separate Simplified vs. Traditional
  Chinese, separate Hiragana vs. Katakana), plus edge cases (input-only / output-only /
  mixed Latin+CJK queries, astral-plane / surrogate-pair characters, observations, the
  issue's exact {en,ar,zh} scenario) and a few unit assertions on the SQL builder.
- web/src/__e2e__/multilingual-search.spec.ts: Playwright e2e tests driving the Traces
  table UI with non-ASCII full-text queries.

All 47 tests fail today (honest assertion failures, not missing-module errors); they pass
once clickhouseSearchCondition also matches the JSON-\uXXXX-escaped form of the query.

* fix(search): match \uXXXX-escaped content in full-text search (issue #11538)

Trace/observation input and output ingested through the OpenTelemetry / Python-SDK path
are persisted in ClickHouse verbatim as JSON serialised with ensure_ascii=True, so a value
like 你好 is stored as the literal 你好. Full-text search built input ILIKE '%你好%',
which never matched, so non-English content was unsearchable while ASCII worked.

clickhouseSearchCondition now also matches the JSON-\uXXXX-escaped form of the query
(astral code points -> UTF-16 surrogate pair) on the input/output columns. ASCII-only
queries are unchanged: the escaped form is identical, so no extra parameter or ILIKE clause
is emitted and the existing query plan is preserved. Plain-string columns (id/user_id/name)
are untouched.

* fixed test comments so they're not stale anymore

* test(search): remove redundant multilingual e2e spec
2026-05-29 16:41:21 +00:00
Valery MeleshkinandGitHub 2f5553910b fix: refine LANGFUSE_DISABLE_LEGACY_TRACING_IO_SEARCH handling (#13929) 2026-05-29 15:10:40 +00:00
242e50ac10 perf(eval): skip FINAL and unused aggregations in checkTraceExistsAndGetTimestamp (#13934)
* perf(eval): skip FINAL and unused aggregations in checkTraceExistsAndGetTimestamp

The function is only used by evalService to decide whether a trace needs
evaluation. Drop the latency, usage_details, and cost_details aggregations
since they are not consumed, and remove FINAL from the traces and
observations reads. Updates are additive enough that a transient
non-matching state is acceptable in exchange for the performance gain.

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

* chore: remove outdated tests

* chore: remove outdated tests

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-29 14:21:24 +00:00
Niklas SemmlerandGitHub 09d33b269d perf(blob-export): replace observations FINAL with LIMIT 1 BY (#13935)
## Summary

Fixes three production OOM incidents ([LFE-10031](https://linear.app/langfuse/issue/LFE-10031)) traced to `MEMORY_LIMIT_EXCEEDED` on `FROM observations FINAL` in the v3 blob storage export.

`FINAL` forces a k-way merge-sort across all parts before the time-range `WHERE` can be applied — measured: **75 GiB read for 1.08M rows in ~9 minutes** on a single 1-hour export window. The replacement `ORDER BY event_ts DESC / LIMIT 1 BY` subquery reads the same window in **0.5 s, reading 1.3 MB**.
2026-05-29 16:19:55 +02:00
8e97196853 feat(ui): notification for MCP v2 (#13895)
* feat(ui): notification for code evals launch

* feat(ui): notification for mcp v2

* test(ui): make sidebar notifications test resilient to new entries

Derive the dismissed-notification list from the exported notifications
array instead of hardcoding launch-week IDs, so the GitHub star badge
test no longer needs an update each time a new notification is added.

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-29 12:20:01 +00:00
Tobias WochingerandGitHub b0481b4ae1 fix(eval): stop retrying context overflow errors (#13930) 2026-05-29 11:39:10 +00:00
Ben BachemandGitHub 2c5dd55f28 fix(mcp): Do not use intersection or union JSON schemas (#13927)
* fix(mcp): Do not use intersection or union JSON schemas

* Resolve PR comments

* Remove undesired changes

* Resolve PR comments
2026-05-29 09:59:30 +00:00
Valery MeleshkinandGitHub c77fcd9dad feat: introducing LANGFUSE_DISABLE_LEGACY_TRACING_IO_SEARCH flag to allow disabling FTS on v3 tables (#13912) 2026-05-29 09:29:09 +00:00
Ben BachemandGitHub 25eabbcd58 fix(mcp): Missing audit logs (#13915)
* fix(mcp): Missing audit logs

* Fix audit log in `createDatasetForApi`
2026-05-29 09:13:50 +00:00
Valery MeleshkinandGitHub 632aafa779 fix: tighten matches operator semantics (#13928) 2026-05-29 09:06:44 +00:00
77d95ad0d8 fix(api,mcp): Stabilize score config pagination order (#13832)
* Stabilize score config pagination order

* fix(score-configs): stabilize tRPC pagination order

* docs: prefer WSL and preflight local env

* chore: drop unrelated docs from score-config PR

---------

Co-authored-by: Nimar <l.nimar.b@gmail.com>
Co-authored-by: Ben Bachem <10088265+bezbac@users.noreply.github.com>
2026-05-29 08:42:29 +00:00
Ben BachemandGitHub d11d6fa69c fix(mcp): Use ids instead of names for dataset tools (#13916) 2026-05-29 08:35:35 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
f7bdaeb0f2 ci(deps): bump the github-actions group with 3 updates (#13924)
Bumps the github-actions group with 3 updates: [github/codeql-action](https://github.com/github/codeql-action), [actions/stale](https://github.com/actions/stale) and [zizmorcore/zizmor-action](https://github.com/zizmorcore/zizmor-action).


Updates `github/codeql-action` from 4.35.4 to 4.35.5
- [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/68bde559dea0fdcac2102bfdf6230c5f70eb485e...9e0d7b8d25671d64c341c19c0152d693099fb5ba)

Updates `actions/stale` from 10.2.0 to 10.3.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/b5d41d4e1d5dceea10e7104786b73624c18a190f...eb5cf3af3ac0a1aa4c9c45633dd1ae542a27a899)

Updates `zizmorcore/zizmor-action` from 0.5.3 to 0.5.6
- [Release notes](https://github.com/zizmorcore/zizmor-action/releases)
- [Commits](https://github.com/zizmorcore/zizmor-action/compare/b1d7e1fb5de872772f31590499237e7cce841e8e...5f14fd08f7cf1cb1609c1e344975f152c7ee938d)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-version: 4.35.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: github-actions
- dependency-name: actions/stale
  dependency-version: 10.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions
- dependency-name: zizmorcore/zizmor-action
  dependency-version: 0.5.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-29 09:35:32 +02:00
Ben BachemandGitHub 28fc124a3d fix(mcp): Throw if not exists in deleteAnnotationQueueAssignment (#13914)
fix(mcp): Throw if not exists in deleteAnnotationQueueAssignment
2026-05-29 06:47:40 +00:00
Ben BachemandGitHub 644a990a9d fix(mcp): Rename relevant tools from create to upsert (#13913) 2026-05-29 06:47:25 +00:00
Hassieb PakzadandGitHub ca8a92810b feat(model-prices): add claude-opus-4-8 (#13919) 2026-05-28 18:30:52 +00:00
edcb19e876 fix(traces): Infinite recursion in buildStepGroups (#13900)
Co-authored-by: Nimar <l.nimar.b@gmail.com>
2026-05-28 17:39:12 +00:00
490779fd34 chore(mcp): Update MCP server version (#13883)
feat(mcp): Update MCP server version

Co-authored-by: Nimar <l.nimar.b@gmail.com>
2026-05-28 17:30:50 +00:00
5714d41b2e feat(ui): notification for code evals launch (#13894)
* feat(ui): notification for code evals launch

* test(ui): make sidebar notifications test resilient to new entries

Derive the dismissed-notification list from the exported notifications
array instead of hardcoding launch-week IDs, so the GitHub star badge
test no longer needs an update each time a new notification is added.

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-28 17:11:55 +00:00
Tobias WochingerandGitHub a7a13b54c3 ci: notify slack on critical workflow failures (#13910)
* ci(sdk): notify slack on api spec workflow failure

* ci: notify slack on release and deploy failures

* ci: reuse slack failure notification workflow
2026-05-28 16:45:10 +02:00
NimarandGitHub 6c2faf10d8 chore(deps): dedupe and bump (#13911) 2026-05-28 14:37:15 +00:00
Tobias WochingerandGitHub 41f584782e ci(sdk): use repo token for SDK spec workflow (#13909)
ci(sdk): use repo token for sdk spec workflow

Remove the protected branches environment so the SDK API spec job uses the repository GH_ACCESS_TOKEN instead of the environment-scoped token.
2026-05-28 15:50:26 +02:00
3940484e42 refactor(mcp,api): Create shared services (#13879)
* refactor(mcp,api): Create shared services

* re-add comments

* revert to previous pub api behaviour

* revert prev api behaviour

---------

Co-authored-by: Nimar <l.nimar.b@gmail.com>
2026-05-28 13:39:59 +00:00
2048fee518 fix(llm): harden LLM connection fetches (#13797)
* fix(llm): secure google model fetches

Route Google AI Studio and Vertex AI LangChain calls through validated secure fetch clients.

* fix(llm): secure openai and anthropic fetches

Route OpenAI, Azure OpenAI, and Anthropic LangChain calls through validated secure fetch.

* fix(llm): preserve google ai studio base url prefixes

* fix(llm): simplify secure google fetch clients

* fix(llm): preserve proxies and bump langchain core

Keep explicit undici dispatchers on secure LLM fetches so HTTPS_PROXY is honored, including Google clients. Bump @langchain/core to 1.1.48 to pick up the CJS uuid export fix.

* fix(llm): avoid dispatcher type conflicts

Keep proxy dispatchers opaque across secure fetch wrappers to avoid mixing undici and undici-types Dispatcher identities during typecheck.

* refactor(llm): clarify dispatcher handoff in secure outbound fetch

Document that any caller-provided dispatcher takes ownership of
connection-time safety, share the dispatcher-aware RequestInit type, and
harden the Google secure API client tests against NEXT_PUBLIC_LANGFUSE_CLOUD_REGION
env contamination.

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

* refactor(llm): drop redundant proxy fetchOptions and tighten secure fetch tests

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

* fix(llm): handle ChatGoogle mixed content blocks and thought parts

The unified @langchain/google SDK emits tool-calling responses as a mixed
array of `{type: "text"}` and `{type: "functionCall"}` blocks, and marks
reasoning text with `thought: true` instead of `type: "reasoning"`.
Accept a per-element content union and detect thought blocks so VertexAI
thinking + tool calling parses again.

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

* chore(deps): drop @langchain/core release-age exception

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

* refactor(llm): consume standard contentBlocks for reasoning and tool calls

Route every chat model through @langchain/core's documented
AIMessage#contentBlocks accessor instead of inspecting raw provider
content. The registered translators normalize Bedrock reasoning_content,
Gemini thought parts, and Anthropic thinking/tool_use blocks into the
standard { type: "reasoning" | "text" | "tool_call" | ... } shape, so
splitAIMessage no longer needs per-adapter block-type sets or
undocumented field checks.

Tightens streaming to handle AIMessageChunk explicitly and replaces the
ad-hoc Anthropic/Google content unions in ToolCallResponseSchema with a
single standard ContentBlock shape.

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

* test(llm): use real bedrock message instances

* fix(llm): ignore caller dispatchers in secure fetch

* fix(llm): pass google thinking options flat

* fix(llm): mark secureLlmFetch validation errors as non-retryable

Synchronous errors from validateLlmConnectionBaseURL and the
fetchWithSecureRedirects error classes carry no HTTP status, so the
catch block defaulted them to 500 + retryable and re-enqueued
permanently broken configs against the 24h eval-retry budget.

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

* fix(llm): walk error cause chain for non-retryable pattern check

Anthropic/OpenAI/Azure SDKs wrap synchronous custom-fetch errors as
APIConnectionError { message: "Connection error.", cause: original },
so the secureLlmFetch validation patterns added in the previous commit
never matched for those three adapters. Walking the .cause chain (with
cycle guard) makes the non-retryable classification fire end-to-end.

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

* fix(llm): surface secure fetch validation messages

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 13:21:50 +00:00
Tobias WochingerandGitHub 9029502aef fix(evals): tighten eval log IO cell padding (#13906)
* fix(evals): tighten eval log io cell padding

* fix(evals): align eval log loading cell padding
2026-05-28 13:13:48 +00:00
0a45d7dded fix(dashboards): drop scoreName filter on traces and observations views (#13901)
* fix(dashboards): drop scoreName filter on traces and observations views

The dashboard exposes a global "Score Name" filter that gets routed to
every widget via dashboardUiTableToViewMapping. The traces and
observations entries mapped it to a scoreName column, but neither
traceView nor observationsView declares a scoreName dimension in the
query data model. queryBuilder.resolveDimension then hit the generic
*Name -> name fallback and silently rewrote the filter to traces.name
or observations.name -- so picking a score label appeared to match
trace names instead.

Removing the mappings lets the filter partition as unsupported on those
views (still applied correctly on scores-numeric / scores-categorical),
which avoids the silent miscarriage without changing the score-side UX.

Fixes LFE-9773.

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

* fix(dashboards): hide scoreName and observationName filters on traces

Follow-up to the previous commit. The widget builder's filter-column
dropdown is fed by web/src/features/widgets/components/widgetFilterColumns.ts,
not by dashboardUiTableToViewMapping. "Observation Name" and "Score Name"
were in the unconditional base list, so they kept appearing for every
selectedView -- including traces, where neither is a real dimension.
Removing them from the mapping silenced the wrong-query bug but the UI
still misleadingly offered the options.

Gate both columns per-view:
- Observation Name: only on observations / scores-numeric / scores-categorical.
- Score Name: only on scores-numeric / scores-categorical.

Also drops observationName from the traces entry in
dashboardUiTableToViewMapping (same 1:n problem as scoreName: traceView
has no observationName dimension, so the *Name->name fallback would
silently rewrite to traces.name).

Refs LFE-9773.

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-28 12:41:37 +00:00
Ben BachemandGitHub b9f8c1e7cf chore: Add .superset/ to .gitignore (#13905) 2026-05-28 12:23:06 +00:00
Ben BachemandGitHub a8f5f17585 docs: Update test running instructions (#13902) 2026-05-28 11:56:45 +00:00
Tobias WochingerandGitHub d393d6bea7 fix(evals): improve code evaluator formatting (#13897)
* fix(evals): handle mac format shortcut by physical key

* fix(evals): support python evaluator formatting

Enable the code evaluator format action for Python by reusing Ruff WASM and preserving the generated editor prelude.
2026-05-28 09:20:17 +00:00
NimarandGitHub 099cbc76d0 fix(prompts): don't comment fetching on many prompt versions (#13870)
* fix(prompts): don't comment fetching on many prompt versions

* fix caching

* invalidate

* remove superfluous route
2026-05-28 09:07:56 +00:00
NimarandGitHub f0e96f0907 fix(comments): always show submit comment button (#13899) 2026-05-28 09:05:18 +00:00
NimarandGitHub 0a523abf51 fix(comments): don't show negative time stamps (#13898) 2026-05-28 08:51:27 +00:00
marliessophieandGitHub 14021334fd fix(evals): overflow and scroll behaviour for setting up eval form (#13896) 2026-05-28 06:12:24 +00:00
Jannik MaierhöferandGitHub 6d9e9f2639 feat(ui): add notification for full text search launch (#13889)
* feat(ui): add notification for full text search launch

* update test
2026-05-28 04:01:38 +00:00
Tobias WochingerandGitHub 37b16fd56f fix(evals): align code evaluator editor and runtime globals (#13891)
* fix(evals): align code evaluator editor and runtime globals

Keep code evaluator language drafts separate in the editor and allow the same async helpers/globals in validation and local execution.

* fix(evals): cover code eval promise and byte helpers

Add Promise combinators and Uint8Array helpers to the synthetic validator declarations so editor validation matches the local runtime.

* fix(evals): expand code eval validation globals

Round out URL and array declarations and avoid helper type collisions in the synthetic TypeScript validator environment.
2026-05-27 20:25:44 +00:00
Tobias WochingerandGitHub ffedf2b819 fix(evals): update code evaluator docs link (#13890) 2026-05-27 19:20:10 +00:00
Hassieb PakzadandGitHub 1fc22b4f10 feat(ingestion): add app root detection (#13718) 2026-05-27 11:47:59 -07:00
Tobias WochingerandGitHub 64d0d50432 fix(evals): surface invalid code eval results (#13887) 2026-05-27 18:17:53 +00:00
802384fd28 feat(ui): add notification for agent skills launch; stack notifications (#13864)
* feat(ui): add notification for agent skills launch; stack notifications

* test(ui): dismiss LW notifications in sidebar test

Stacked notifications only render the front card's content, so the GitHub
stars badge stays hidden while a higher-ranked Launch Week notification
is within its TTL. Pre-seed the dismissed list with the LW IDs so
github-star surfaces and the badge alt-text assertion remains stable.

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-27 17:08:30 +00:00
Tobias WochingerandGitHub 27f1b42a4d fix(web): allow experiment code eval configs without sample trace (#13886) 2026-05-27 16:40:16 +00:00
Tobias WochingerandGitHub 368aef0ef1 fix(evals): use correct events flag for code eval test runs (#13884)
* fix(evals): use events flag for code eval test runs

* test(evals): align code eval flag fixture

* test(evals): gate code eval tests on events flag
2026-05-27 18:01:51 +02:00
Ben BachemandGitHub 76622d14fd docs(mcp): Update README.md (#13858) 2026-05-27 15:55:47 +00:00
Ben BachemandGitHub 5a5b3d7876 fix(mcp): Remove deleteScore tool (#13877) 2026-05-27 15:55:40 +00:00
Tobias WochingerandGitHub cc83ea0573 fix(evals): parse mapped observation variables (#13875)
* fix(evals): parse mapped observation variables

* test(web): update code eval test expectation

* test(worker): update observation eval processor expectations
2026-05-27 13:54:55 +00:00
Valery MeleshkinandGitHub f4c605f34f feat: matches string filters that always uses TEXT index (#13863)
* feat: introducing matches operator that is guaranteed to use FTS index.

* chore: refactor and consolidate a bit

* chore: addressig review and fixing CI

* chore: addressing review comments

* chore: clarifying API docs
2026-05-27 14:26:45 +02:00
marliessophieandGitHub ece9e9db0f fix(evals): add info alert for updated evaluator in NewEvaluatorPage (#13876) 2026-05-27 12:20:10 +00:00
Max DeichmannandGitHub ba8eea890b docs(agents): replace prod regression skill with weekly review (#13873)
* docs(agents): add weekly production review skill

* docs(agents): remove prod regression skill

* docs(agents): clarify weekly review bug table
2026-05-27 13:21:11 +02:00
Ben BachemandGitHub 28f3f474aa refactor(web): Disallow limit 0 in paginationZod (#13869) 2026-05-27 10:52:22 +00:00
Tobias WochingerandGitHub c4bec3b69b build(web): add code eval env to Docker image (#13871)
build(web): add code eval public env to image build
2026-05-27 12:42:39 +02:00
Tobias WochingerandGitHub 70b9f0301e docs: add code evaluator
Updated the Evaluations section to include 'Code evaluators' as part of the LLM application development workflow.
2026-05-27 12:18:56 +02:00
cf24ee840a feat(evals): add code-based eval (#13685)
* feat(evals): add code-based eval template data model

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

* fix(worker): mark unsupported eval templates unrecoverable

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

* fix(evals): filter public evaluator creation by template type

* fix(evals): scope default model blocking to llm templates

* fix(seed): include eval template type in upsert key

* test(evals): use enum values for eval template types

* test(evals): avoid import type lint warnings

* fix(evals): scope public rule name checks to llm templates

* revert(evals): drop type from eval template unique constraint

* feat(evals): add code eval execution queue (#13696)

* feat(evals): add code eval execution queue boundary

* fix(evals): forward eval deps to observation executors

* fix(evals): skip observation configs without templates

* refactor(evals): build score metadata in shared observation flow

* feat(evals): implement local and AWS Lambda code eval dispatchers (#13755)

* feat(evals): implement local and AWS Lambda code eval dispatchers

* chore(evals): suppress semgrep findings on intentional code-eval runners

* chore(evals): exclude code-eval runners from semgrep scan

* fix(evals): run local code eval dispatcher in worker_threads to avoid ESM registry leak

* fix(evals): preserve typed extracted variables and share lambda client across invocations

* fix(evals): use nested code eval execution context

* test(evals): cover code-based observation eval execution

* ci(evals): dump floci diagnostics on worker test failures

* ci(evals): run floci as root for docker socket access

* fix(evals): address code eval runner review feedback

* fix(evals): use pythonic experiment context fields

* fix(evals): mark local code eval dispatcher insecure

* fix(evals): hide code eval internal environment

* fix(evals): trace failed code eval executions

* fix(evals): address code eval review feedback

* fix(evals): address dispatcher review follow-ups

* fix(evals): make code eval traces best effort

* refactor(evals): centralize code eval error codes

* feat(evals): add code eval test run endpoint (#13749)

* feat(evals): add code eval test run endpoint

* fix(evals): trace code eval source code

* test(evals): update observation eval schema fixture

* fix(evals): drop experiment item metadata mapping

* fix(evals): address code eval test run feedback

* fix(evals): type check test run trace auth

* fix(evals): address code eval review feedback

Pass experiment item metadata into code eval payloads, restore score-count tracing on existing eval spans, and remove the unused LLM evaluator environment parameter.

* fix(worker): gate legacy eval creation to llm templates

Skip the legacy trace/dataset eval-create path for non-LLM templates so code evals do not enqueue work through the LLM-only queue.

* feat(evals): allow code eval score metadata

Accept object metadata on code-eval scores and merge it with execution metadata when persisting score events.

* fix(worker): mask code eval internal trace errors

* fix(evals): preserve experiment payload for observation evals

* test(evals): align experiment payload expectations

* fix(evals): stabilize eval score ids (#13772)

Derive evaluator score IDs at score payload creation time and keep code eval test runs experiment-aware.

* refactor(evals): wrap up review comments (#13775)

* fix(worker): centralize code eval organization context

Avoid a per-execution project lookup by passing the organization id from the observation eval processor, and remove a duplicate code eval span attribute.

* fix(evals): remove configurable code eval environment

* fix(shared): bound code eval lambda invoke timeout

Keep the AWS SDK retry strategy unchanged while setting a throwing HTTP request timeout for Lambda invokes.

* refactor(worker): rename eval execution metadata param

* refactor(evals): filter code eval test templates in query

* refactor(worker): drop unused eval executor environment param

* refactor(worker): derive code eval job execution id

* refactor(worker): centralize observation eval dispatch

* fix(shared): increase code eval lambda request timeout

* fix(shared): reduce code eval queue retry attempts

* chore(dev): reduce floci compose mounts

* fix(web): narrow code eval test observation lookup

* ci: document floci compose profile usage

* fix(web): keep code eval test traces internal

* chore(shared): remove unused code eval template assertion

* fix(web): type internal eval environment fallback

* fix(evals): align batch prompt preview formatting

* fix(evals): return code eval trace timestamp

* fix(evals): require code eval score names

* fix(evals): mask internal code eval errors

Return public error codes for code eval dispatch failures while keeping raw dispatcher details in internal trace metadata.

* fix(evals): preserve retryable code eval errors

Run code eval sources as plain evaluate functions and keep retryable execution errors visible when queue retries are exhausted.

* fix(evals): normalize local code eval error messages

Read VM error messages without relying on cross-realm instanceof checks.

* fix(evals): improve code eval error guidance

* refactor(evals): remove duplicate LLM output debug log

* fix(evals): timeout async local code evaluators

* fix(evals): align frontend and backend code eval context

* fix(evals): support legacy observation code eval test runs

* feat(evals): add code eval web flow (#13784)

---------

Co-authored-by: Codex Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: marliessophie <74332854+marliessophie@users.noreply.github.com>
2026-05-27 09:46:41 +00:00
9c825ad025 fix(web): preserve trace URL filters when opening shared links in new tab (#13665)
* fix(web): preserve trace URL filters when opening shared links in new tab

* add test coverage1

* Fix lint

---------

Co-authored-by: Nimar <l.nimar.b@gmail.com>
2026-05-27 08:48:18 +00:00
62c5c775c1 fix(blob-export): surface root cause and query_id in blob storage error logs (#13854)
* attach query id to logs

* ensure causes are correctly reported

* re-add error stack

* fix(blob-export): surface root cause and query_id in blob storage error logs

Before this change, blob export job failures reported only "Failed to upload
file to S3 (buffered)" — masking ClickHouse OOM errors and making triage
require manual trace correlation.

- Append [query_id: <id>] to errors thrown from queryClickhouseStream so
  the query id survives the full error chain to the BullMQ job failure log
- Add formatErrorChain helper that walks .cause and joins messages with
  "caused by", used in logger.error and the rethrown job error so both the
  Datadog log and BullMQ failure entry show the full root cause inline
- Pass { stack } (not the Error) to logger.error to capture the stack
  without triggering Winston's message-concatenation behaviour
- Copy the original stack onto the rethrown error so the queue processor
  sees the real failure site, not the rethrow line

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

---------

Co-authored-by: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
2026-05-27 07:10:52 +00:00
Hassieb PakzadGitHubgreptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
1594b7cff5 fix(ui-saved-views): do not restore view filters if queryparams are provided (#13865)
* fix(ui-saved-views): do not restore view filters if queryparams are provided

* Update web/src/components/table/table-view-presets/hooks/useTableViewManager.ts

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>

---------

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2026-05-26 23:54:13 +00:00
Ben BachemandGitHub 06f8385b78 fix(api): Do not accept limit below 1 in pagination (#13855) 2026-05-26 15:24:50 +00:00
Ben BachemandGitHub 6a32893eb4 fix(mcp,api): Ensure consistent ordering (#13856)
fix(mcp,public-api): Ensure consistent ordering
2026-05-26 14:24:11 +00:00
Ben BachemandGitHub 1ba4cd8925 refactor(mcp): Clean up MCP server (#13850)
* refactor(mcp): Clean up MCP tool otel instrumentation

* refactor(mcp): Consistently allow in-app agent keys

* refactor(mcp): Consistently specify `destructiveHint`

* refactor(mcp): Use `meta` instead of `pagination`

* refactor(mcp): Extract tools into individual files

* test(mcp): Clean up tests

* fix(mcp): Improve tool descriptions
2026-05-26 13:01:35 +00:00
96f6e30098 fix: Fix typo in MembersTable component (#13827)
Fix typo in MembersTable component

Co-authored-by: Nimar <l.nimar.b@gmail.com>
2026-05-26 10:07:42 +00:00
Umair KhurshidandGitHub f620087270 docs(docker): use shallow clone when self-host Langfuse (#13834) 2026-05-26 09:00:58 +00:00
Ben BachemandGitHub b5f4356e39 feat(mcp): Make media available via MCP (#13798) 2026-05-26 08:09:00 +00:00
7c06d21e6e fix(mcp): inject root "type: object" so Tool.inputSchema satisfies MCP SDK (#13805)
* fix(mcp): inject root type: "object" for intersection/union inputSchema

Zod's JSON Schema converter emits intersections as bare `allOf` and unions
as `anyOf`, omitting the root `type` keyword. The MCP TypeScript SDK
validates `Tool.inputSchema.type` as `z.literal("object")`, so SDK-based
clients (e.g. Claude Code) reject these tools and silently drop them
from `tools/list`.

Normalize the generated JSON Schema so the root always declares
`type: "object"`. Draft-7 permits `type` alongside `allOf`/`oneOf`/`anyOf`
- all constraints must hold - so this is semantically a no-op for
already-conformant schemas.

This restores compatibility for `createScore`, `createScoreConfig`, and
`updateScoreConfig` introduced in #13781.

Fixes #13804

* refactor: Format code

* fix: Make `defineTool` type injection more robust

---------

Co-authored-by: Nimar <l.nimar.b@gmail.com>
Co-authored-by: Ben Bachem <10088265+bezbac@users.noreply.github.com>
2026-05-26 07:55:01 +00:00
NimarandGitHub 077ac485de chore(skills): update pnpm upgrade skill (#13847) 2026-05-26 07:52:07 +00:00
NimarandGitHub ca5cc9e87d chore(deps): bump qs to 6.15.2 (#13845) 2026-05-26 07:23:09 +00:00
NimarandGitHub 8d63c00388 chore(deps): bump uuid to 11.1.1 (#13844) 2026-05-26 07:11:49 +00:00
NimarandGitHub f28e793b75 chores(deps): bump hono to 4.12.21 (#13843) 2026-05-26 06:57:57 +00:00
Jannik MaierhöferandGitHub 151eac0a59 feat(ui): add notification for lw5-d1 (#13837) 2026-05-25 15:39:40 -07:00
Max DeichmannandGitHub ed5142cb9f docs(agents): include paging monitors in regression sweep (#13831) 2026-05-25 13:35:39 +00:00
291c351c8f feat(evals): allow manual batch runs on inactive evaluators (#13824)
Manual batch evaluation runs now bypass the LIVE/INACTIVE toggle so
users can re-evaluate historic data with a paused evaluator. Blocked
configs (auth/model issues) are still skipped.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 08:31:25 +00:00
NimarandGitHub 7c501c41b1 feat(mcp): add health,comments, datasets, annotationqueues models routes (#13783)
* feat(mcp): add health,comments, datasets, annotationqueues models routes

* fix

* bump mcp limit to be feature parity with api

* clean

* cleanup

* simp

* fix test

* clean

* better descr

* even better
2026-05-22 15:31:42 +00:00
288133ca07 feat(email): support AWS SES transport via default credential chain (#13670)
* feat(email): support AWS SES transport via default credential chain

Detect SES from a `ses://<region>` scheme in SMTP_CONNECTION_URL and build a
nodemailer transport on top of @aws-sdk/client-sesv2 using the default AWS
credential chain. SMTP and SES-over-SMTP (smtps://) paths are unchanged.

A new shared helper (createMailTransport / buildMailServerConfig) replaces the
duplicated createTransport(parseConnectionUrl(...)) call sites across the email
services and is wired through NextAuth's EmailProvider in web/src/server/auth.ts.

Refs langfuse/langfuse#13669

* docs(email): document ses:// URL form on SMTP_CONNECTION_URL

Refs langfuse/langfuse#13669

* fix(email): guard SES SentMessageInfo lacking rejected/pending fields

nodemailer's SES transport returns `{ envelope, messageId, response, raw }`
with no `rejected`/`pending` arrays, so `.concat()` on those undefined fields
threw on every successful SES send through the NextAuth password-reset path.

Refs langfuse/langfuse#13669

* test(email): fix expected SES transport name

nodemailer assigns `this.name = 'SESTransport'` (not "SES") at
ses-transport/index.js:23, so the assertion was wrong from the start.

Refs langfuse/langfuse#13669

* test(email): test parseSesRegion directly instead of probing SESv2Client

Inspecting `sesClient.config.region` returned the SDK's async region provider
(`AsyncFunction`) rather than the string we passed in, so the assertion
diverged from runtime behavior. Test the region extraction via the exposed
`__testing.parseSesRegion` helper instead and keep the transport-shape checks
limited to the dispatch boundary (transporter name + options-object shape).

No AWS credential resolution; full suite runs in ~15 ms.

Refs langfuse/langfuse#13669

---------

Co-authored-by: Steffen Schmitz <steffen@langfuse.com>
2026-05-22 17:15:30 +02:00
Valery MeleshkinandGitHub d498437724 feat: initial FTS implementation (#13782)
* feat: initial TEXT-index-friendly query level treatment

* feat: initial round of making queries TEXT-index friendly

* fix: switch to lower() for now as lowerUTF8 has issues in 25.12

* feat: add TEXT indexes to dev-tables

* fix: ensure IO search uses events_full

Fixes https://github.com/langfuse/langfuse/issues/13658

* chore: gate new tests behind events-only flag

* chore: addressing review comments

* chore: addressing review comments
2026-05-22 13:41:49 +00:00
NimarandGitHub c34eac83d4 chore(deps): bump toolnate to 2.0.1 (#13800) 2026-05-22 12:59:43 +00:00
NimarandGitHub 3d73152502 fix(tracing): sanitize external urls wholistically (#13791)
* fix(tracing): sanitize external urls wholistically

* keep comments

* fix

* fix
2026-05-22 12:21:25 +00:00
ca8d9d4bbc feat(mcp): Make scores available via MCP (#13781)
* feat(mcp): Make scores available via MCP

* Continue to accept empty string ids in the public scores API

* better error messages for agent

---------

Co-authored-by: Nimar <l.nimar.b@gmail.com>
2026-05-22 12:15:41 +00:00
Ben BachemandGitHub 33e5cfcebf feat(mcp): Make metrics available via MCP (#13769) 2026-05-22 11:48:22 +00:00
Ben BachemandGitHub b207a95c0a fix(prompts): Align prompt variable handling in UI with SDK/compiler (#13680) 2026-05-22 11:25:10 +00:00
Valery MeleshkinGitHubgreptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
4f99a4d78c fix: route more queries to readonly pools (#13793)
* chore: move blob exports from v1 observations onto the readonly pool

* chore: route most event table queries to read-only pool

* Update packages/shared/src/server/repositories/scores.ts

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>

---------

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2026-05-22 11:10:04 +00:00
NimarandGitHub 1ede00d88d fix(tool-calls): parse available tools and map them to input object (#13594)
* fix(tool-calls): parse available tools correctly from AI sdk

* add mapping test

* full otel mapping

* fix parsing

* add test

* only parse metadata if tools are found

* fix parse

* simplify

* comment todod

* fix available tools

* perf

* parse tools also from IO

* playground parse

* fix build

* fix

* adapters

* also make it work for new other adapters

* tighten remapping

* fix langgraph

* ordering
2026-05-22 09:34:29 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
f4e4f4c9a7 ci(deps): bump the github-actions group with 2 updates (#13787)
Bumps the github-actions group with 2 updates: [github/codeql-action](https://github.com/github/codeql-action) and [pnpm/action-setup](https://github.com/pnpm/action-setup).


Updates `github/codeql-action` from 4.35.3 to 4.35.4
- [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/e46ed2cbd01164d986452f91f178727624ae40d7...68bde559dea0fdcac2102bfdf6230c5f70eb485e)

Updates `pnpm/action-setup` from 6.0.7 to 6.0.8
- [Release notes](https://github.com/pnpm/action-setup/releases)
- [Commits](https://github.com/pnpm/action-setup/compare/739bfe42ca9233c5e6aca07c1a25a9d34aca49b0...0e279bb959325dab635dd2c09392533439d90093)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-version: 4.35.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: github-actions
- dependency-name: pnpm/action-setup
  dependency-version: 6.0.8
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-22 09:47:10 +02:00
Mark SalpeterandGitHub 300b564b26 feat(automations): narrow getAutomations by event source and event (#13779)
feat(automations): narrow getAutomations by eventSource and matches
2026-05-21 20:39:30 +00:00
Ben BachemandGitHub 7d2afa498e feat(agent): Update bedrock auth strategy (#13770) 2026-05-21 15:09:11 +00:00
2645ba459c feat(dashboards): import/export widget configs (#13011)
* working on the widget import/Export feature. Done for now but i am working on making it future-proof

* fixed minor edgecase in regards to malformed json

* minor fixes/changes

* working version

* lint fix

* safe commit

* safe commit

* changed error message to pass test

* sign off

* cleanup of classes and added filter-config. also added several code snippets to shared

* multi -> single upload

* undoing shared modules

* added claude preview changes

* more claude changes

* more claude changes

* claude review fix

* added claude review fix

* safe commit

* claude review fix

* cleanup

* more cleanup

* merge conflict hopefully resolved

* added claude correction

* merge fix

* fix(widgets): normalize traces imports and drop get parsing

* fix(widgets): narrow exported widget metric aggs

* fix(widgets): surface dropped import filters

---------

Co-authored-by: Nimar <l.nimar.b@gmail.com>
2026-05-21 13:50:00 +00:00
Steffen SchmitzandGitHub 5236cf1651 chore: bump clickhouse client to 1.18.5 (#13778) 2026-05-21 13:34:00 +00:00
364522a445 feat(auth): In-app agent API keys (#13692)
* feat(auth): In-app agent API keys

* Fix the MCP read tool tests

* Fix agent key auth for routes not using `verifyAuthHeaderAndReturnScope`

* Don't allow updating or deleting in-app agent keys

* Fix type errors and tests

* Consistently return 403

* Fix audit log order

* Don't throw ForbiddenError in `verifyAuthHeaderAndReturnScope`

* Revert irrelevant changes

* move migration

---------

Co-authored-by: Nimar <l.nimar.b@gmail.com>
2026-05-21 13:25:26 +00:00
Nimar d06593f625 chore: release v3.175.0 2026-05-21 14:41:53 +02:00
aa1545af74 feat(monitors): tRPC router, RBAC scopes, requireFeatureFlag middleware (#13771)
* feat(monitors): tRPC router, RBAC scopes, requireFeatureFlag middleware

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

* refactor(monitors): MonitorNotFoundError + drop redundant tRPC try/catch

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

* feat(monitors): grant MEMBER monitors:CUD

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

* docs(agents): terse error-handling block covering tRPC + REST

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

* improvements(agents): added error handling playbook

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 12:27:52 +00:00
Ben BachemandGitHub 264db8d81d chore: Improve vitest test lifecycle (#13753)
chore: Improve vitest setup and teardown
2026-05-21 09:13:14 +00:00
NimarandGitHub 0bcccfb0b8 chore(deps): bump ws to 8.20.1 (#13768) 2026-05-21 11:15:06 +02:00
c9274a20b7 feat(monitors): Service & Schema (#13725)
* feat(monitors): seeded the monitors package with schema and data validators

* feat(monitors): validate handlebars message templates against MonitorMessageContext

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

* test(monitors): align test imports + fixtures with refactored schema

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

* fix(monitors): address PR review feedback and codespell findings

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

* fix(monitors): coerce BigInt wire fields and add top-level barrel export

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

* fix(monitors): address remaining PR review feedback

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

* feat(monitors): added the monitor service

* fix(monitors): remove orphan features/monitor leftovers from MonitorService move

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

* improvement(monitors): cleaned up types

* improvement(monitors): refactor into a feature partition

* refactor(monitors): drop handlebars template validator and message field

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

* fix(monitors): include `key` in sortFiltersCanonically canonical order

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

* fix(monitors): fix js docs typo nit

* fix(monitors): enforce nonnegative schedulerBatchId on the queue wire schema

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

* fix(monitors): use Object.hasOwn for query validation + correct threshold-order message

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

* fix(monitors): canonicalize set-semantics value arrays in sortFiltersCanonically

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

* fix(monitors): narrow input DTO status + orderBy column; reject histogram

- Add MonitorWriteStatusSchema (active|paused) and use it on
  CreateMonitorInputSchema / UpdateMonitorInputSchema. `error-bad-query` is
  scheduler-owned; callers can no longer forge a broken state or clear a
  legitimately broken monitor without scheduler revalidation.
- Narrow MonitorListInputSchema.orderBy.column to the columns the admin
  table actually sorts on (name/status/severity/createdAt). Without this,
  an unknown column reached Prisma and raised a 500-class
  PrismaClientValidationError instead of a clean 400.
- Reject `histogram` aggregation in isValidQuery — it returns a
  bucket-array at the ClickHouse layer, but monitor thresholds are scalar.
  Catch at the input boundary rather than failing in the worker.

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

* feat(monitors): allow updatedAt as a sortable column on MonitorListInputSchema

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

* feat(monitors): expand orderBy columns + sort NULLS LAST on list

- Add severityChangedAt, alertedAt to the orderBy.column allowlist.
- Apply NULLS LAST unconditionally on list ordering so nullable columns
  (alertedAt, severityChangedAt) sort intuitively in both directions; no-op
  on non-nullable columns.
- Cover all 7 allowed columns in the input-schema test, plus a real-Postgres
  integration test asserting NULLS LAST holds under ASC and DESC for both
  nullable columns.

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

* feat(monitors): reorder severity + status enums; drop unused message column

Reorder MonitorSeverity to UNKNOWN, NO_DATA, OK, WARNING, ALERT so DESC
sorts in attention-priority order (ALERT first). Reorder MonitorStatus to
PAUSED, ACTIVE, ERROR_BAD_QUERY so DESC reads as ERROR_BAD_QUERY → ACTIVE
→ PAUSED. Drop the unused `message` column (templating was removed
earlier; the column was kept then under Option A and is now retired).

Migration uses the canonical Postgres enum-swap pattern (CREATE _new, cast
column via text, rename _old, drop _old, rename _new). Default values
preserved. Zod enum order + service mapper cases reordered to match the
new canonical sequence.

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

* fix(monitors): gate orderBy nulls last on the nullable column subset

Prisma's validator only accepts the { sort, nulls } object form on nullable
columns — unconditional nulls last raised PrismaClientValidationError for
the 5 non-nullable columns (name/status/severity/createdAt/updatedAt). Add
nullableOrderColumns typed against MonitorListOrderBy so the set stays in
sync with the sortable allowlist, attach nulls only on its members. Add
5 integration cases proving each non-nullable column list call goes
through.

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

* docs(monitors): trim stale JSDoc on MonitorListInputSchema

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

* test(monitors): cover schedulerBatchId invariance to property + value array order

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

* feat(monitors): reject non-stringObject metadata filters; lock scheduler batch id invariance under property order

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

* test(monitors): drop stale message field from prismaRow fixture

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

* fix(monitors): reject set-semantics filters with duplicate values; relocate JSDoc

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

* feat(monitors): reject scalar filters on array-typed dimensions; add positive set-semantics coverage

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-21 08:41:44 +00:00
Tobias WochingerandGitHub 7a4b92117b ci: adjust zizmor advanced security handling (#13767) 2026-05-21 10:46:15 +02:00
fb2d74f259 refactor(blob-export): gate pricing fields on model group, not usage (#13716)
* refactor(blob-export): gate pricing fields on model group, not usage

Previously input_price / output_price / total_price were enriched whenever
the `usage` field group was selected, and model_export columns (model_id,
provided_model_name, model_parameters) were fetched from ClickHouse for
both `model` and `usage` requests — even when only `usage` was asked for,
so the pricing lookup had the model_id it needed.

This split the semantics: `usage` gave you cost data, but enriched prices
came from asking for `usage` too. The model_export columns were then
silently dropped unless `model` was also selected.

After this change:
- `model` gates everything model-related: identification columns AND prices.
- `usage` covers only the cost/usage maps (usage_details, cost_details,
  total_cost, usage_pricing_tier_name) — no pricing lookup, no model_export
  fetch in ClickHouse.
- Selecting `usage` without `model` is cheaper (skips the model_export SQL
  field set) and produces no price columns in the output.

Changes:
- worker handler: `includePricing` gate collapsed into `includeModelId`
- shared events.ts: `needsModelFields` drops the `|| usage` branch
- analytics-integrations labels: prices moved to model description, removed
  from usage description
- unit tests: flip expectations to match new semantics

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

* refactor(blob-export): inline model_export selection into the field group loop

Now that needsModelFields is just fieldGroups.includes("model"), the
two-step pattern (skip "model" in the loop, select model_export below)
is redundant. Collapse into a single conditional branch inside the loop.

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

* refactor(blob-export): remove dead model_export scrub in enrichObservationStream

The else-branch that deleted model_id/provided_model_name/model_parameters
was needed when model_export was fetched for usage-without-model requests
(so the pricing lookup had a model_id, then the columns were scrubbed before
output). That code path no longer exists after gating model_export on the
model group only — the columns are never present in the row when model is
absent, so the deletes were a no-op.

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

* fix(blob-export): add usage_pricing_tier_id to usage group description

The usage FieldSet projects usage_pricing_tier_id but the UI label omitted
it, causing the column to appear undocumented in exports. Pre-existing gap
surfaced by the PR review.

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

* fix(blob-export): remove FieldSetName cast that was not imported

The refactor introduced `group as FieldSetName` but FieldSetName is not
imported in events.ts. Revert to the plain `group` call that main used,
which satisfies the TypeScript overload without an explicit cast.

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

* fix comment

---------

Co-authored-by: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
2026-05-21 08:10:03 +00:00
NimarandGitHub c4c4ffdaf2 chore(deps): bump turbo 2.9.14 (#13766)
* chores(deps): bump brace expansion 5.0.6

* chore(deps): bump turbo 2.9.14
2026-05-21 10:13:38 +02:00
NimarandGitHub 833f34c24f chore(deps): bump brace expansion 5.0.6 (#13765)
chores(deps): bump brace expansion 5.0.6
2026-05-21 10:09:22 +02:00
8cbad17b56 feat(dashboarding): support experiment dimensions and filters in observations view (#13602)
* feat(dashboarding): support experiment filters, dimensions in observations view via preset

* feat(widget-form): render experiment as view rather than preset

* Revert "feat(widget-form): render experiment as view rather than preset"

This reverts commit 841d8437c85b27897e57ccd97b3d12118856713f.

* refactor: handle metadata filter appropriately

* chore: push

* chore: push

* feat(widget-form): keep only view agnostic filters on view change

* feat(tests): add v2-only experiment filters for observations in dashboard widget tests

* fix(dataModel): enable highCardinality for experimentName and experimentDatasetId fields

* revert: rm experiment_metadata as dimension

* fix: add highCardinality flag for experimentId field

* chore: push

* fix: correct import path for views type in widgetFilterPresets

The import path was using a non-existent local path @/src/features/query/types
instead of the correct shared package path @langfuse/shared/query. This was
causing TypeScript build failures.

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

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-05-20 22:29:09 +00:00
Hassieb PakzadandGitHub 07d0fe0bff feat(models): add new Gemini models (#13750) 2026-05-20 21:58:21 +02:00
Tobias WochingerandGitHub 8575d1f501 test(web): scope projects api cleanup to seed org (#13758) 2026-05-20 18:58:34 +00:00
marliessophieandGitHub 5b121282ca feat(evals): add experiment item metadata mapping for experiment evaluators (#13702)
* feat(evals): add experiment item metadata mapping for experiment evaluators

* chore: push

* feat(evals): add experiment_item_metadata mapping and validation

- Updated PUBLIC_MAPPING_SOURCE_TO_INTERNAL_COLUMN to include experiment_item_metadata.
- Enhanced SUPPORTED_MAPPING_SOURCES_BY_TARGET to support experiment_item_metadata for the experiment target.
- Modified ExperimentEvaluationRuleMappingSource to include experiment_item_metadata in the schema.

* fix: protect against correct mapping in front-end

* fix: protect against correct mapping in back-end

* Revert "fix: protect against correct mapping in back-end"

This reverts commit 6f7a1f4cc11b01a8233e055063610608d3dca2ef.

* fix(evals): include experiment metadata in batch eval stream

* fix(evals): update fern mapping source contract
2026-05-20 17:54:57 +00:00
Max DeichmannandGitHub 04e70d09df feat(api): track api key ids in auth telemetry (#13605) 2026-05-20 17:41:56 +00:00
Tobias WochingerandGitHub b76b1234e8 fix(worker): truncate oversized GitHub dispatch payloads (#13752)
Fixes LFE-9891.
2026-05-20 16:21:46 +00:00
058f54581d feat(mcp): Make observations available via MCP (#13656)
* feat(mcp): Make observations available via MCP

* Improve filter schema

* bump readme

* add check for envs

* Remove feature flag for MCP observation tools

* Fix validation for empty fields array in ObservationFieldsSchema

* Import `OBSERVATION_FIELD_GROUPS_PUBLIC_API`

* Make `hasParentObservation` filter values boolean in MCP tools

* Make `ObservationMcpFilterSchema` stricter

* Use full table for filters on input, output, or metadata fields

* Improve `getObservationFilterValues` performance

* Undo irrelevant changes

* Add tie-breaker sorting to "group by" queries in events repository

* Improve documentation about metadata truncation

* Add `requiresKey` property to observation filter schema

* Extend filter schema MCP tests

* Respect LANGFUSE_ENABLE_EVENTS_TABLE_V2_APIS for MCP tools

* Remove invalid comment

* require filters to get full io / metadata

* cleanup test

---------

Co-authored-by: Nimar <l.nimar.b@gmail.com>
2026-05-20 10:17:56 +00:00
Niklas SemmlerandGitHub 612ffdfada fix(v2/observations): declare and normalize enrichment price fields (#13711)
## Summary

The v2 observations endpoint always returns `modelId`, `inputPrice`, `outputPrice`, and `totalPrice` on every response row. These fields are populated when the `model` field group is requested; otherwise they are `null`. They were flowing through the Zod `.loose()` passthrough undeclared, making them invisible in the API contract.

- **`fern/apis/server/definition/commons.yml`**: Add `modelId`, `inputPrice`, `outputPrice`, `totalPrice` to `ObservationV2` as `nullable<T>` (always present in the response, not optional). Includes gating docs explaining the `model` field group condition.
- **`web/src/features/public-api/types/observations.ts`**: Declare the three price fields in the `APIObservationV2` Zod schema alongside the existing `modelId`.
- **`web/src/pages/api/public/v2/observations/index.ts`**: Normalize `Decimal` price values to `number` in the route handler for wire-format parity with v1 (mirrors `transformDbToApiObservation`).
- **`packages/shared/src/domain/observation-field-groups.ts`**: Expand docstring with enrichment field gating notes and code cross-references.
2026-05-20 12:05:59 +02:00
Tobias WochingerandGitHub d56791bd91 fix(blob-storage): harden endpoint connection validation (#13694)
* fix(blob-storage): harden endpoint connection validation

Block blob storage DNS rebinding for S3-compatible and Azure endpoints while keeping self-hosted validation opt-in via allowlist env vars.

* fix(blob-storage): address endpoint validation review feedback

* fix(blob-storage): keep secure storage agents alive

* test(blob-storage): run worker integration suite as self-hosted

* test(blob-storage): run integration suite as self-hosted
2026-05-20 09:22:34 +00:00
annabellschaandGitHub ebf32c79dc feat(worker): add production monitoring evaluator templates (#13591)
* feat(worker): add production monitoring evaluator templates

* fix(worker): keep only new monitoring evaluator templates
2026-05-20 07:49:59 +00:00
Max DeichmannandGitHub 530d11aa5e chore: ignore local DeepSec workspace (#13722) 2026-05-19 17:47:45 +02:00
Mark SalpeterandGitHub 5e86ceb6ae feat(monitors): add Monitor Prisma schema and migration (#13677)
* feat(monitors): add Monitor Prisma schema and migration

* feat(monitors): add UNKNOWN severity as default for cold-start monitors

* feat(monitors): decouple Monitor.view from DashboardWidgetViews

* fix(monitors): align Monitor.id with cuid convention and wire createdBy/updatedBy FKs
2026-05-19 14:29:48 +00:00
Niklas SemmlerandGitHub 2d2b0fab0a fix(agents): repair Playwright MCP by using --save-session (#13683)
## Summary

Upstream `@playwright/mcp` renamed `--save-trace` to `--save-session`. The current `latest` build (v0.0.75) rejects the old flag with `error: unknown option '--save-trace'` and the server exits immediately, so Claude Code (and any other MCP client launching the server via `.mcp.json`) reports `Failed to reconnect to playwright`. This blocks the `frontend-browser-review` skill end-to-end.

Swapping to `--save-session` is upstream's straight rename and keeps the same intent: Playwright MCP writes its session artifacts (including traces) under `--output-dir .playwright-mcp`, which is what the skill (`.agents/skills/frontend-browser-review/SKILL.md`) tells reviewers to inspect on failure.

## Impacted packages

- `.agents/config.json` — canonical MCP server config (source of truth)
- `.agents/README.md` — illustrative snippet kept in sync to avoid re-introducing the stale flag via copy-paste

Generated provider configs (`.mcp.json`, `.claude/`, `.codex/`, `.cursor/`, `.vscode/`) are regenerated by `pnpm run agents:sync` and remain gitignored per the agent-setup contract — no changes need committing there.

## Verification

- `pnpm run agents:sync` — regenerates all provider shims with the new flag
- `pnpm run agents:check` — clean
- Manual launch with the new args (`npx -y @playwright/mcp@latest --isolated --save-session --output-dir .playwright-mcp --test-id-attribute data-testid`) — process stays alive past handshake (old `--save-trace` exited with code 1)
- Live confirmation: with the fix applied locally, the Playwright MCP tools loaded successfully in my Claude Code session, whereas `/mcp` had previously reported `Failed to reconnect to playwright`

<!-- greptile_comment -->

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

This PR corrects the Playwright MCP flag in the agent configuration from `--save-trace` to `--save-session`, which is the correct flag for automatic session recording per the Playwright MCP documentation.

- Both `.agents/config.json` (the canonical source) and `.agents/README.md` (which embeds the current JSON shape inline) are updated consistently, keeping documentation and config in sync.
- Generated shim files (`.claude/settings.json`, `.cursor/mcp.json`, etc.) are not committed to the repo and are regenerated from `.agents/config.json` via `pnpm run agents:sync`, so no further changes are needed in this PR.
</details>

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

Safe to merge — both changed files are updated consistently and the replacement flag is documented as correct by Playwright MCP.

The change swaps a single CLI flag in two files that are intentionally kept in sync (the canonical config and its embedded README snapshot). The flag --save-session is confirmed in the Playwright MCP documentation as the correct option for automatic session recording. Generated shim files are not committed and will pick up the corrected flag on the next pnpm install or agents:sync run.

No files require special attention.
</details>

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

```mermaid
%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[".agents/config.json\n(canonical source)"] -->|pnpm run agents:sync| B["Generated Shim Files\n(.claude/settings.json\n.cursor/mcp.json\n.vscode/mcp.json\n.mcp.json etc.)"]
    A -->|inline snapshot| C[".agents/README.md\n(documentation)"]
    B --> D["Playwright MCP Server\nnpx @playwright/mcp@latest\n--isolated\n--save-session\n--output-dir .playwright-mcp\n--test-id-attribute data-testid"]
    style D fill:#d4edda,stroke:#28a745
```
</details>

<sub>Reviews (1): Last reviewed commit: ["fix(agents): use --save-session for Play..."](https://github.com/langfuse/langfuse/commit/789bceb39b392e4a677e455c9d819ae864a17e8a) | [Re-trigger Greptile](https://app.greptile.com/api/retrigger?id=32588333)</sub>

<!-- /greptile_comment -->
2026-05-19 14:38:37 +02:00
Niklas SemmlerandGitHub 6019785c79 feat(blob-export): hide Export Source field for post-cutoff Cloud projects (#13681)
## Summary

Follow-up to [LFE-9688](https://linear.app/langfuse/issue/LFE-9688) /
[#13627](https://app.graphite.com/github/pr/langfuse/langfuse/13627).
Tracked as [LFE-9830](https://linear.app/langfuse/issue/LFE-9830).

- After PR #13627 merged, post-cutoff Cloud projects saw a one-option Export Source dropdown. Team feedback: hide the field entirely.
- Wrap the FormField in `{showExportSourceField && (...)}` where `showExportSourceField = isBetaEnabled && !isPostCutoffCloud`. Composes with the existing `isPostCutoffCloud` derivation — `isLegacyBlobExportAllowed` is called exactly once per render.
- Form value stays pinned to `EVENTS` via the existing `defaultValues` / `reset` logic so submission is valid even with the field hidden (`react-hook-form` retains values of unmounted fields by default).
- Drops dead `availableExportSourceOptions` filter, the unreachable `isPostCutoffCloud` arm of `FormDescription`, and the unused `LEGACY_BLOB_EXPORT_SOURCES` import.

### Why no unit test

The rendering condition is a single-line AND of two existing booleans. The cutoff-bracket logic lives in `isLegacyBlobExportAllowed` (covered by its own tests in the shared package). Browser review of the affected page is the intended safety net — see the Test plan below.

### CI fix (fixup commit)

This PR also removes `--experimental-cli` from the `prettier-check` CI job (`pipeline.yml`). The flag's glob resolver treats bracket characters in Next.js dynamic-route paths (e.g. `[projectId]`) as glob character classes, causing exit 123 ("No files matching the given patterns were found") for any PR that touches a file under such a directory. `blobstorage.tsx` lives under `[projectId]`, which is what triggered the failure here. Standard prettier resolves explicit file paths correctly; the parallelism and ephemeral cache that `--experimental-cli` adds provide no practical benefit when checking a handful of changed files per PR.

### Impacted packages

- `web` — single page component, net 10+/26−.
- `.github/workflows/pipeline.yml` — CI prettier-check fix.

## Test plan

- [x] `pnpm --filter web run typecheck`
- [x] `pnpm --filter web exec vitest run --project=client src/__tests__/blob-storage-form-field-groups.clienttest.ts` — 5/5 passed (regression check on related form schema)
- [x] Reproduced prettier-check failure locally with the old command; confirmed fix passes with the new command.
- [ ] Browser review on Cloud with a seeded pre-cutoff and a seeded post-cutoff project (assert Export Source field hidden in the post-cutoff case, visible in the pre-cutoff case)
- [ ] Self-hosted parity check (`LANGFUSE_CLOUD_REGION` unset → field visible)

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-05-19 14:06:12 +02:00
Niklas SemmlerandGitHub 00c7d7d201 feat(analytics-integrations): extend legacy export source cutoff gate to PostHog and Mixpanel (#13684)
## Summary

Follow-up to [LFE-9688](https://linear.app/langfuse/issue/LFE-9688) /
[#13627](https://app.graphite.com/github/pr/langfuse/langfuse/13627),
which intentionally scoped the cutoff gate to blob-storage and listed
PostHog / Mixpanel under "Out of scope". Tracked as
[LFE-9838](https://linear.app/langfuse/issue/LFE-9838); planning doc:
`ideabox/Implementations/Proposed/2026-05-18 extend-cutoff-gate-to-posthog-mixpanel.md`.

- Post-cutoff Cloud projects (`createdAt >= 2026-05-20`) now see the Export Source field hidden in PostHog and Mixpanel settings pages (form value pinned to `EVENTS` via `defaultValues`).
- The matching tRPC `update` mutations reject any legacy `exportSource` (`TRACES_OBSERVATIONS`, `TRACES_OBSERVATIONS_EVENTS`) for post-cutoff Cloud projects with `BAD_REQUEST`.
- Pre-cutoff Cloud projects and self-hosted deployments keep full choice — no behavior change.
- Pure parity work: reuses the shared `isLegacyBlobExportAllowed` predicate and `assertLegacyBlobExportSourceAllowed` guard. No new constants, no shared-package edits, no public REST surface to gate (neither integration has one).
- The `LEGACY_BLOB_EXPORT_*` / `assertLegacyBlobExportSourceAllowed` names retain their "Blob" prefix; renaming is deferred to a separate cleanup PR (see planning doc, Decision #2).

### Impacted packages

- `web` — two settings pages, two routers, one extended servertest, one new servertest. No other package touched.

## Test plan

- [x] `pnpm --filter web run typecheck`
- [x] `pnpm --filter web exec vitest run --project=server src/__tests__/server/posthog-integration.servertest.ts src/__tests__/server/mixpanel-integration.servertest.ts` — 11/11 passed (PostHog 6, Mixpanel 5)
- [x] Browser review on dev (Playwright MCP): post-cutoff cloud projects (dev `.env` overrides cutoff to `2020-01-01`) — Export Source hidden on both PostHog and Mixpanel settings pages, Enabled switch + other form fields still render correctly
- [ ] Browser review with pre-cutoff Cloud (toggle `NEXT_PUBLIC_LANGFUSE_BLOB_EXPORT_CUTOFF` to a future date, restart dev server)
- [ ] Self-hosted parity check (`LANGFUSE_CLOUD_REGION` unset → field visible for any `createdAt`)

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

<!-- greptile_comment -->

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

This PR extends the legacy export source cutoff gate (originally applied to blob-storage in LFE-9688) to the PostHog and Mixpanel analytics integrations. Post-cutoff Cloud projects (`createdAt >= 2026-05-20`) can no longer save a legacy `exportSource` value; the field is hidden in the UI and pinned to `EVENTS`, while the tRPC `update` mutations enforce the same rule server-side.

- **Routers**: Both `posthogIntegrationRouter` and `mixpanelIntegrationRouter` gain the same `assertLegacyBlobExportSourceAllowed` guard that already protects the blob-storage router; the gate is correctly placed before the audit log and DB write.
- **UI pages**: Both settings pages derive `isPostCutoffCloud` from `useQueryProject` + `isLegacyBlobExportAllowed`, hide the Export Source `FormField` when that flag is true, and pin the form default to `EVENTS` — exactly mirroring the blob-storage page pattern.
- **Tests**: New `servertest` files (and an extended PostHog file) cover all five gate scenarios (pre-cutoff Cloud allow, two legacy-source rejections, `EVENTS` allow, self-hosted bypass) using a shared `buildSession` helper refactored from the existing SSRF test.
</details>

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

Safe to merge — the server-side gate is correctly placed and always reachable, the UI correctly hides and pins the field, and tests cover all five gate scenarios for both integrations.

The change is tightly scoped: two routers gain the same guard already proven in blob-storage, two settings pages hide a single field for post-cutoff Cloud projects, and tests exercise every code branch. No new public API surface is added, and self-hosted / pre-cutoff behaviour is unchanged.

No files require special attention. The only observation is a duplicated buildSession helper in the two new test files, which is a maintenance concern rather than a functional one.
</details>

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

```mermaid
%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[User submits PostHog / Mixpanel settings form] --> B{exportSource provided?}
    B -- "always truthy after Zod .default()" --> C[Fetch project.createdAt from DB]
    C --> D{Is legacy export source?}
    D -- No: EVENTS --> E[Allow — skip gate]
    D -- Yes: TRACES_OBSERVATIONS / TRACES_OBSERVATIONS_EVENTS --> F{isCloud AND project.createdAt >= cutoff?}
    F -- No: self-hosted OR pre-cutoff --> G[Allow]
    F -- Yes: post-cutoff Cloud --> H[Throw InvalidRequestError → BAD_REQUEST]
    E --> I[Audit log + DB upsert]
    G --> I
```
</details>

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

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

---

### Issue 1 of 1
web/src/__tests__/server/mixpanel-integration.servertest.ts:13-44
**Duplicated `buildSession` helper across test files**

The `buildSession` function in this file is byte-for-byte identical to the one added to `posthog-integration.servertest.ts`. If the session shape ever changes (e.g., a new required project field), both copies need updating in sync. Consider extracting it to a shared test utility (e.g., `web/src/__tests__/server/fixtures/session.ts`) so there is a single source of truth.

`````

</details>

<sub>Reviews (1): Last reviewed commit: ["feat(analytics-integrations): extend leg..."](https://github.com/langfuse/langfuse/commit/6418f93afafa9dede9899f65747256c8e42fd309) | [Re-trigger Greptile](https://app.greptile.com/api/retrigger?id=32589990)</sub>

<!-- /greptile_comment -->
2026-05-19 14:05:52 +02:00
Valery MeleshkinandGitHub be8b88c659 fix: multienv queries for observations v2 (#13714) 2026-05-19 11:50:09 +00:00
Valery MeleshkinandGitHub 841f0fbfec fix: observations v2 shouldn't be joining reconstructed traces for userId (#13708) 2026-05-19 10:40:51 +00:00
d14d67cbc5 refactor(shared): promote query feature to @langfuse/shared (LFE-9806) (#13678)
* refactor(shared): promote query feature to @langfuse/shared (LFE-9806)

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

* refactor(shared): import query server deps from source modules

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

* chore(shared): drop no-barrel comment; qualify mapDashboards path

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

* fix(shared): inject rootEventCondition threshold into QueryBuilder

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

* refactor(web): move dashboardUiTableToViewMapping back to dashboard/lib

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

* refactor(shared): flatten features/query — drop server/ subdir

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

* refactor: prefer direct subpath imports for query module

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

* revert(shared): drop QueryBuilder rootEventCondition override; self-import env

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

* fix(shared): read rootEventCondition threshold from process.env

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

* refactor(shared): address review feedback on query module

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

* revert(shared): rolled back diff to bare minumum

* fix(shared): added back an explicit query export following AGENTS.md guildelines

* fix(shared): added a formal threshold hours overried to side step the dual package hazard

* fix(web): missing query imports in execute query stream

* fix(shared): split query server-only files under @langfuse/shared/query/server

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

* fix(web): route QueryBuilder/executeQuery imports in 3 tests via /query/server

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-19 10:05:30 +00:00
Valery MeleshkinandGitHub a1c43faf50 chore: propagate tags onto 422 errors (#13707) 2026-05-19 09:55:24 +00:00
NimarandGitHub a7e51fbf30 chore(dx): bump pnpm to v11 (#13506)
* chore(dx): bump pnpm to v11

* bump pnpm to 11.0.9

* bump

* upgrade skill

* bump to 11.1.0

* bump pnpm to 11.1.3

* run dedupe

* clean exclusions

* add jsdom

* buump remaining versions
2026-05-19 08:39:25 +00:00
Ben BachemandGitHub 843a75b3e8 chore(dx): Make turbo logs quieter by default (#13700)
chore: Make turbo logs quiter by default
2026-05-19 08:38:24 +00:00
Ben BachemandGitHub 9af5d0d43b chore(dx): Make prisma quieter by default (#13701)
chore: Make prisma quieter by default
2026-05-19 08:38:16 +00:00
Ben BachemandGitHub f283dbbffd chore(dx): Make vitest run without separate env sourcing (#13699)
chore: Make vitest run without separate env sourcing
2026-05-19 08:38:10 +00:00
Ben BachemandGitHub 19ccc4768c feat(traces): Add new trace download endpoint (#13033)
* feat(traces): Add new trace download endpoint

* Add `providedUsageDetails` to trace export

* Sanitize trace download filename

* Clean up copy/download loading state handling in `LogViewToolbar`

* Show spinner while downloading in `TracePanelNavigationHeader`

* Show toast about large downloads only after download was successful

* Sanitize trace download filename

* Explicitly parse agg count of observations from as number

* Explicitly convert numeric details to numbers

* Revert changing `LogViewToolbarProps` from interface to type

* Fix metadata output in trace export

* Add `level` to trace export

* Use repository methods instead of inline SQL

* Sinplify tests

* Exclude unused fields from trace query

* Add defensive try/catch

* Fix public trace export

* Clean up code

* Fix tests

* Remove duplicated `toDomainArrayWithStringifiedMetadata`

* Reintroduce `TraceDownloadTooLargeError`

* Always include tool call names

* Include pricing tier name and id

* Remove reduntant serialization

* Fix trace export payload size calculation

* Explicitly map score fields

* Remove dead code

* Undo irrelevant changes

* Resolve PR comments

* Fix tests

* Fix typescript error

* Always hide download button in trace log view

* Fix issues caused by adding usage_pricing_tier_id

* Fetch orgId for admin access webhook

* Use `getTraceByIdFromEventsTable` instaed of `getTraceById`
2026-05-19 08:18:18 +00:00
Valery MeleshkinandGitHub ddb3699e7a fix: lightweight updates/deletes interact incorrectly with lazy materialization in 25.10. (#13693)
fix: lightweight updates/deletes interact incorrectly with lazy
materialization in 25.10.
2026-05-18 16:14:57 +00:00
Valery MeleshkinandGitHub a53be09edf fix: clear free-tier suspension state on Stripe subscription upgrade (#13689) 2026-05-18 15:42:56 +00:00
annabellschaandGitHub 74face2fe3 feat(web): simplify onboarding survey (#13600)
* feat(web): simplify onboarding survey

* reduce to 1 q

* rmv dead code

* fix(web): prevent empty onboarding survey submission

* rmv unused code for only 1 question

* implemented feedback ben
2026-05-18 12:46:18 +00:00
Ben BachemandGitHub ba5008212d feat(web): In-app agent scaffolding (#13451)
* feat(web): In-app agent scaffolding

* Improve styling

* Address PR comments

* Improve abort handling in createAgUiStream

* Fix ai assistant navigation item

* Preserve thread id when refreshing agent
2026-05-18 11:29:19 +00:00
Ben BachemandGitHub 58ce6d6d4c chore: Add AGENTS.override.md to .gitignore (#13679) 2026-05-18 08:28:09 +00:00
annabellschaandGitHub 352cdf323f fix(auth): redirect to /onboarding after initial password set on Cloud (#13662)
redirect net new users to onboarding survey after verification and password set
2026-05-15 15:50:28 +00:00
Niklas SemmlerandGitHub 30d787c451 feat(blob-export): gate legacy export sources for post-cutoff Cloud projects (#13627)
## Summary

- Cloud projects created on or after **2026-05-20T00:00:00Z** can no longer use `LEGACY_TRACES_OBSERVATIONS` or `LEGACY_TRACES_AND_ENRICHED_OBSERVATIONS`. Attempts via tRPC or the public REST API return `BAD_REQUEST` / HTTP 400.
- Self-hosted deployments and projects created before the cutoff are fully unaffected.
- Single shared helper (`assertLegacyBlobExportSourceAllowed`) enforces the rule identically on both write surfaces.
- Settings UI hides legacy options and defaults to `OBSERVATIONS_V2` for post-cutoff Cloud projects, with an inline message explaining the restriction.
- `project.createdAt` added to the NextAuth session so the UI can derive the gate without an extra DB round-trip.
2026-05-15 17:25:40 +02:00
Niklas SemmlerandGitHub 71a5dedf24 fix(observations-v2): omit trace_context keys from partial response when not in ClickHouse row (#13660)
## What does this PR do?

Fixes a wire-format leak in the V2 observations endpoint where `traceName`, `tags`, `release`, `userId`, `sessionId`, `bookmarked`, and `public` appeared as `null` keys in responses even when the client did not request the `trace_context` field group.

**Root cause:** `convertEventsObservation` in `observations_converters.ts` used unconditional `record.x ?? null` assignments for those seven fields regardless of the `complete` flag. When ClickHouse omits a column from the projection (because the field group was not requested), the value is `undefined`, and `undefined ?? null === null` — so the key was always emitted. The peer converter `convertObservationPartial` already uses conditional spreads (`...(record.x !== undefined && { x: record.x })`) to enforce this discipline; `convertEventsObservation` deviated from that pattern.

**Fix:**
- Split the `complete`/partial branches in `convertEventsObservation`. The `complete: true` (V1) branch keeps the unconditional defaults since V1 always returns all fields. The `complete: false` (V2) branch gates each extra field on presence, matching `convertObservationPartial`.
- Tighten the contract test assertion in `observations-api-v2.servertest.ts`: removes the `?? undefined` softening that allowed `null` values to pass `toBeUndefined()`.
- Add a unit test for `convertEventsObservation` directly — no such test existed before.

## Type of change

- [x] Bug fix (non-breaking change which fixes an issue)

## Impacted packages

- `packages/shared` — `observations_converters.ts`
- `web` — contract test + new unit test

## Verification

- `pnpm --filter @langfuse/shared run lint` ✓
- `pnpm --filter @langfuse/shared run typecheck` ✓
- CI green (tests-web, tests-worker, e2e)

<!-- greptile_comment -->

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

This PR fixes a wire-format leak in the V2 observations endpoint where seven `trace_context` fields (`traceName`, `tags`, `release`, `userId`, `sessionId`, `bookmarked`, `public`) appeared as explicit `null` keys even when the client did not request that field group. The root cause was `undefined ?? null === null` in the single shared code path.

- **Core fix** (`observations_converters.ts`): Splits `convertEventsObservation` into separate `complete: true` (V1) and `complete: false` (V2) branches. The V1 path keeps unconditional `?? null` defaults; the V2 path gates each field on `!== undefined` using conditional spreads, matching the pattern already used in `convertObservationPartial`.
- **Contract test** (`observations-api-v2.servertest.ts`): Tightens the assertion from `obs[field] ?? undefined` (which let `null` pass `toBeUndefined()`) to a direct `obs[field]` check, so the test now correctly fails when a field leaks as `null`.
- **New unit test** (`observations-converters.servertest.ts`): Adds direct coverage for `convertEventsObservation` that was previously missing, testing both branches with absent, null, and non-null field values.
</details>

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

The production change to `observations_converters.ts` is safe: the fix is minimal, well-scoped, and the conditional-spread pattern it introduces for the V2 path already exists in the peer converter.

The converter change and the contract-test tightening are both correct. The new unit test has two TypeScript type incompatibilities (`tags: null` where only `string[] | undefined` is valid, and `undefined` passed for a required `RenderingProps` parameter). Both are caught only at type-check time — the PR description reports running typecheck only for `@langfuse/shared`, not for the `web` package where the new test lives. The issues don't affect runtime or production behaviour, but they leave the test file in a state that fails strict type-checking.

web/src/__tests__/server/unit/observations-converters.servertest.ts — two call-site type errors; all other files are clean.
</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/__tests__/server/unit/observations-converters.servertest.ts:137-143
The `makeRecord` override passes `tags: null`, but `tags` in `EventsObservationRecordReadType` is typed as `z.array(z.string()).optional()` — i.e. `string[] | undefined` — which is not nullable. Passing `null` here is a TypeScript type error that would surface under `tsc --noEmit` on the `web` package. The intent of the test (verifying `?? null` defaulting) is better expressed by omitting `tags` entirely (letting it be `undefined`) or by asserting that the complete path emits `null` when the field is absent rather than null in the row.

```suggestion
      const record = makeRecord({
        user_id: null,
        session_id: null,
        trace_name: null,
        release: null,
        // tags is omitted → undefined in the record; the converter defaults it to null
      });
```

### Issue 2 of 2
web/src/__tests__/server/unit/observations-converters.servertest.ts:70-72
The overload signatures for `convertEventsObservation` declare `renderingProps` as a required `RenderingProps` parameter (not `RenderingProps | undefined`). Passing `undefined` here works at runtime (the implementation has a default value), but TypeScript checks call sites against the overloads and will flag this as a type error. The same pattern appears at the other `convertEventsObservation` call sites in this file. Passing the exported `DEFAULT_RENDERING_PROPS` is the idiomatic fix and makes the intent explicit — note that the import for `DEFAULT_RENDERING_PROPS` from `@langfuse/shared/src/server` would also need to be added.

```suggestion
      const result = convertEventsObservation(record, DEFAULT_RENDERING_PROPS, false);

      for (const field of TRACE_CONTEXT_FIELDS) {
```

`````

</details>

<sub>Reviews (1): Last reviewed commit: ["fix(test): import convertEventsObservati..."](https://github.com/langfuse/langfuse/commit/5074fe72723a7812e8ff7f3b6ff0f9d0820ed316) | [Re-trigger Greptile](https://app.greptile.com/api/retrigger?id=32323301)</sub>

<!-- /greptile_comment -->
2026-05-15 16:40:03 +02:00
Steffen SchmitzandGitHub f62373f1ac chore: Revert: "fix(sso): strip openid scope on custom save when idToken is false" (#13661)
Revert "fix(sso): strip openid scope on custom save when idToken is false (#1…"

This reverts commit d8d1fe232e.
2026-05-15 14:27:04 +00:00
d8d1fe232e fix(sso): strip openid scope on custom save when idToken is false (#13659)
The self-service SSO form exposes `idToken` but not `scope`. When an
admin sets `idToken: false` (for IdPs that release email only via the
userinfo endpoint), the stored config inherits the runtime default
`openid email profile` scope. NextAuth then chooses
`client.oauthCallback()` (idToken === false branch), which openid-client
refuses with

  "id_token detected in the response, you must use client.callback()
   instead of client.oauthCallback()"

because the IdP still returns an id_token whenever `openid` is in scope.

Normalize the stored scope at write time in `ssoConfig.save`: when the
saved provider is `custom` and `idToken === false`, drop the `openid`
token from the scope (falling back to `email profile` if stripping
leaves it empty). This also handles the merge case where the existing
config (often written via the legacy admin endpoint) supplied a scope
that the new save needs to bring into line.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 13:43:18 +00:00
annabellschaandGitHub 4cfc91b82c feat(onboarding): remove invite team members from org creation flow (#13640)
* initial commit, remove invite members step

* remove org type dropdown question

* update project creation copy

* fix(web): remove setup page scroll

* fix(web): address setup flow review feedback
2026-05-15 13:37:52 +00:00
4a214d12ed fix(events): tolerate non-JSON model_parameters in read path (#13655)
The events.all read path returned 500 when an observation's
model_parameters contained a non-JSON sentinel string (e.g. Python
SDK v4's "<not serializable object of type: dict>"). Replace the bare
JSON.parse in convertObservationPartial with parseJsonPrioritised so
unparseable values fall through to the raw string instead of throwing
for the entire row. This converter feeds both convertObservation and
convertEventsObservation.

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-15 12:39:17 +00:00
Niklas SemmlerandGitHub 41c45ca342 feat(observations-v2): expose trace_context field group in public API (#13620)
## Summary

Stacked on top of [#13617](https://app.graphite.com/github/pr/langfuse/langfuse/13617) (the `OBSERVATION_FIELD_GROUPS` / `BLOB_EXPORT_FIELD_GROUPS` split). Exposes `trace_context` on the public `/api/public/v2/observations` API after that split, restoring the group to the v2 contract with a documented surface (it had been incidentally available via the un-narrowed constant before #13617).

- Adds `trace_context` back to `OBSERVATION_FIELD_GROUPS` — the narrowed list of public v2 groups, distinct from `BLOB_EXPORT_FIELD_GROUPS`. `tools` stays only on the blob-export side as before.
- Fern source documents `trace_context` in both the field-selection list and the `Available groups` line on the `fields` query parameter. Regenerated `openapi.yml` propagates to the SDK clients.
- Columns exposed: `tags`, `release`, `traceName` (denormalized trace metadata). `usagePricingTierName` was moved to the `usage` group by #13617 and is documented there.

### How does this differ from pre-#13617 behavior?

Before #13617, `trace_context` was already accepted at runtime via the Zod filter against `OBSERVATION_FIELD_GROUPS` — but the Fern docstring listed only 9 of 11 groups, so it was undocumented. #13617 narrowed the constant for safety (separating v2 API from blob-export selection). This PR restores `trace_context` on the v2 side intentionally, with explicit Fern documentation, while leaving `tools` blob-export-only.

### Test coverage

Extends the parametrized `field group contract` loop in `observations-api-v2.servertest.ts` with a `trace_context` row that asserts the three denormalized fields flow through when `fields=trace_context` is requested. Uses fixture values for `tags`, `release`, `traceName` so a null regression is caught.

### Impacted packages

- `@langfuse/shared` — re-adds `trace_context` to `OBSERVATION_FIELD_GROUPS`; updates the docblock to reflect that only `tools` is now in the broader blob-export set
- `web` — extends servertest coverage; regenerated `openapi.yml`
- `fern` — observations endpoint docstring

## Test plan

- [x] `pnpm --filter web run typecheck`
- [x] `dotenv -e .env -- pnpm --filter web exec vitest run observations-api-v2` — 27/27 passed
- [ ] Manual: verify regenerated SDKs (Python, TypeScript) include `trace_context` as a valid `fields` value

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

<!-- greptile_comment -->

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

This PR re-adds `trace_context` to `OBSERVATION_FIELD_GROUPS` so that `tags`, `release`, and `traceName` are exposed on the public `/api/public/v2/observations` endpoint, and documents the group in both the Fern definition and the generated OpenAPI spec.

- **`packages/shared`**: `trace_context` is appended to `OBSERVATION_FIELD_GROUPS`; docblock updated to reflect `tools` is now the only blob-export-only group.
- **`fern` / `openapi.yml`**: `trace_context` and its three columns are added to the field-selection list and the `Available groups` doc line.
- **Server test**: parametrized contract test extended with a `trace_context` row and fixture values for `tags`, `release`, `traceName`, but `ALL_NON_CORE_FIELDS` (the sentinel list used for absence checks) is not updated, leaving a gap in isolation coverage.
</details>

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

The implementation change itself is straightforward and isolated; the test gap means field-isolation regressions won't be caught by the existing suite.

Adding three fields to `ALL_NON_CORE_FIELDS` is the only change needed to complete the test contract — without it, the parametrized absence loop never fires for `traceName`, `tags`, or `release` when a different group is requested, so a future leak of `trace_context` data into unrelated group responses would go undetected in CI.

web/src/__tests__/server/observations-api-v2.servertest.ts — the `ALL_NON_CORE_FIELDS` constant needs to include `traceName`, `tags`, and `release`.
</details>

<details><summary><h3>Sequence Diagram</h3></summary>

```mermaid
sequenceDiagram
    participant Client
    participant PublicAPI as /api/public/v2/observations
    participant QueryBuilder as buildObservationsQueryComponents
    participant CH as ClickHouse (events table)
    participant Traces as traces CTE

    Client->>PublicAPI: "GET ?fields=trace_context&traceId=..."
    PublicAPI->>QueryBuilder: "fields=["trace_context"]"
    QueryBuilder->>QueryBuilder: Validates group in OBSERVATION_FIELD_GROUPS
    QueryBuilder->>Traces: JOIN traces CTE (tags, release, traceName)
    QueryBuilder->>CH: SELECT core + trace_context columns
    CH-->>PublicAPI: rows with tags, release, traceName
    PublicAPI-->>Client: "{ data: [{ id, traceId, ..., tags, release, traceName }] }"
```
</details>

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

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

---

### Issue 1 of 1
web/src/__tests__/server/observations-api-v2.servertest.ts:538-542
The `ALL_NON_CORE_FIELDS` list is not updated with the three fields from the new `trace_context` group (`traceName`, `tags`, `release`). The absence-check loop only iterates over members of this list, so when any other group (e.g. `basic`, `io`, `usage`) is tested in isolation, the test never asserts that `traceName`, `tags`, and `release` are absent from the response. A regression that leaks `trace_context` fields into unrelated group responses would pass undetected.

```suggestion
      // prompt
      "promptId",
      "promptName",
      "promptVersion",
      // trace_context
      "traceName",
      "tags",
      "release",
    ] as const;
```

`````

</details>

<sub>Reviews (1): Last reviewed commit: ["feat(observations-v2): expose trace\_cont..."](https://github.com/langfuse/langfuse/commit/ae0d5f26f2afb8019cb6a7aff75452d0184b08cc) | [Re-trigger Greptile](https://app.greptile.com/api/retrigger?id=31989455)</sub>

<!-- /greptile_comment -->
2026-05-15 14:33:31 +02:00
Steffen SchmitzGitHubgreptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
7a5afd392e fix(sso): expose custom oidc options (#13647)
* fix(sso): expose custom oidc options

* Update web/src/ee/features/sso-settings/components/SSOSettings.tsx

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>

* chore: remove scope config options

* chore: test case naming

* chore: lint

* chore: patch tests

---------

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2026-05-15 11:50:58 +00:00
da7d917597 refactor(blob-export): split OBSERVATION_FIELD_GROUPS from BLOB_EXPORT_FIELD_GROUPS (#13617)
* refactor(blob-export): split OBSERVATION_FIELD_GROUPS from BLOB_EXPORT_FIELD_GROUPS

OBSERVATION_FIELD_GROUPS was driving both the public /v2/observations API
contract and the blob exporter's column selection. The blob exporter needs
a broader set (tools, trace_context) that shouldn't silently leak into the
public observations API.

Decouple the two:
- OBSERVATION_FIELD_GROUPS stays narrow (9 groups) — drives /v2/observations
- BLOB_EXPORT_FIELD_GROUPS owns the broader 11 groups — drives blob exporter
- Worker handler now imports BLOB_EXPORT_FIELD_GROUPS from the shared
  analytics-integrations module, not the repository symbol

Also relocate usagePricingTierName from trace_context to usage. It's a
pricing-tier attribute on the observation, not part of the trace context.
The /v2/observations API now exposes it under the usage group; the Fern
docstring and openapi.yml are updated to match. Blob export's
trace_context shrinks accordingly.

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

* feat(observations-v2): expose usagePricingTierName field in API response

Adds usagePricingTierName to the v2 observations API response contract:
- Fern commons.yml: optional<nullable<string>> on Observations type
- Zod APIObservationV2: nullable + optional
- Regenerated openapi.yml propagates to SDKs

usagePricingTierName was already runtime-selectable via the `usage`
field group (FIELD_SETS.usage in event-query-builder.ts:296), but the
public response contract didn't declare it — so the value was being
stripped on the way out. This adds the field to the surface that
matches the selection.

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

* refactor(observations): own field-group vocabulary in domain layer

Per review feedback: the events repository depended on a feature-flavored
type (`BLOB_EXPORT_FIELD_GROUPS` / `BlobExportFieldGroup`) defined in
`features/analytics-integrations`. A foundational component shouldn't
depend on a feature-named type.

Move both group lists to a new client-safe domain module
(`packages/shared/src/domain/observation-field-groups.ts`):

- `OBSERVATION_FIELD_GROUPS_PUBLIC_API` (was `OBSERVATION_FIELD_GROUPS`):
  the v2 public API contract — 9 groups exposed by the v2 observations
  endpoint.
- `OBSERVATION_FIELD_GROUPS_FULL` (was `BLOB_EXPORT_FIELD_GROUPS`): the
  complete set of column groups the events repository can project —
  adds `tools` and `trace_context` on top of the API surface. Mirrors
  the existing `events_full` / `events_core` ClickHouse naming.

`events.ts` (repository) and `analytics-integrations/index.ts` (feature)
both consume from the domain module instead of from each other. Frontend
forms, Zod enums, and worker jobs reach the values via the existing
`@langfuse/shared` barrel. No behavior change.

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-15 09:06:45 +00:00
613995f01d test(otel): add e2e tenant isolation test for OTEL ingestion (#13622)
* test(otel): add e2e tenant isolation test for OTEL ingestion

Adds a server-side e2e test (LFE-9771) proving that a span POSTed via
project A's API key lands exclusively in project A's observations table
and is absent from project B's. Guards the auth-scope invariant
(projectId resolved from API key, never from the OTLP payload) across
the web route, BullMQ job payload, and ClickHouse write layers.

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

* style: apply prettier formatting to otel tenant isolation test

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

* test(e2e): fix fragile Redis key count assertion in ingest trace test

The `ingest a trace` test asserted exactly 1 `api-key:*` key in Redis,
which breaks when another e2e test file runs concurrently and caches its
own API key. Replace the count check with a lookup by projectId so the
assertion is robust against parallel test execution.

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

* add assertion

* test(otel): clean up created orgs in tenant isolation test afterAll

Claude CI review on PR #13622 flagged that the two test orgs/projects created
via createOrgProjectAndApiKey() were never deleted. Add an afterAll that
deletes them by org ID — Prisma cascades to project and apiKey rows.

Track org IDs in module scope and push immediately after each creation so
the cleanup fires even if a later assertion fails or the test times out.
Mirrors the pattern used by blob-storage-integration-trpc.servertest.ts.

ClickHouse observation rows from the test span aren't cleaned up here —
they're partitioned by the new project_id which won't be reused, so they're
inert. The Postgres org/project rows are the actual leak.

* test(otel): dedup observations count to tolerate ReplacingMergeTree retries

Claude CI review on PR #13622 flagged that the bare `SELECT count() FROM
observations` returns physical (pre-merge) rows on the ReplacingMergeTree.
If the OtelIngestionQueue retries the ingestion job during the 40s
waitForExpect window (attempts: 6 per otelIngestionQueue.ts), a second
insert for the same span lands and `toBe(1)` fails for a retry reason,
not a tenant-isolation reason.

Wrap the count in the repo's standard dedup pattern:
`ORDER BY event_ts DESC + LIMIT 1 BY id, project_id` (same shape used
throughout observations.ts). The count of the deduped subquery is 0 or 1
regardless of how many physical inserts occurred.

---------

Co-authored-by: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
2026-05-15 08:57:12 +00:00
4ae71e59be refactor(blob-export): replace internal exportSource enum with public LEGACY_TRACES_OBSERVATIONS/OBSERVATIONS_V2/LEGACY_TRACES_AND_ENRICHED_OBSERVATIONS (#13619)
* refactor(blob-export): replace internal exportSource enum with public LEGACY/ENRICHED/LEGACY_AND_ENRICHED

The REST API was exposing AnalyticsIntegrationExportSource (a Prisma enum
intended as an internal identifier) directly to public consumers. Its values
— TRACES_OBSERVATIONS, EVENTS, TRACES_OBSERVATIONS_EVENTS — don't match the
UI labels users see ("Enriched observations" etc.) and bake legacy internal
naming into the public contract.

Introduce a distinct public enum and a mapping layer:
- Public values: LEGACY, ENRICHED, LEGACY_AND_ENRICHED
- toInternalExportSource / toPublicExportSource bidirectional helpers
- PUT handler maps public → internal before Prisma; GET responses map
  internal → public before serializing
- Fern docstring references /api/public/v2/observations for ENRICHED so
  consumers have a concrete anchor for the data model

This is a hard break of the enum values exposed in PR #13598 (merged ~5h
ago); no SDKs are believed to have been published or integrated against
those values. The internal AnalyticsIntegrationExportSource enum (Prisma,
tRPC, UI) is unchanged.

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

* test(blob-export): align test labels with public enum and add LEGACY_AND_ENRICHED coverage

Two CI review nits on the test file:

1. Renames — describe/it labels, section comments, and the
   `tracesObsIntegration` / `eventsIntegration` variable names now use the
   public enum values (LEGACY, ENRICHED, LEGACY_AND_ENRICHED) instead of the
   legacy internal names (TRACES_OBSERVATIONS, EVENTS,
   TRACES_OBSERVATIONS_EVENTS). The Prisma-direct seeds and DB-read
   assertions inside the test bodies still use internal values, since those
   reach the Postgres enum directly.

2. New regression test — adds `GET response maps internal
   TRACES_OBSERVATIONS_EVENTS to public LEGACY_AND_ENRICHED`. The existing
   multi-project test only covered the first two public values; the third
   was relying on compile-time exhaustiveness via `satisfies Record<…>` in
   INTERNAL_TO_PUBLIC_EXPORT_SOURCE, which won't catch a copy-paste error.
   A runtime assertion through the public REST surface closes that gap.

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

* refactor(blob-export): rename public exportSource values to be self-descriptive

Per colleague feedback on PR #13619, swap the public REST enum tag-style
names for more self-describing identifiers:

- LEGACY → LEGACY_TRACES_OBSERVATIONS
- ENRICHED → OBSERVATIONS_V2
- LEGACY_AND_ENRICHED → LEGACY_TRACES_AND_ENRICHED_OBSERVATIONS

Updates Fern source, regenerated openapi.yml, the public-API Zod schema's
mapping helpers, and the server-test fixtures. Internal Prisma enum values
(TRACES_OBSERVATIONS / EVENTS / TRACES_OBSERVATIONS_EVENTS) are unchanged —
this is purely a public-surface rename, isolated by the toPublic /
toInternal mapping helpers introduced in this PR.

* updated API docs

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-15 08:51:47 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Tobias WochingerCodex Opus 4.6
2306e86e5f ci(deps): bump the github-actions group across 1 directory with 13 updates (#13614)
* ci(deps): bump the github-actions group across 1 directory with 13 updates

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

| Package | From | To |
| --- | --- | --- |
| [actions/checkout](https://github.com/actions/checkout) | `4.3.1` | `6.0.2` |
| [aws-actions/configure-aws-credentials](https://github.com/aws-actions/configure-aws-credentials) | `6.1.0` | `6.1.1` |
| [aws-actions/amazon-ecr-login](https://github.com/aws-actions/amazon-ecr-login) | `2.1.2` | `2.1.5` |
| [aws-actions/amazon-ecs-render-task-definition](https://github.com/aws-actions/amazon-ecs-render-task-definition) | `1.8.4` | `1.8.5` |
| [aws-actions/amazon-ecs-deploy-task-definition](https://github.com/aws-actions/amazon-ecs-deploy-task-definition) | `2.6.1` | `2.6.2` |
| [github/codeql-action](https://github.com/github/codeql-action) | `4.35.1` | `4.35.3` |
| [actions/setup-node](https://github.com/actions/setup-node) | `6.3.0` | `6.4.0` |
| [pnpm/action-setup](https://github.com/pnpm/action-setup) | `5.0.0` | `6.0.5` |
| [actions/cache](https://github.com/actions/cache) | `5.0.4` | `5.0.5` |
| [slackapi/slack-github-action](https://github.com/slackapi/slack-github-action) | `3.0.1` | `3.0.3` |
| [useblacksmith/setup-docker-builder](https://github.com/useblacksmith/setup-docker-builder) | `1.6.0` | `1.8.0` |
| [useblacksmith/build-push-action](https://github.com/useblacksmith/build-push-action) | `2.1.0` | `2.2.0` |
| [astral-sh/setup-uv](https://github.com/astral-sh/setup-uv) | `8.0.0` | `8.1.0` |



Updates `actions/checkout` from 4.3.1 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/v4.3.1...de0fac2e4500dabe0009e67214ff5f5447ce83dd)

Updates `aws-actions/configure-aws-credentials` from 6.1.0 to 6.1.1
- [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/ec61189d14ec14c8efccab744f656cffd0e33f37...d979d5b3a71173a29b74b5b88418bfda9437d885)

Updates `aws-actions/amazon-ecr-login` from 2.1.2 to 2.1.5
- [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/f2e9fc6c2b355c1890b65e6f6f0e2ac3e6e22f78...fa648b43de3d4d023bcb3f89ed6940096949c419)

Updates `aws-actions/amazon-ecs-render-task-definition` from 1.8.4 to 1.8.5
- [Release notes](https://github.com/aws-actions/amazon-ecs-render-task-definition/releases)
- [Changelog](https://github.com/aws-actions/amazon-ecs-render-task-definition/blob/master/CHANGELOG.md)
- [Commits](https://github.com/aws-actions/amazon-ecs-render-task-definition/compare/77954e213ba1f9f9cb016b86a1d4f6fcdea0d57e...6853cfae8c3a7d978fbf68b5a55453395541dfbb)

Updates `aws-actions/amazon-ecs-deploy-task-definition` from 2.6.1 to 2.6.2
- [Release notes](https://github.com/aws-actions/amazon-ecs-deploy-task-definition/releases)
- [Changelog](https://github.com/aws-actions/amazon-ecs-deploy-task-definition/blob/master/CHANGELOG.md)
- [Commits](https://github.com/aws-actions/amazon-ecs-deploy-task-definition/compare/fc8fc60f3a60ffd500fcb13b209c59d221ac8c8c...a310a830f5c14e583e35d84e4e1ec7dd177c3c9c)

Updates `github/codeql-action` from 4.35.1 to 4.35.3
- [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/c10b8064de6f491fea524254123dbe5e09572f13...e46ed2cbd01164d986452f91f178727624ae40d7)

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

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

Updates `actions/cache` from 5.0.4 to 5.0.5
- [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/668228422ae6a00e4ad889ee87cd7109ec5666a7...27d5ce7f107fe9357f9df03efb73ab90386fccae)

Updates `slackapi/slack-github-action` from 3.0.1 to 3.0.3
- [Release notes](https://github.com/slackapi/slack-github-action/releases)
- [Changelog](https://github.com/slackapi/slack-github-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/slackapi/slack-github-action/compare/af78098f536edbc4de71162a307590698245be95...45a88b9581bfab2566dc881e2cd66d334e621e2c)

Updates `useblacksmith/setup-docker-builder` from 1.6.0 to 1.8.0
- [Release notes](https://github.com/useblacksmith/setup-docker-builder/releases)
- [Commits](https://github.com/useblacksmith/setup-docker-builder/compare/5241b2e9423e8b1fa37ed6050ecb62d0fb9a4e38...722e97d12b1d06a961800dd6c05d79d951ad3c80)

Updates `useblacksmith/build-push-action` from 2.1.0 to 2.2.0
- [Release notes](https://github.com/useblacksmith/build-push-action/releases)
- [Commits](https://github.com/useblacksmith/build-push-action/compare/cbd1f60d194a98cb3be5523b15134501eaf0fbf3...fb9e3e6a9299c78462bfadd0d93352c316adc9b8)

Updates `astral-sh/setup-uv` from 8.0.0 to 8.1.0
- [Release notes](https://github.com/astral-sh/setup-uv/releases)
- [Commits](https://github.com/astral-sh/setup-uv/compare/cec208311dfd045dd5311c1add060b2062131d57...08807647e7069bb48b6ef5acd8ec9567f424441b)

---
updated-dependencies:
- dependency-name: actions/cache
  dependency-version: 5.0.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: github-actions
- 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: actions/setup-node
  dependency-version: 6.4.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions
- dependency-name: astral-sh/setup-uv
  dependency-version: 8.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions
- dependency-name: aws-actions/amazon-ecr-login
  dependency-version: 2.1.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: github-actions
- dependency-name: aws-actions/amazon-ecs-deploy-task-definition
  dependency-version: 2.6.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: github-actions
- dependency-name: aws-actions/amazon-ecs-render-task-definition
  dependency-version: 1.8.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: github-actions
- dependency-name: aws-actions/configure-aws-credentials
  dependency-version: 6.1.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: github-actions
- dependency-name: github/codeql-action
  dependency-version: 4.35.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: github-actions
- dependency-name: pnpm/action-setup
  dependency-version: 6.0.5
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
- dependency-name: slackapi/slack-github-action
  dependency-version: 3.0.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: github-actions
- dependency-name: useblacksmith/build-push-action
  dependency-version: 2.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions
- dependency-name: useblacksmith/setup-docker-builder
  dependency-version: 1.8.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions
...

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

* ci: remove stale action version comments

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

---------

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: Codex Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-15 09:50:14 +02:00
Max DeichmannandGitHub 6d1c9d5eb5 docs(agents): improve shared skill guidance (#13643)
* docs(agents): improve shared skill guidance

* docs(agents): add skill creator shared skill
2026-05-14 16:31:21 +00:00
Max DeichmannandGitHub c19b2b3181 chore: Update agent guidance for repo workflows (#13642)
* Update agent guidance for repo workflows

* fix(api): keep api key id out of baggage
2026-05-14 15:45:42 +00:00
c048498024 fix(otel): recognize OpenInference llm.token_count.prompt_details.cache_read/cache_write (#13572)
fix(otel): recognize OpenInference prompt_details.cache_read/cache_write

Adds prompt_details.cache_read and prompt_details.cache_write to the
cache token resolver in extractGenericGenAiUsageDetails so cache tokens
emitted by openinference-instrumentation-* (openai, anthropic, agno) are
normalized into Langfuse's canonical input_cached_tokens /
input_cache_creation usage_details keys instead of being passed through
as opaque raw keys.

Without this, Langfuse ingests the values (they show up in usageDetails
as prompt_details.cache_read etc.) but the cost engine prices input at
full base rate and silently skips the cache keys, leading to ~30%
under-reported cost on cache-heavy observations. The values also are not
subtracted from input, which risks double-counting depending on what the
instrumentor populates.

llm.token_count.prompt_details.cache_read and cache_write are the
canonical OpenInference semantic-convention names (defined in
openinference-semantic-conventions/src/openinference/semconv/trace/__init__.py),
emitted by every OpenInference instrumentor that supports prompt caching.
The right place to fix is here, not upstream.

Same shape of fix as #12248 (pydantic-ai cache token names).

Fixes #13571 (partial — addresses the OpenInference half of #12635).

Co-authored-by: gragragrab <12702336+gragragrab@users.noreply.github.com>
Co-authored-by: Hassieb Pakzad <68423100+hassiebp@users.noreply.github.com>
2026-05-14 12:56:14 +00:00
Mark SalpeterandGitHub 5c36611994 feat(widgets): add observation type filter (#13639)
* feat(widgets): add observation type filter

* refactor(widgets): hoist observation option lists to module scope
2026-05-14 10:44:20 +00:00
Max DeichmannandGitHub da8a4f54c9 test(clickhouse): seed nested metadata (#13635) 2026-05-14 10:20:06 +00:00
Steffen SchmitzandGitHub f63b9d31c5 fix(worker): stream core data prompt exports (#13634) 2026-05-14 09:08:40 +00:00
Nimar 1e3535e126 chore: release v3.174.1 2026-05-13 22:00:32 +02:00
NimarandGitHub 13008b77db chore(deps): bump langsmith to 0.6.0 (#13625) 2026-05-13 19:54:38 +00:00
NimarandGitHub d8a0772a6e chore(deps): bump otel to 0.218.0 (#13624)
* chore(deps): bump otel to 0.218.0

* remove stale
2026-05-13 19:26:28 +00:00
6187863f23 chore: add CODEOWNERS for GitHub config (#13616)
chore: add codeowners for github config

Co-authored-by: Codex Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-13 12:27:00 +00:00
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
Nimar 1bd069f24f chore: release v3.167.4 2026-04-10 19:33:39 +02:00
52dcb23953 chore(deps): override path-to-regexp to bump to non-vulnerable version (#12931)
'path-to-regexp' v0.1.13 was released 5 days ago to fix CVE-2026-4867
https://github.com/pillarjs/path-to-regexp/commit/7ccf02cee33402f06ed2125085992ee9cd3a7c45

This PR manually override `path-to-regexp` dependency coming from `dd-trace` to patch the CVE

Co-authored-by: Nimar <l.nimar.b@gmail.com>
2026-04-10 19:33:05 +02:00
NimarandGitHub 2abaa0438e chore(ci): fix docker image upload (#13113)
* chore(ci): fix docker image upload

* simp
2026-04-10 17:31:11 +00:00
1e6d0a70a0 fix(worker): advance experiment backfill cursor when no items to process (#13107)
The backfill cursor was only advanced inside the chunk-processing loop,
which is never reached when the query returns zero dataset run items.
This caused last_run_delay_seconds to grow indefinitely in environments
with no recent experiment activity.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 15:41:29 +00:00
Nimar e91239e3ac chore: release v3.167.3 2026-04-10 17:35:44 +02:00
NimarandGitHub 418a2bf308 chore(ci): use blacksmith arm runners for docker image build (#13103)
* chore(ci): use blacksmith arm runners for docker image build

* stable rerun

* fix login
2026-04-10 15:34:13 +00:00
marliessophieandGitHub d08ce5bb71 style(experiment-compare): Add structured experiment color styles and visual accents for grid rows/columns (#13061)
* feat(web): improve experiment detail compare run styling

* fix(web): refine experiment compare accent markers

* fix(web): replace grid accent lines with marker bars

* style: improve

* refactor: streamline rendering logic in ExperimentItemsTable and enhance loading state presentation
2026-04-10 14:55:00 +00:00
baab82adae chore(ai): add skill to upgrade dependencies easily (#13094)
* chore(ai): add skill to upgrade dependencies easily

* fix(evals): prevent llm-as-a-judge queue stalls (#13037)

* clneaup

* bump transitive deps if possible

* up skill

* fix

* add timeout

---------

Co-authored-by: Hassieb Pakzad <68423100+hassiebp@users.noreply.github.com>
2026-04-10 14:54:45 +00:00
Valery MeleshkinandGitHub 932bd18df8 fix(codex): run clickhouse server as clickhouse user (#13101) 2026-04-10 16:01:38 +02:00
Nimar 4a13377e35 chore: release v3.167.2 2026-04-10 15:40:12 +02:00
NimarandGitHub 30af822ac9 chore(deps): bump defu (#13100) 2026-04-10 13:32:54 +00:00
NimarandGitHub c2c0b661e7 chore(deps): bump hono to 4.12.12 (#13099) 2026-04-10 13:22:12 +00:00
Hassieb PakzadandGitHub 2e94ebfe4b fix(evals): prevent llm-as-a-judge queue stalls (#13037) 2026-04-10 14:11:59 +02:00
NimarandGitHub b8544b3423 chore(deps): bump next to 16.2.3 (#13092) 2026-04-10 10:20:59 +00:00
NimarandGitHub 24cc309fb8 chore(deps): bump lodash 4.18.1 (#13090) 2026-04-10 09:44:35 +00:00
NimarandGitHub 1ca70d7033 chore(deps): bump langchain 1.1.39 and related (#13089) 2026-04-10 09:33:32 +00:00
NimarandGitHub ba980c302e chore(deps): bump slack and thus axios 1.15.0 (#13088) 2026-04-10 09:26:48 +00:00
Hassieb PakzadandGitHub ea197e4287 fix(llm-execution-tracing): imperatively set internal tracing environment on events (#13085) 2026-04-10 11:28:48 +02:00
NimarandGitHub 0b20e4d366 chore(deps): build go migrate with clickhouse only (#13082)
* chore(deps): build go migrate with clickhouse only

* add comment
2026-04-10 09:21:55 +00:00
NimarandGitHub 31a1a34616 chore(deps): bump node mocks to 1.17.2 (#13087) 2026-04-10 09:16:59 +00:00
Valery MeleshkinandGitHub 3c3d4bf129 chore: add scripts to provision and run local cloud dependencies (Postgres, Redis, ClickHouse, MinIO) and setup/maintenance helpers (#13054) 2026-04-10 11:19:24 +02:00
07cae52cc7 fix: validate Azure blob storage container names (#13080)
* fix: validate Azure blob storage container names

Azure requires container names to be 3-63 chars, lowercase alphanumeric
and hyphens only. Add Zod superRefine validation to the form schema,
tRPC router, and public API schema so invalid names like "Feedback N8N Bot"
are rejected at submission time with a clear error message.

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

* fix: address PR review feedback for Azure container name validation

- Add empty-string guard in validateAzureContainerName to avoid double
  error when bucketName is blank
- Add .min(1) to public API bucketName schema to match tRPC form schema
- Add Fern docs note describing Azure container naming constraints
- Add server test for invalid Azure container name rejection
- Add client test for empty-string guard behavior

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-10 09:11:04 +00:00
Ben BachemandGitHub a81edec0be fix(scores-table): Unused omittedFilter prop (#13079) 2026-04-10 09:07:16 +00:00
Ben BachemandGitHub 497179934d fix(web): Table padding issues (#13060)
* fix(web): Table padding issues

* Increase cell padding in `SelectDashboardDialog` and `SelectWidgetDialog`

* Fix memoization comparison for cellPadding in DataTable

* Set cellPadding="comfortable" for `MembersTable` in org settings
2026-04-10 09:07:08 +00:00
NimarandGitHub ad9dfc41a2 chore(deps): bump vitest to 4.1.4 (#13086) 2026-04-10 09:03:48 +00:00
Tobias Wochinger 9cc69f4c67 chore: release v3.167.1 2026-04-10 10:29:22 +02:00
557f284cd1 fix(web): allow all unicode letters for signups (#12999)
* fix(web): allow unicode letters in signup name validation

* refactor(web): share name schema between signup and display name

* fix(web): enforce 100-char limit in shared name schema

* fix(web): allow hyphens, apostrophes, and periods in name validation

The nameSchema regex was too strict, rejecting common name characters
like O'Brien, Smith-Jones, and Dr. Smith. Also align the backend
updateDisplayName schema with the shared nameSchema for consistency.

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

* fix(web): normalize smart quotes and require letter in name validation

Normalize curly/smart apostrophes (U+2018, U+2019, U+02BC) from mobile
autocorrect to straight apostrophe before validation. Require at least
one letter to reject degenerate punctuation-only names like "---".

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

* fix(web): require base letter not combining mark in name validation

The "must contain at least one letter" refine accepted standalone
combining marks (\p{M}) without an actual letter (\p{L}), allowing
inputs like "\u0301\u0301" to pass as valid names.

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

* fix(web): require base letter not combining mark in name validation

Add NFC normalization before validation so decomposed characters merge
into precomposed form, and add a negative lookahead (?!\p{M}) to reject
names that still start with a combining mark after normalization.

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

* fix(web): revert display name form to permissive schema and simplify nameSchema

Revert settings and userAccount display name validation back to
StringNoHTML.min(1).max(100) — the signup-oriented nameSchema is too
restrictive for existing display names containing underscores, ampersands, etc.

Simplify nameSchema: merge transforms, combine regex constraints into a single
refine that requires names start with a letter, and remove U+02BC from
smart-quote normalization (it's a linguistic letter, not a typographic quote).

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-09 21:35:41 +00:00
NimarandGitHub 6702c7b50f chore(deps): bump lodash to 4.18.1 in worker (#13063)
* chore(deps): update package wait to 5days

* remove superfluous

* chore(deps): bump lodash to 4.18.1 in worker
2026-04-09 16:57:03 +00:00
25d99aa371 fix: make Slack integration more robust (#13004)
* fix: make Slack integration more robust

* refactor: deduplicate scopes

* fix(slack): make SlackChannel isPrivate and isMember optional

These fields are only known for channels from the fetched list, not for
manually-typed channel names. Making them optional avoids placeholder
booleans and fixes a type error when constructing partial channel objects.

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

* chore: gracefully handle missing scopes

* fix(slack): cap rate-limit retry, resolve manual channel IDs, add empty state

- Cap retryAfter to 60s max to avoid gateway timeouts on large Slack values
- Add onSuccess handler in SlackActionForm to resolve #channel names to real IDs
- Show empty state message in ChannelSelector when bot has no accessible channels

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

* fix(slack): resolve manual channel IDs, virtualize list, fix audit log

- Use resolved Slack channel ID in audit log instead of #-prefixed input
- Replace VirtualizedList with cmdk Command + @tanstack/react-virtual
  for keyboard navigation and DOM-efficient rendering of ~5k channels
- Move "Use typed name" fallback to separate CommandGroup so it stays
  visible when the virtualized group has zero height
- Import SlackChannel type from @langfuse/shared instead of redeclaring
- Add getChannelInfo mock and #-prefixed channelId test
- Use .concat() instead of spread for channel pagination (repo convention)

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

* refactor: switch to SDK retry policies

* fix(slack): strip duplicate # prefix and use functional setState

Strip leading # from channelId fallback in test message block to avoid
displaying ##general for manually-typed channel names. Use functional
setSelectedChannel form in slack.tsx to match SlackActionForm.tsx pattern.

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

* fix(slack): guard CommandEmpty on filteredChannels length

Prevent flash of "No channels available." on popover open by explicitly
guarding CommandEmpty rendering on filteredChannels.length === 0 instead
of relying on cmdk's internal item count, which is 0 on the first
render before the virtualizer scroll container mounts.

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

* chore: review comments

* chore: another round of review feedback

* chore: more review comments

* fix(slack): improve channel selector search

* fix comment

* fix(slack): refine channel selector search

* fix(slack): sync manifest scopes

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 16:46:36 +00:00
NimarandGitHub e4d5f914cc chore(deps): bump next to 16.2.2 (#13068)
* chore(deps): bump next to 16.2.2

* bump big
2026-04-09 18:26:23 +02:00
Hassieb PakzadandGitHub dcb5dbf528 fix(llm-connections): validate new LLM base URLs (#13073) 2026-04-09 17:25:59 +02:00
Valery MeleshkinandGitHub d003a9c3f4 fix: limt media deletion batch size to avoid pg bind limits (#13072) 2026-04-09 16:29:25 +02:00
Valery MeleshkinandGitHub d2d56f0337 fix: allow Bearer auth on POST scores API (#13064)
fix: allow Bearer auth on POST scores API.

Addresses https://github.com/langfuse/langfuse/issues/12947
2026-04-09 13:45:29 +00:00
NimarandGitHub 6c0cf07a5a chore(deps): update package wait to 5days (#13062)
* chore(deps): update package wait to 5days

* remove superfluous
2026-04-09 12:39:11 +00:00
1764 changed files with 148807 additions and 41219 deletions
+55 -129
View File
@@ -1,70 +1,33 @@
# Agent Guidelines for Langfuse
This is the canonical root agent guide for the repo. The root `AGENTS.md`
should remain only as a discovery symlink so tools that require that filename
continue to work while `.agents/` stays the source of truth.
Langfuse is an open source LLM engineering platform for developing, monitoring,
evaluating, and debugging AI applications.
## Maintenance Contract
## How To Work
- `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/`.
- Update this file in the same PR when monorepo-level architecture, workflows,
dependency boundaries, mandatory verification commands, or release/security
processes materially change.
- Update this file and the relevant shared skills when user feedback introduces
a durable repo-level default for future agents. Do not edit this file for
one-off task preferences.
- For package-local material changes, update the nearest package `AGENTS.md` in
the same PR.
## Start Here By Task
- 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)
- 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)
- 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)
- Code review tasks:
[`skills/code-review/SKILL.md`](skills/code-review/SKILL.md)
- Changelog drafting for completed feature branches:
[`skills/changelog-writing/SKILL.md`](skills/changelog-writing/SKILL.md)
- ClickHouse schema/query review:
[`skills/clickhouse-best-practices/SKILL.md`](skills/clickhouse-best-practices/SKILL.md)
- Monorepo/Turbo task graph changes:
[`skills/turborepo/SKILL.md`](skills/turborepo/SKILL.md)
- User-visible frontend changes, Playwright review, or browser signoff:
[`skills/frontend-browser-review/SKILL.md`](skills/frontend-browser-review/SKILL.md)
- Web UI and frontend entry points:
`../web/AGENTS.md`
- Worker queues and processors:
`../worker/AGENTS.md`
- Shared contracts, exports, schema, and migrations:
`../packages/shared/AGENTS.md`
- EE-only work:
`../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.
- Read the minimal local context required for the task.
- Keep changes scoped and avoid unrelated refactors.
- 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 before signoff.
- For documentation screenshots in Markdown, avoid fixed `height` on `<img>`
tags; prefer Markdown images or width-only HTML so previews preserve aspect
ratio.
- Never commit secrets or credentials. Keep `.env*.example` files in
sync with required env vars.
## Project Structure
```text
langfuse/
├─ web/ # Next.js app (UI + tRPC + public REST)
├─ worker/ # Queue consumers and background processing
├─ packages/shared/ # Shared domain, DB, queue contracts, repositories
├─ ee/ # Enterprise package consumed by web
├─ generated/ # Generated API clients (do not hand-edit)
├─ fern/ # API definition sources
└─ scripts/ # Repo scripts
|- web/ # Next.js app (UI + tRPC + public REST)
|- worker/ # Queue consumers and background processing
|- packages/shared/ # Shared domain, DB, queue contracts, repositories
|- ee/ # Enterprise package consumed by web
|- generated/ # Generated API clients (do not hand-edit)
|- fern/ # API definition sources
`- scripts/ # Repo scripts
```
- Dependency direction:
@@ -79,10 +42,7 @@ langfuse/
- Postgres schema: `packages/shared/prisma/schema.prisma`
- ClickHouse migrations:
`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
`../langfuse-docs/content/handbook/product-engineering/architecture.mdx`
- Architecture principles live in `.agents/ARCHITECTURE_PRINCIPLES.md`.
## Core Commands
@@ -94,82 +54,48 @@ langfuse/
- Typecheck all: `pnpm run typecheck` / `pnpm tc`
- 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`
- Install Playwright Chromium for agent browser review: `pnpm run playwright:install`
- Worktree bootstrap: `bash scripts/codex/setup.sh`
- Worktree maintenance: `bash scripts/codex/maintenance.sh`
- Install Playwright Chromium: `pnpm run playwright:install`
Minimum verification matrix:
## Verification
| Change scope | Minimum verification |
| --- | --- |
| `web/**` only | `pnpm --filter web run lint` + targeted web tests |
| `worker/**` only | `pnpm --filter worker run lint` + targeted worker tests |
| `packages/shared/**` (non-schema) | `pnpm --filter @langfuse/shared run lint` + one targeted web check + one targeted worker check |
| `packages/shared/prisma/**` or `packages/shared/clickhouse/**` | `pnpm --filter @langfuse/shared run lint` + `pnpm run db:generate` + targeted web/worker regressions |
| Public API contract (`web/src/pages/api/public/**`, `web/src/features/public-api/types/**`, `fern/apis/**`) | web lint + targeted server API tests + Fern update/regeneration; never hand-edit `generated/**` |
| Cross-package refactor (`web` + `worker` + `shared`) | `pnpm run lint` + `pnpm run typecheck` + targeted tests per impacted package |
- `web/**`: `pnpm run lint` plus targeted web tests.
- `worker/**`: `pnpm run lint` plus targeted worker tests.
- `packages/shared/**` non-schema changes:
`pnpm run lint` plus one targeted web check and one targeted worker check.
- `packages/shared/prisma/**` or `packages/shared/clickhouse/**`:
`pnpm run lint`, `pnpm run db:generate`, and targeted web/worker
regressions.
- Public API contracts in `web/src/pages/api/public/**`,
`web/src/features/public-api/types/**`, or `fern/apis/**`: `pnpm run lint`,
targeted server API tests, and Fern update/regeneration.
- Cross-package refactors: `pnpm run lint`, `pnpm run typecheck`, and targeted
tests for impacted packages.
## Repo Rules
## Generated Files
- Keep changes scoped; avoid unrelated refactors.
- Prefer package-local implementation details in package `AGENTS.md` files.
- Do not hand-edit generated/build artifacts:
- `generated/*`
- `web/.next/*`
- `web/.next-check/*`
- `*/dist/*`
- `packages/shared/prisma/generated/*`
- Public API contract changes must update Fern sources in `fern/apis/**` and
regenerated outputs; never hand-edit `generated/**`.
- 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
browser-review loop.
- Never commit secrets or credentials. Keep `.env*.example` files in sync with
required env vars.
Do not hand-edit generated or build artifacts:
- `generated/*`
- `web/.next/*`
- `web/.next-check/*`
- `*/dist/*`
- `packages/shared/prisma/generated/*`
Public API contract changes must update Fern sources in `fern/apis/**` and
regenerated outputs. Never hand-edit `generated/**`.
## Shared Agent Setup
- `.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/`.
- 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:
- `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.
## Commit, PR, and Release Rules
- Commit messages and PR titles must follow Conventional Commits:
`type(scope): description` or `type: description`.
- PR titles are validated by `.github/workflows/validate-pr-title.yml`.
- In PR descriptions, list impacted packages and executed verification commands.
- Release workflow is managed at root with `pnpm run release`.
- Promote `main` to `production` via
`.github/workflows/promote-main-to-production.yml` or
`pnpm run release:cloud`.
- Do not change release/versioning flow without updating this file and impacted
package guides.
## Git and Tooling Notes
- Use `gh search issues` for GitHub issue search.
- Do not use destructive git commands such as `reset --hard` unless explicitly
requested.
- Do not revert unrelated working-tree changes.
- Keep commits focused and atomic.
- Remaining `.cursor/rules/*.mdc` files should stay thin wrappers around shared
docs or skills rather than owning durable repo guidance directly.
- When creating or editing `.agents/skills/**`, use
`.agents/skills/skill-creator/SKILL.md`; keep skills concise with
progressive disclosure.
- After changing shared agent setup, run `pnpm run agents:sync` and
`pnpm run agents:check`.
- Generated provider config and shim outputs under `.claude/`, `.cursor/`,
`.codex/`, `.vscode/`, or `.mcp.json` are local artifacts, not source of
truth files.
+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.
+34 -6
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-session",
"--output-dir",
"/tmp/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": {
@@ -175,7 +204,6 @@ Use them for durable, reusable guidance such as:
Do not use skills for one-off task notes or tool runtime configuration.
Use `skills/skill-creator/SKILL.md` when creating or editing shared skills.
`pnpm run agents:sync` projects the shared skills into `.claude/skills/` so
Claude can discover the same repo-owned skills.
For the skill authoring workflow, see [skills/README.md](skills/README.md).
+2 -2
View File
@@ -12,9 +12,9 @@
"-y",
"@playwright/mcp@latest",
"--isolated",
"--save-trace",
"--save-session",
"--output-dir",
".playwright-mcp",
"/tmp/playwright-mcp",
"--test-id-attribute",
"data-testid"
]
-109
View File
@@ -1,109 +0,0 @@
# Shared Skills
Shared repo skills for any coding agent working in Langfuse.
Use these from `AGENTS.md`. Claude Code reaches the same shared instructions via
the root `CLAUDE.md` compatibility symlink. Shared skills should stay focused on
reusable implementation guidance rather than runtime automation.
For the shared agent config and generated shim model, start with
[`../README.md`](../README.md).
Claude discovers these shared skills through symlinks under `.claude/skills/`.
Those discovery links are created and verified by `pnpm run agents:sync` and
`pnpm run agents:check`.
Shared skills should use progressive disclosure:
- `SKILL.md` is the short entrypoint with trigger guidance and navigation.
- `AGENTS.md` is optional and should stay concise when it exists.
- `references/` holds focused prose references that agents should open only
when the task needs them.
- `scripts/` holds deterministic helpers for repetitive or fragile steps.
## Available Skills
### agent-setup-maintenance
Use for:
- `.agents/config.json`, `.agents/AGENTS.md`, or `.agents/README.md`
- shared skill additions or shared skill routing changes
- generated shim behavior in `scripts/agents/sync-agent-shims.mjs`
- install-time agent sync behavior and provider discovery paths
Open: [agent-setup-maintenance/SKILL.md](agent-setup-maintenance/SKILL.md)
### frontend-browser-review
Use for:
- user-visible changes in `web/**`
- Playwright MCP browser review before signoff
- checking visible regressions in layout, styling, navigation, or responsive behavior
Open: [frontend-browser-review/SKILL.md](frontend-browser-review/SKILL.md)
### backend-dev-guidelines
Use for:
- tRPC routers and procedures
- public API endpoints
- worker queue processors
- Prisma and ClickHouse backed services
- backend auth, validation, observability, and tests
Open: [backend-dev-guidelines/SKILL.md](backend-dev-guidelines/SKILL.md)
### add-model-price
Use for:
- `worker/src/constants/default-model-prices.json`
- `packages/shared/src/server/llm/types.ts`
- pricing tiers, tokenizer IDs, and model `matchPattern` changes
Open: [add-model-price/SKILL.md](add-model-price/SKILL.md)
### code-review
Use for:
- PR or branch review
- correctness, regression, and risk-focused review tasks
- applying the repo-specific review policy in
`code-review/references/review-checklist.md`
Open: [code-review/SKILL.md](code-review/SKILL.md)
### changelog-writing
Use for:
- changelog entries for completed features
- drafting user-facing release notes
- checking related docs links for changelog posts
Open: [changelog-writing/SKILL.md](changelog-writing/SKILL.md)
## Adding a New Shared Skill
1. Codex may create or refine shared skills under `.agents/skills/` when a
repo-specific workflow becomes repeated enough to justify durable guidance.
2. Create a concise `.agents/skills/<skill-name>/SKILL.md`.
3. Add `.agents/skills/<skill-name>/AGENTS.md` only when the skill benefits
from a short router or checklist on top of `SKILL.md`.
4. Prefer `references/` for detailed prose and `scripts/` for deterministic
execution helpers.
5. Keep the skill tightly scoped to one domain or workflow.
6. Link the skill from `AGENTS.md` if it is relevant across the repo.
7. Run `pnpm run agents:sync` and `pnpm run agents:check` so Claude's projected
`.claude/skills/` view stays in sync.
8. Update `AGENTS.md` or package-local `AGENTS.md` if the new skill changes the
default reusable workflow for future agents.
9. Run the relevant verification for the package or workflow the skill affects.
## Skill Design Rules
- Keep the skill tool-neutral.
- Use `SKILL.md` as the short entrypoint, not the full knowledge dump.
- Prefer `references/` for deeper docs and `scripts/` for deterministic helpers.
- Avoid copying large sections of repo docs into the skill when a stable link is
enough.
- If the skill is web- or package-specific, link the nearest package
`AGENTS.md` or package docs instead of restating them.
-544
View File
@@ -1,544 +0,0 @@
# Add Model Price
Guide for adding or updating model pricing entries in Langfuse. Use this when
editing `worker/src/constants/default-model-prices.json`,
`packages/shared/src/server/llm/types.ts`, model `matchPattern` values,
tokenizer IDs, or pricing tiers.
## Purpose
This guide keeps model pricing changes consistent across providers and runtime
surfaces so Langfuse can calculate token costs accurately.
## How to Use This Skill
1. Read [references/schema-and-tiers.md](references/schema-and-tiers.md) for
the JSON shape and pricing-tier rules.
2. Read
[references/provider-sources-and-price-keys.md](references/provider-sources-and-price-keys.md)
for official pricing URLs, per-token conversion, and provider-specific usage
keys.
3. Read [references/match-patterns.md](references/match-patterns.md) when you
need to add or expand regex coverage.
4. Read
[references/workflow-and-validation.md](references/workflow-and-validation.md)
for the end-to-end edit workflow, validation rules, and common mistakes.
## Deterministic Helpers
- Validate the pricing file:
`node .agents/skills/add-model-price/scripts/validate-pricing-file.mjs`
- Test a regex directly:
`node .agents/skills/add-model-price/scripts/test-match-pattern.mjs --pattern '(?i)^(openai/)?(gpt-4o)$' --accept gpt-4o openai/gpt-4o --reject gpt-4o-mini`
- Test the regex for an existing model entry:
`node .agents/skills/add-model-price/scripts/test-match-pattern.mjs --model gpt-4o --accept gpt-4o openai/gpt-4o --reject gpt-4o-mini`
## Quick Start Checklist
### Adding a New Model
- [ ] Gather official pricing from the provider documentation
- [ ] Generate a lowercase UUID for the model entry
- [ ] Create a `matchPattern` that covers supported provider formats
- [ ] Add at least one default pricing tier
- [ ] Insert the pricing entry into
`worker/src/constants/default-model-prices.json`
- [ ] Update `packages/shared/src/server/llm/types.ts` if the model should be
selectable in playground or evaluation flows
- [ ] Validate the JSON after editing
### Updating an Existing Model
- [ ] Update the relevant prices, keys, tiers, or regexes
- [ ] Refresh `updatedAt` to today's ISO-8601 timestamp
- [ ] Validate the JSON after editing
## Target Files
- Pricing data:
`worker/src/constants/default-model-prices.json`
- Shared model types:
`packages/shared/src/server/llm/types.ts`
- Validation logic:
`packages/shared/src/features/model-pricing/validation.ts`
- Matching logic:
`packages/shared/src/server/pricing-tiers/matcher.ts`
- Tests:
`worker/src/__tests__/pricing-tier-matcher.test.ts`
## Data Structure
### Complete Model Entry Schema
```json
{
"id": "uuid-generated-with-uuidgen",
"modelName": "model-name-identifier",
"matchPattern": "(?i)^regex-pattern$",
"createdAt": "ISO-8601-timestamp",
"updatedAt": "ISO-8601-timestamp",
"tokenizerConfig": null,
"tokenizerId": "claude|openai|null",
"pricingTiers": [
{
"id": "model-uuid_tier_default",
"name": "Standard",
"isDefault": true,
"priority": 0,
"conditions": [],
"prices": {
"input": 0.000005,
"output": 0.000025
}
}
]
}
```
### Required Fields
| Field | Type | Description |
| --- | --- | --- |
| `id` | string | Unique lowercase UUID |
| `modelName` | string | Primary model identifier |
| `matchPattern` | string | Regex for matching model names |
| `createdAt` | string | ISO-8601 timestamp set on creation |
| `updatedAt` | string | ISO-8601 timestamp refreshed whenever the entry changes |
| `pricingTiers` | array | At least one pricing tier |
### Optional Fields
| Field | Type | Default | Description |
| --- | --- | --- | --- |
| `tokenizerId` | string | `null` | `"claude"`, `"openai"`, or `null` |
| `tokenizerConfig` | object | `null` | Custom tokenizer settings |
## Pricing Tier Structure
### Default Tier
Every model must have exactly one default tier:
```json
{
"id": "{model-id}_tier_default",
"name": "Standard",
"isDefault": true,
"priority": 0,
"conditions": [],
"prices": {}
}
```
Rules for the default tier:
- `isDefault` must be `true`
- `priority` must be `0`
- `conditions` must be `[]`
### Additional Tiers
Use extra tiers for context-window or usage-based pricing:
```json
{
"id": "uuid-for-tier",
"name": "Large Context (>200K)",
"isDefault": false,
"priority": 1,
"conditions": [
{
"usageDetailPattern": "(input|prompt|cached)",
"operator": "gt",
"value": 200000,
"caseSensitive": false
}
],
"prices": {}
}
```
Supported condition operators: `gt`, `gte`, `lt`, `lte`, `eq`, `neq`
## Official Pricing Sources
Always fetch pricing from the provider's official docs before editing. Do not
infer or estimate missing values.
| Provider | Source |
| --- | --- |
| Anthropic Claude | `https://platform.claude.com/docs/en/about-claude/pricing` |
| OpenAI | `https://openai.com/api/pricing/` |
| Google Gemini | `https://ai.google.dev/pricing` |
| AWS Bedrock | `https://aws.amazon.com/bedrock/pricing/` |
| Azure OpenAI | `https://azure.microsoft.com/pricing/details/cognitive-services/openai-service/` |
Gather:
1. Base input token price per million tokens
2. Output token price per million tokens
3. Cache write price when supported
4. Cache read price when supported
5. Any long-context pricing tiers
6. All model ID formats that Langfuse should match
## Price Conversion
Values in `default-model-prices.json` are per token, not per million tokens.
| Provider Price | JSON Value |
| --- | --- |
| `$5 / MTok` | `5e-6` |
| `$25 / MTok` | `25e-6` |
| `$0.50 / MTok` | `0.5e-6` |
| `$6.25 / MTok` | `6.25e-6` |
Formula:
```text
price_per_token = price_per_mtok / 1_000_000
```
## Common Price Keys by Provider
### Anthropic Claude Models
```json
{
"input": "<base_input_price>",
"input_tokens": "<base_input_price>",
"output": "<output_price>",
"output_tokens": "<output_price>",
"cache_creation_input_tokens": "<cache_write_price>",
"input_cache_creation": "<cache_write_price>",
"cache_read_input_tokens": "<cache_read_price>",
"input_cache_read": "<cache_read_price>"
}
```
### OpenAI Models
```json
{
"input": "<input_price>",
"input_cached_tokens": "<cached_input_price>",
"input_cache_read": "<cached_input_price>",
"output": "<output_price>"
}
```
### Google Gemini Models
```json
{
"input": "<input_price>",
"input_modality_1": "<input_price>",
"prompt_token_count": "<input_price>",
"promptTokenCount": "<input_price>",
"input_cached_tokens": "<cached_price>",
"cached_content_token_count": "<cached_price>",
"output": "<output_price>",
"output_modality_1": "<output_price>",
"candidates_token_count": "<output_price>",
"candidatesTokenCount": "<output_price>"
}
```
## Match Pattern Examples
### Anthropic Claude: API + Bedrock + Vertex
```regex
(?i)^(anthropic\/)?(claude-opus-4-6|(eu\\.|us\\.|apac\\.)?anthropic\\.claude-opus-4-6-v1(:0)?|claude-opus-4-6)$
```
Matches:
- `claude-opus-4-6`
- `anthropic/claude-opus-4-6`
- `anthropic.claude-opus-4-6-v1:0`
- `us.anthropic.claude-opus-4-6-v1:0`
- `claude-opus-4-6`
### With Version Date
```regex
(?i)^(anthropic\/)?(claude-opus-4-5-20251101|(eu\\.|us\\.|apac\\.)?anthropic\\.claude-opus-4-5-20251101-v1:0|claude-opus-4-5@20251101)$
```
### OpenAI
```regex
(?i)^(openai\/)?(gpt-4o)$
```
### Google Gemini
```regex
(?i)^(google\/)?(gemini-2.5-pro)$
```
### Pattern Components
| Component | Purpose | Example |
| --- | --- | --- |
| `(?i)` | Case-insensitive match | `gpt-4o` and `GPT-4O` |
| `^...$` | Full-string match | Avoids partial matches |
| `(provider\/)?` | Optional provider prefix | `openai/gpt-4o` |
| `(eu\\.|us\\.|apac\\.)?` | Optional AWS region prefix | `us.anthropic.model` |
| `(:0)?` | Optional version suffix | Bedrock model versions |
| `@date` | Vertex AI version format | `claude-3-5-sonnet@20240620` |
## Step-by-Step Workflow
### 1. Fetch Official Pricing
Open the official provider pricing page and capture the model's input, output,
cache write, and cache read prices.
### 2. Generate a Lowercase UUID
```bash
uuidgen
```
Convert the output to lowercase before using it.
### 3. Create the JSON Entry
Example for a model with $5 input, $25 output, $6.25 cache write, and
$0.50 cache read:
```json
{
"id": "13458bc0-1c20-44c2-8753-172f54b67647",
"modelName": "claude-opus-4-6",
"matchPattern": "(?i)^(anthropic\/)?(claude-opus-4-6|(eu\\.|us\\.|apac\\.)?anthropic\\.claude-opus-4-6-v1(:0)?|claude-opus-4-6)$",
"createdAt": "2026-03-09T00:00:00.000Z",
"updatedAt": "2026-03-09T00:00:00.000Z",
"tokenizerConfig": null,
"tokenizerId": "claude",
"pricingTiers": [
{
"id": "13458bc0-1c20-44c2-8753-172f54b67647_tier_default",
"name": "Standard",
"isDefault": true,
"priority": 0,
"conditions": [],
"prices": {
"input": 5e-6,
"input_tokens": 5e-6,
"output": 25e-6,
"output_tokens": 25e-6,
"cache_creation_input_tokens": 6.25e-6,
"input_cache_creation": 6.25e-6,
"cache_read_input_tokens": 0.5e-6,
"input_cache_read": 0.5e-6
}
}
]
}
```
### 4. Insert the Entry
Add the entry to the JSON array in
`worker/src/constants/default-model-prices.json`. Keep related models grouped
together.
### 5. Update Shared Model Types When Needed
If the model should be available in the playground or LLM-as-judge flows, add
it to the correct array in `packages/shared/src/server/llm/types.ts`.
Model arrays include:
- `anthropicModels`
- `openAIModels`
- `vertexAIModels`
- `googleAIStudioModels`
Do not add a new model as the first entry in one of these arrays. The first
entry is used as a default model in some test or evaluation paths and newer
models may not be available to all users yet.
### 6. Validate the Change
```bash
jq . worker/src/constants/default-model-prices.json > /dev/null
```
You can also inspect a specific entry:
```bash
jq '.[] | select(.modelName == "claude-opus-4-6")' worker/src/constants/default-model-prices.json
```
## Multi-Tier Example
For models with long-context pricing:
```json
{
"id": "uuid-here",
"modelName": "model-name",
"matchPattern": "...",
"pricingTiers": [
{
"id": "uuid-here_tier_default",
"name": "Standard",
"isDefault": true,
"priority": 0,
"conditions": [],
"prices": {
"input": 5e-6,
"output": 25e-6
}
},
{
"id": "uuid-for-large-context-tier",
"name": "Large Context (>200K)",
"isDefault": false,
"priority": 1,
"conditions": [
{
"usageDetailPattern": "(input|prompt|cached)",
"operator": "gt",
"value": 200000,
"caseSensitive": false
}
],
"prices": {
"input": 10e-6,
"output": 37.5e-6
}
}
]
}
```
## Validation Rules
1. Exactly one default tier must have `isDefault: true`
2. The default tier must have `priority: 0`
3. The default tier must have `conditions: []`
4. Non-default tiers must have `priority > 0`
5. Non-default tiers must have at least one condition
6. Priorities must be unique within a model
7. Tier names must be unique within a model
8. Each tier must contain at least one price
9. All tiers must expose the same usage-type keys
10. Regex patterns must be valid and safe
## Common Mistakes
### Guessing Instead of Using Official Pricing
Wrong:
```json
{
"cache_creation_input_tokens": "input_price * 1.25"
}
```
Correct:
```json
{
"cache_creation_input_tokens": 6.25e-6
}
```
### Using MTok Values Directly
Wrong:
```json
{
"input": 5
}
```
Correct:
```json
{
"input": 5e-6
}
```
### Missing the Default Tier Suffix
Wrong:
```json
{
"id": "some-uuid"
}
```
Correct:
```json
{
"id": "model-uuid_tier_default"
}
```
### Invalid Regex Escaping
Wrong:
```json
{
"matchPattern": "anthropic.claude"
}
```
Correct:
```json
{
"matchPattern": "anthropic\\.claude"
}
```
### Forgetting to Update `updatedAt`
Wrong:
```json
{
"updatedAt": "2025-12-12T15:00:06.513Z"
}
```
Correct:
```json
{
"updatedAt": "2026-03-09T00:00:00.000Z"
}
```
## Testing Model Matching
After adding a model, verify that the regex matches the intended provider
variants:
```javascript
const pattern = new RegExp(matchPattern);
console.log(pattern.test("claude-opus-4-6")); // true
console.log(pattern.test("anthropic/claude-opus-4-6")); // true
console.log(pattern.test("anthropic.claude-opus-4-6-v1:0")); // true
console.log(pattern.test("us.anthropic.claude-opus-4-6-v1:0")); // true
```
## Existing Model Templates
Use nearby entries as templates:
- `claude-opus-4-5-20251101` for Anthropic multi-provider patterns
- `gpt-4o` for a simple OpenAI pattern
- `gemini-2.5-pro` for a multi-tier Gemini entry
+28 -8
View File
@@ -19,18 +19,36 @@ updates in `packages/shared/`.
## How to Read This Skill
- Start with [AGENTS.md](AGENTS.md) for the high-level workflow and helper
scripts.
- Then open only the specific reference file that matches the task.
- Use this `SKILL.md` as the high-level workflow and helper index.
- Open only the specific reference file that matches the task.
## Quick Start Checklist
### Adding a New Model
- Gather official pricing from the provider documentation.
- Generate a lowercase UUID for the model entry.
- Create a `matchPattern` that covers supported provider formats.
- Add at least one default pricing tier.
- Insert the pricing entry into `worker/src/constants/default-model-prices.json`.
- Update `packages/shared/src/server/llm/types.ts` if the model should be
selectable in playground or evaluation flows.
- Validate the JSON after editing.
### Updating an Existing Model
- Update the relevant prices, keys, tiers, or regexes.
- Refresh `updatedAt` to today's ISO-8601 timestamp.
- Validate the JSON after editing.
## Reference Map
| Topic | Read this when | File |
| --- | --- | --- |
| Schema and tier rules | You need the entry shape or pricing-tier invariants | [references/schema-and-tiers.md](references/schema-and-tiers.md) |
| Topic | Read this when | File |
| ------------------------------- | ------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- |
| Schema and tier rules | You need the entry shape or pricing-tier invariants | [references/schema-and-tiers.md](references/schema-and-tiers.md) |
| Provider sources and price keys | You need official pricing URLs, per-token conversion, or provider-specific usage keys | [references/provider-sources-and-price-keys.md](references/provider-sources-and-price-keys.md) |
| Match patterns | You are editing `matchPattern` regexes or provider coverage | [references/match-patterns.md](references/match-patterns.md) |
| Workflow and validation | You are applying the end-to-end edit process or checking common mistakes | [references/workflow-and-validation.md](references/workflow-and-validation.md) |
| Match patterns | You are editing `matchPattern` regexes or provider coverage | [references/match-patterns.md](references/match-patterns.md) |
| Workflow and validation | You are applying the end-to-end edit process or checking common mistakes | [references/workflow-and-validation.md](references/workflow-and-validation.md) |
## Deterministic Helpers
@@ -38,3 +56,5 @@ updates in `packages/shared/`.
`node .agents/skills/add-model-price/scripts/validate-pricing-file.mjs`
- Match-pattern tester:
`node .agents/skills/add-model-price/scripts/test-match-pattern.mjs --model <modelName> --accept <sample...> --reject <sample...>`
- Direct regex tester:
`node .agents/skills/add-model-price/scripts/test-match-pattern.mjs --pattern '(?i)^(openai/)?(gpt-4o)$' --accept gpt-4o openai/gpt-4o --reject gpt-4o-mini`
@@ -94,3 +94,45 @@ Use extra tiers for context-window or usage-based pricing:
```
Supported operators: `gt`, `gte`, `lt`, `lte`, `eq`, `neq`
## Multi-Tier Example
Use multiple tiers when a provider changes pricing by context window or usage
class. Keep exactly one default tier and assign higher priorities to conditional
tiers:
```json
{
"pricingTiers": [
{
"id": "{model-id}_tier_default",
"name": "Standard",
"isDefault": true,
"priority": 0,
"conditions": [],
"prices": {
"input": 0.000001,
"output": 0.000002
}
},
{
"id": "uuid-for-large-context-tier",
"name": "Large Context (>200K)",
"isDefault": false,
"priority": 1,
"conditions": [
{
"usageDetailPattern": "(input|prompt|cached)",
"operator": "gt",
"value": 200000,
"caseSensitive": false
}
],
"prices": {
"input": 0.000002,
"output": 0.000004
}
}
]
}
```
@@ -22,10 +22,58 @@ template, then:
- add the new entry near related models
- refresh `updatedAt` when editing an existing entry
- update `packages/shared/src/server/llm/types.ts` when the model should be
selectable in product flows
### 4. Validate the Result
Example for a model with `$5` input, `$25` output, `$6.25` cache write, and
`$0.50` cache read per million tokens:
```json
{
"id": "13458bc0-1c20-44c2-8753-172f54b67647",
"modelName": "claude-opus-4-6",
"matchPattern": "(?i)^(anthropic\/)?(claude-opus-4-6|(eu\\.|us\\.|apac\\.)?anthropic\\.claude-opus-4-6-v1(:0)?)$",
"createdAt": "2026-03-09T00:00:00.000Z",
"updatedAt": "2026-03-09T00:00:00.000Z",
"tokenizerConfig": null,
"tokenizerId": "claude",
"pricingTiers": [
{
"id": "13458bc0-1c20-44c2-8753-172f54b67647_tier_default",
"name": "Standard",
"isDefault": true,
"priority": 0,
"conditions": [],
"prices": {
"input": 5e-6,
"input_tokens": 5e-6,
"output": 25e-6,
"output_tokens": 25e-6,
"cache_creation_input_tokens": 6.25e-6,
"input_cache_creation": 6.25e-6,
"cache_read_input_tokens": 0.5e-6,
"input_cache_read": 0.5e-6
}
}
]
}
```
### 4. Update Shared Model Types When Needed
If the model should be available in playground or LLM-as-judge flows, add it to
the correct array in `packages/shared/src/server/llm/types.ts`.
Common arrays include:
- `anthropicModels`
- `openAIModels`
- `vertexAIModels`
- `googleAIStudioModels`
Do not add a new model as the first entry in one of these arrays. The first
entry is used as a default model in some test or evaluation paths, and newer
models may not be available to all users yet.
### 5. Validate the Result
Run the bundled validator:
@@ -33,6 +81,12 @@ Run the bundled validator:
node .agents/skills/add-model-price/scripts/validate-pricing-file.mjs
```
For quick manual inspection, use `jq`:
```bash
jq '.[] | select(.modelName == "claude-opus-4-6")' worker/src/constants/default-model-prices.json
```
## Validation Rules
1. Exactly one default tier must have `isDefault: true`
@@ -46,6 +100,17 @@ node .agents/skills/add-model-price/scripts/validate-pricing-file.mjs
9. All tiers must expose the same usage-type keys
10. Regex patterns must be valid
## Testing Model Matching
Use the bundled tester before finishing any `matchPattern` change:
```bash
node .agents/skills/add-model-price/scripts/test-match-pattern.mjs --model <modelName> --accept <sample...> --reject <sample...>
```
Use representative accepted and rejected model IDs for every provider format the
regex is intended to cover.
## Common Mistakes
- Guessing prices instead of using official provider docs
@@ -14,6 +14,9 @@ Use this skill when changing the shared agent setup for the repository.
- Read [`../../README.md`](../../README.md) for the shared config and shim model.
- Read root [`../../AGENTS.md`](../../AGENTS.md) for repo-level expectations.
- When adding or editing shared skills, use
[`../skill-creator/SKILL.md`](../skill-creator/SKILL.md), then apply the
repo-specific checks in this skill.
- Inspect [`../../../scripts/agents/sync-agent-shims.mjs`](../../../scripts/agents/sync-agent-shims.mjs)
before changing generated outputs or provider discovery behavior.
- Inspect [`../../../scripts/postinstall.sh`](../../../scripts/postinstall.sh)
@@ -28,11 +31,14 @@ Use this skill when changing the shared agent setup for the repository.
3. Treat tool-specific directories such as `.claude/`, `.cursor/`, `.codex/`,
`.vscode/`, and `.mcp.json` as generated discovery surfaces unless the tool
requires a truly tool-specific feature.
4. Keep root `AGENTS.md` concise and router-like. Move detailed or conditional
workflows into shared skills or package `AGENTS.md` files.
5. When adding or changing a shared skill, update `skills/README.md` and link
it from root `AGENTS.md` if it changes the default reusable workflow.
6. When shared setup behavior changes materially, update `README.md` and
4. Keep root `AGENTS.md` concise. Move detailed or conditional workflows into
shared skills or package `AGENTS.md` files.
5. Treat developer feedback as a learning loop: when a task reveals a durable
repo convention, recurring pitfall, reusable workflow, or verification
pattern, update the smallest relevant `AGENTS.md` or shared skill.
6. When adding or changing a shared skill, keep `SKILL.md` as the entrypoint; do
not add skill-by-skill links to root `AGENTS.md`.
7. When shared setup behavior changes materially, update `README.md` and
contributor-facing docs in the same PR.
## Docker / Install-Time Constraint
@@ -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.
@@ -1,577 +0,0 @@
# Backend Development Guidelines
## Purpose
Establish consistency and best practices across Langfuse's backend packages (web, worker, packages/shared) using Next.js 14, tRPC, BullMQ, and TypeScript patterns.
## When to Use This Skill
Use this guide when working on:
- Creating or modifying tRPC routers and procedures
- Creating or modifying public API endpoints (REST)
- Creating or modifying BullMQ queue consumers and producers
- Building services with business logic
- Authenticating API requests
- Accessing resources based on entitlements
- Implementing middleware (tRPC, NextAuth, public API)
- Database operations with Prisma (PostgreSQL) or ClickHouse
- Observability with OpenTelemetry, DataDog, logger, and traceException
- Input validation with Zod v4
- Environment configuration from env variables
- Backend testing and refactoring
---
## Quick Start
### UI: New tRPC Feature Checklist (Web)
- [ ] **Router**: Define in `features/[feature]/server/*Router.ts`
- [ ] **Procedures**: Use appropriate procedure type (protected, public)
- [ ] **Authentication**: Use JWT authorization via middlewares.
- [ ] **Entitlement check**: Access resources based on resource and role
- [ ] **Validation**: Zod v4 schema for input
- [ ] **Service**: Business logic in service file
- [ ] **Error handling**: Use traceException wrapper
- [ ] **Tests**: Unit + integration tests in `__tests__/`
- [ ] **Config**: Access via env.mjs
### SDKs: New Public API Endpoint Checklist (Web)
- [ ] **Route file**: Create in `pages/api/public/`
- [ ] **Wrapper**: Use `withMiddlewares` + `createAuthedProjectAPIRoute`
- [ ] **Types**: Define in `features/public-api/types/`
- [ ] **Authentication**: Authorization via basic auth
- [ ] **Validation**: Zod schemas for query/body/response
- [ ] **Versioning**: Versioning in API path and Zod schemas for query/body/response
- [ ] **Fern API Docs**: Update `fern/apis/server/definition/` to match TypeScript types
- [ ] **Tests**: Add end-to-end test in `__tests__/async/`
### New Queue Processor Checklist (Worker)
- [ ] **Processor**: Create in `worker/src/queues/`
- [ ] **Queue types**: Create queue types in `packages/shared/src/server/queues`
- [ ] **Service**: Business logic in `features/` or `worker/src/features/`
- [ ] **Error handling**: Distinguish between errors which should fail queue processing and errors which should result in a succeeded event.
- [ ] **Queue registration**: Add to WorkerManager in app.ts
- [ ] **Tests**: Add vitest tests in worker
---
## Architecture Overview
### Layered Architecture
```
# Web Package (Next.js 14)
┌─ tRPC API ──────────────────┐ ┌── Public REST API ──────────┐
│ │ │ │
│ HTTP Request │ │ HTTP Request │
│ ↓ │ │ ↓ │
│ tRPC Procedure │ │ withMiddlewares + │
│ (protectedProjectProcedure)│ │ createAuthedProjectAPIRoute│
│ ↓ │ │ ↓ │
│ Service (business logic) │ │ Service (business logic) │
│ ↓ │ │ ↓ │
│ Prisma / ClickHouse │ │ Prisma / ClickHouse │
│ │ │ │
└─────────────────────────────┘ └─────────────────────────────┘
[optional]: Publish to Redis BullMQ queue
┌─ Worker Package (Express) ──────────────────────────────────┐
│ │
│ BullMQ Queue Job │
│ ↓ │
│ Queue Processor (handles job) │
│ ↓ │
│ Service (business logic) │
│ ↓ │
│ Prisma / ClickHouse │
│ │
└─────────────────────────────────────────────────────────────┘
```
**Key Principles:**
- **Web**: tRPC procedures for UI OR public API routes for SDKs → Services → Database
- **Worker**: Queue processors → Services → Database
- **packages/shared**: Shared code for Web and Worker
See [references/architecture-overview.md](references/architecture-overview.md)
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
```typescript
// ❌ NEVER: Business logic in procedures
export const traceRouter = createTRPCRouter({
byId: protectedProjectProcedure
.input(z.object({ traceId: z.string() }))
.query(async ({ input, ctx }) => {
// 200 lines of logic here
}),
});
// ✅ ALWAYS: Delegate to service
export const traceRouter = createTRPCRouter({
byId: protectedProjectProcedure
.input(z.object({ traceId: z.string() }))
.query(async ({ input, ctx }) => {
return await getTraceById(input.traceId);
}),
});
```
### 2. Access Config via env.mjs, NEVER process.env
```typescript
// ❌ NEVER (except in env.mjs itself)
const dbUrl = process.env.DATABASE_URL;
// ✅ ALWAYS
import { env } from "@/src/env.mjs";
const dbUrl = env.DATABASE_URL;
```
### 3. Validate ALL Input with Zod v4
```typescript
import { z } from "zod/v4";
const schema = z.object({
email: z.string().email(),
projectId: z.string(),
});
const validated = schema.parse(input);
```
### 4. Services Use Prisma Directly for Simple CRUD or Repositories for Complex Queries
```typescript
// Services use Prisma directly for simple CRUD
import { prisma } from "@langfuse/shared/src/db";
const dataset = await prisma.dataset.findUnique({
where: { id: datasetId, projectId }, // Always filter by projectId for tenant isolation
});
// Or use repositories for complex queries (traces, observations, scores)
import { getTracesTable } from "@langfuse/shared/src/server";
const traces = await getTracesTable({
projectId,
filter: [...],
limit: 1000,
});
```
### 6. Observability: OpenTelemetry + DataDog (Not Sentry for Backend)
**Langfuse uses OpenTelemetry for backend observability, with traces and logs sent to DataDog.**
```typescript
// Import observability utilities
import {
logger, // Winston logger with OpenTelemetry/DataDog context
traceException, // Record exceptions to OpenTelemetry spans
instrumentAsync, // Create instrumented spans
} from "@langfuse/shared/src/server";
// Structured logging (includes trace_id, span_id, dd.trace_id)
logger.info("Processing dataset", { datasetId, projectId });
logger.error("Failed to create dataset", { error: err.message });
// Record exceptions to OpenTelemetry (sent to DataDog)
try {
await operation();
} catch (error) {
traceException(error); // Records to current span
throw error;
}
// Instrument critical operations (all API routes auto-instrumented)
const result = await instrumentAsync(
{ name: "dataset.create" },
async (span) => {
span.setAttributes({ datasetId, projectId });
// Operation here
return dataset;
},
);
```
**Note**: Frontend uses Sentry, but backend (tRPC, API routes, services, worker) uses OpenTelemetry + DataDog.
### 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
### 8. Always Filter by projectId for Tenant Isolation
```typescript
// ✅ CORRECT: Filter by projectId for tenant isolation
const trace = await prisma.trace.findUnique({
where: { id: traceId, projectId }, // Required for multi-tenant data isolation
});
// ✅ CORRECT: ClickHouse queries also require projectId
const traces = await queryClickhouse({
query: `
SELECT * FROM traces
WHERE project_id = {projectId: String}
AND timestamp >= {startTime: DateTime64(3)}
`,
params: { projectId, startTime },
});
```
### 9. Keep Fern API Definitions in Sync with TypeScript Types
When modifying public API types in `web/src/features/public-api/types/`, the corresponding Fern API definitions in `fern/apis/server/definition/` must be updated to match.
**Zod to Fern Type Mapping:**
| Zod Type | Fern Type | Example |
| -------- | --------- | ------- |
| `.nullish()` | `optional<nullable<T>>` | `z.string().nullish()``optional<nullable<string>>` |
| `.nullable()` | `nullable<T>` | `z.string().nullable()``nullable<string>` |
| `.optional()` | `optional<T>` | `z.string().optional()``optional<string>` |
| Always present | `T` | `z.string()``string` |
**Source References:**
Add a comment at the top of each Fern type referencing the TypeScript source file:
```yaml
# Source: web/src/features/public-api/types/traces.ts - APITrace
Trace:
properties:
id: string
name:
type: nullable<string>
```
---
## Common Imports
```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";
```
---
## Quick Reference
### 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
---
## Anti-Patterns to Avoid
❌ Business logic in routes/procedures
❌ Direct process.env usage (always use env.mjs/env.ts)
❌ Missing error handling
❌ No input validation (always use Zod v4)
❌ Missing projectId filter on tenant-scoped queries
❌ console.log instead of logger/traceException (OpenTelemetry)
---
## Navigation Guide
| Need to... | Read this |
| ------------------------- | ------------------------------------------------------------ |
| Understand architecture | [architecture-overview.md](references/architecture-overview.md) |
| Create routes/controllers | [routing-and-controllers.md](references/routing-and-controllers.md) |
| Organize business logic | [services-and-repositories.md](references/services-and-repositories.md) |
| Create middleware | [middleware-guide.md](references/middleware-guide.md) |
| Database access | [database-patterns.md](references/database-patterns.md) |
| Manage config | [configuration.md](references/configuration.md) |
| 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 ✅
+90 -17
View File
@@ -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
@@ -20,24 +20,97 @@ Use this skill for backend and API work across `web/`, `worker/`, and
## How to Read This Skill
- Start with [AGENTS.md](AGENTS.md) when the task spans multiple backend areas
or you need the end-to-end checklists.
- Use this `SKILL.md` when the task spans multiple backend areas or you need the
end-to-end reference map.
- Read only the specific reference file that matches the work when the scope is
narrower.
- If the task introduces a user-supplied URL, an outbound HTTP request, a new
integration, or touches secrets, RBAC, or redirect handling, also load the
shared [`security-review`](../security-review/SKILL.md) skill before
designing or implementing the change.
## Quick Start Checklists
### UI: New tRPC Feature
- Define the router in `features/[feature]/server/*Router.ts`.
- Use the appropriate protected or public procedure.
- Authenticate with JWT-aware middleware.
- Check project/resource access and entitlements.
- Validate input with Zod v4.
- Put business logic in a service file.
- Use `traceException` for error handling where relevant.
- Add unit or integration tests in `__tests__/`.
- Access config via `env.mjs`.
### SDKs: New Public API Endpoint
- Create the route in `pages/api/public/`.
- Wrap it with `withMiddlewares` and `createAuthedProjectAPIRoute`.
- Define types in `features/public-api/types/`.
- Authenticate with basic auth.
- Validate query, body, and response with Zod schemas.
- Include API versioning in paths and schemas.
- Update Fern API definitions to match TypeScript types.
- Add end-to-end tests in `__tests__/async/`.
### Worker: New Queue Processor
- Create the processor in `worker/src/queues/`.
- Define queue types in `packages/shared/src/server/queues`.
- Place business logic in `features/` or `worker/src/features/`.
- Distinguish failed jobs from jobs that should succeed with a recorded error.
- Register the queue in `WorkerManager` in `app.ts`.
- Add worker vitest coverage.
## Core Principles
- tRPC procedures, public API routes, and queue processors delegate business
logic to services.
- Access configuration through `env.mjs`; do not read `process.env` directly
outside env setup.
- Validate all external input with Zod v4.
- Use Prisma directly for simple CRUD and repositories for complex query access.
- Use OpenTelemetry and DataDog for backend observability.
- Always filter project-scoped database queries by `projectId`.
- Keep Fern API definitions in sync with public TypeScript API contracts.
- Keep backend tests independent and parallel-safe.
## Live Examples
- 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`.
## Naming Conventions
- tRPC routers: `camelCaseRouter.ts`, for example `datasetRouter.ts`.
- Services: `service.ts` in the feature server directory.
- Queue processors: `camelCaseQueue.ts`, for example `evalQueue.ts`.
- Public API routes: kebab-case filenames, for example `dataset-items.ts`.
## Anti-Patterns to Avoid
- Business logic in routes or procedures.
- Direct `process.env` usage instead of `env.mjs` / `env.ts`.
- Missing error handling.
- Missing input validation.
- Missing `projectId` filters on tenant-scoped queries.
- `console.log` instead of `logger` / `traceException`.
## Reference Map
| Topic | Read this when | File |
| --- | --- | --- |
| Architecture and package boundaries | You need the web/worker/shared split, request flow, or queue lifecycle | [references/architecture-overview.md](references/architecture-overview.md) |
| Routing and controllers | You are writing tRPC procedures, public API routes, or queue entrypoints | [references/routing-and-controllers.md](references/routing-and-controllers.md) |
| Middleware and auth | You are changing request auth, permissions, or middleware composition | [references/middleware-guide.md](references/middleware-guide.md) |
| Services and repositories | You are placing business logic, repository code, or DI patterns | [references/services-and-repositories.md](references/services-and-repositories.md) |
| Database access | You are touching Prisma, ClickHouse, tenant filters, or query patterns | [references/database-patterns.md](references/database-patterns.md) |
| Configuration | You are adding env vars, startup config, or runtime toggles | [references/configuration.md](references/configuration.md) |
| Testing | You are adding or updating backend tests | [references/testing-guide.md](references/testing-guide.md) |
## Full Compiled Guide
Read [AGENTS.md](AGENTS.md) for the complete backend guide with checklists,
directory conventions, imports, architecture, and cross-cutting practices.
| Topic | Read this when | File |
| ----------------------------------- | ------------------------------------------------------------------------ | ---------------------------------------------------------------------------------- |
| Architecture and package boundaries | You need the web/worker/shared split, request flow, or queue lifecycle | [references/architecture-overview.md](references/architecture-overview.md) |
| Routing and controllers | You are writing tRPC procedures, public API routes, or queue entrypoints | [references/routing-and-controllers.md](references/routing-and-controllers.md) |
| Middleware and auth | You are changing request auth, permissions, or middleware composition | [references/middleware-guide.md](references/middleware-guide.md) |
| Services and repositories | You are placing business logic, repository code, or DI patterns | [references/services-and-repositories.md](references/services-and-repositories.md) |
| Database access | You are touching Prisma, ClickHouse, tenant filters, or query patterns | [references/database-patterns.md](references/database-patterns.md) |
| Configuration | You are adding env vars, startup config, or runtime toggles | [references/configuration.md](references/configuration.md) |
| Testing | You are adding or updating backend tests | [references/testing-guide.md](references/testing-guide.md) |
@@ -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";
@@ -864,7 +864,7 @@ const validated = bodySchema.parse(req.body);
**Related Files:**
- [../AGENTS.md](../AGENTS.md) - Main guide
- [../SKILL.md](../SKILL.md) - Main guide
- [routing-and-controllers.md](routing-and-controllers.md) - tRPC and Public API details
- [services-and-repositories.md](services-and-repositories.md) - Service patterns
- [testing-guide.md](testing-guide.md) - Testing strategies
@@ -286,7 +286,7 @@ const licenseKey = env.LANGFUSE_EE_LICENSE_KEY;
**When Set:**
| Environment | Value | Purpose |
|--------------------------|------------------------|------------------------------------------------|
| ------------------------ | ---------------------- | ---------------------------------------------- |
| **Developer Laptop** | `"DEV"` or `"STAGING"` | Local development against cloud infrastructure |
| **Langfuse Cloud US** | `"US"` | Production US region |
| **Langfuse Cloud EU** | `"EU"` | Production EU region |
@@ -559,5 +559,5 @@ langfuse/
**Related Files:**
- [../AGENTS.md](../AGENTS.md) - Main guide
- [../SKILL.md](../SKILL.md) - Main guide
- [architecture-overview.md](architecture-overview.md) - Architecture patterns
@@ -57,7 +57,7 @@ const project = await prisma.project.create({
// ✅ GOOD: Read with projectId filter
const trace = await prisma.trace.findUnique({
where: { id: traceId, projectId }, // ← Always include projectId for tenant isolation
where: { id: traceId, projectId }, // ← Always include projectId for tenant isolation
include: {
scores: true,
project: { select: { id: true, name: true } },
@@ -77,12 +77,12 @@ await prisma.user.update({
// ✅ GOOD: Delete with projectId
await prisma.apiKey.delete({
where: { id: apiKeyId, projectId }, // ← Always include projectId
where: { id: apiKeyId, projectId }, // ← Always include projectId
});
// ✅ GOOD: Count with projectId
const traceCount = await prisma.trace.count({
where: { projectId, userId }, // ← Always include projectId
where: { projectId, userId }, // ← Always include projectId
});
```
@@ -225,7 +225,7 @@ const rows = await queryClickhouse<{ id: string; name: string }>({
LIMIT {limit: UInt32}
`,
params: {
projectId, // ← Required for tenant isolation
projectId, // ← Required for tenant isolation
startTime: convertDateToClickhouseDateTime(startDate),
limit: 100,
},
@@ -341,6 +341,7 @@ const query = `
```
**Why this is important:**
- Langfuse is multi-tenant - each project's data must be isolated
- The `project_id` filter ensures queries only access data from the intended tenant
- All queries on project-scoped tables (traces, observations, scores, sessions, etc.) must filter by `project_id`
@@ -358,6 +359,34 @@ const query = `
`;
```
**`is_deleted` on `traces`, `observations`, and `scores` is dormant — avoid new filters.**
These three tables are declared as
`ReplacingMergeTree(event_ts, is_deleted)`, but no production
code writes `is_deleted = 1` for them — all deletes use
ClickHouse's lightweight `DELETE FROM` mutation (e.g.
`deleteObservationsByTraceIds`,
`deleteObservationsByProjectId`,
`deleteObservationsOlderThanDays`), which marks rows via the
engine-managed `_row_exists` column. `_row_exists` is handled
transparently by the read path; no special query handling is
needed.
What this means for query authors:
- **`WHERE is_deleted = 0` filters on these three tables are
dead weight in practice.** A few legacy reads still carry
them (e.g. `web/src/features/score-analytics/server/`); new
code should not add them unless soft-delete writes have
actually been introduced.
**Separate case: `blob_storage_file_log`.** This table is also
a `ReplacingMergeTree` but **does** use soft-delete
intentionally — `ingestionFileDeletion.ts` writes
`is_deleted: "1"`, `batch-project-blob-cleaner` reads with
`countIf(is_deleted = 1)`. The guidance above does not apply
to it.
**3. Use time-based filtering for performance:**
```typescript
@@ -520,6 +549,7 @@ export const getScoresByTraceId = async (
### Project-Scoped vs Global Tables
**Project-scoped tables (MUST filter by `project_id`):**
- `traces` - All trace queries require `project_id`
- `observations` - All observation queries require `project_id`
- `scores` - All score queries require `project_id`
@@ -527,6 +557,7 @@ export const getScoresByTraceId = async (
- `dataset_run_items_rmt` - All dataset run queries require `project_id`
**Global tables (no `project_id` filter needed):**
- `users` - User management (use `id` for filtering)
- `organizations` - Organization data (use `id` for filtering)
- System configuration tables
@@ -599,12 +630,12 @@ try {
**Common Prisma error codes:**
| Code | Meaning | Typical Cause |
| -------- | ---------------------------- | ------------------------------------- |
| `P2002` | Unique constraint violation | Duplicate email, API key, etc. |
| `P2003` | Foreign key constraint | Referenced record doesn't exist |
| `P2025` | Record not found | Update/delete of non-existent record |
| `P2018` | Required relation not found | Connect to non-existent related record |
| Code | Meaning | Typical Cause |
| ------- | --------------------------- | -------------------------------------- |
| `P2002` | Unique constraint violation | Duplicate email, API key, etc. |
| `P2003` | Foreign key constraint | Referenced record doesn't exist |
| `P2025` | Record not found | Update/delete of non-existent record |
| `P2018` | Required relation not found | Connect to non-existent related record |
### ClickHouse Errors
@@ -636,11 +667,11 @@ try {
**ClickHouse error types:**
| Error Type | Discriminator | Meaning | Solution |
| --------------- | ----------------------- | ---------------------------- | -------------------------------------------------- |
| `MEMORY_LIMIT` | "memory limit exceeded" | Query used too much memory | Use more specific filters or shorter time range |
| `OVERCOMMIT` | "OvercommitTracker" | Memory overcommit limit hit | Reduce query complexity or result set size |
| `TIMEOUT` | "Timeout", "timed out" | Query took too long | Add filters, reduce time range, or optimize query |
| Error Type | Discriminator | Meaning | Solution |
| -------------- | ----------------------- | --------------------------- | ------------------------------------------------- |
| `MEMORY_LIMIT` | "memory limit exceeded" | Query used too much memory | Use more specific filters or shorter time range |
| `OVERCOMMIT` | "OvercommitTracker" | Memory overcommit limit hit | Reduce query complexity or result set size |
| `TIMEOUT` | "Timeout", "timed out" | Query took too long | Add filters, reduce time range, or optimize query |
**ClickHouse retries:**
@@ -648,13 +679,13 @@ ClickHouse queries automatically retry network errors (socket hang up) with expo
```typescript
// In packages/shared/src/env.ts
LANGFUSE_CLICKHOUSE_QUERY_MAX_ATTEMPTS: z.coerce.number().positive().default(3)
LANGFUSE_CLICKHOUSE_QUERY_MAX_ATTEMPTS: z.coerce.number().positive().default(3);
```
---
**Related Files:**
- [../AGENTS.md](../AGENTS.md) - Main backend development guidelines
- [../SKILL.md](../SKILL.md) - Main backend development guidelines
- [architecture-overview.md](architecture-overview.md) - System architecture
- [configuration.md](configuration.md) - Environment variable configuration
@@ -116,7 +116,9 @@ const enforceUserIsAuthedAndProjectMember = t.middleware(async (opts) => {
const projectId = parsedInput.data.projectId;
const sessionProject = ctx.session.user.organizations
.flatMap((org) => org.projects.map((project) => ({ ...project, organization: org })))
.flatMap((org) =>
org.projects.map((project) => ({ ...project, organization: org })),
)
.find((project) => project.id === projectId);
if (!sessionProject) {
@@ -164,7 +166,9 @@ const enforceIsAuthedAndOrgMember = t.middleware(async (opts) => {
}
const orgId = result.data.orgId;
const sessionOrg = ctx.session.user.organizations.find((org) => org.id === orgId);
const sessionOrg = ctx.session.user.organizations.find(
(org) => org.id === orgId,
);
if (!sessionOrg && ctx.session.user.admin !== true) {
throw new TRPCError({
@@ -179,7 +183,8 @@ const enforceIsAuthedAndOrgMember = t.middleware(async (opts) => {
...ctx.session,
user: ctx.session.user,
orgId: orgId,
orgRole: ctx.session.user.admin === true ? Role.OWNER : sessionOrg!.role,
orgRole:
ctx.session.user.admin === true ? Role.OWNER : sessionOrg!.role,
},
},
});
@@ -221,7 +226,8 @@ const enforceTraceAccess = t.middleware(async (opts) => {
if (!trace.public && !sessionProject && ctx.session?.user?.admin !== true) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "User is not a member of this project and this trace is not public",
message:
"User is not a member of this project and this trace is not public",
});
}
@@ -348,6 +354,7 @@ export default withMiddlewares({
### createAuthedProjectAPIRoute Pattern
Factory function for authenticated public API routes with:
- Authentication (Basic auth or Admin API key)
- Rate limiting
- Input/output validation (Zod)
@@ -355,17 +362,21 @@ Factory function for authenticated public API routes with:
```typescript
export const createAuthedProjectAPIRoute = <TQuery, TBody, TResponse>(
routeConfig: RouteConfig<TQuery, TBody, TResponse>
routeConfig: RouteConfig<TQuery, TBody, TResponse>,
) => {
return async (req: NextApiRequest, res: NextApiResponse) => {
// 1. Authentication (verifyAuth)
const auth = await verifyAuth(req, routeConfig.isAdminApiKeyAuthAllowed || false);
const auth = await verifyAuth(
req,
routeConfig.isAdminApiKeyAuthAllowed || false,
);
// 2. Rate limiting
const rateLimitResponse = await RateLimitService.getInstance().rateLimitRequest(
auth.scope,
routeConfig.rateLimitResource || "public-api"
);
const rateLimitResponse =
await RateLimitService.getInstance().rateLimitRequest(
auth.scope,
routeConfig.rateLimitResource || "public-api",
);
if (rateLimitResponse?.isRateLimited()) {
return rateLimitResponse.sendRestResponseIfLimited(res);
@@ -474,15 +485,20 @@ Public APIs use **Basic Auth** with API keys:
```typescript
async function verifyBasicAuth(authHeader: string | undefined) {
const regularAuth = await new ApiAuthService(prisma, redis)
.verifyAuthHeaderAndReturnScope(authHeader);
const regularAuth = await new ApiAuthService(
prisma,
redis,
).verifyAuthHeaderAndReturnScope(authHeader);
if (!regularAuth.validKey) {
throw { status: 401, message: regularAuth.error };
}
if (regularAuth.scope.accessLevel !== "project") {
throw { status: 401, message: "Access denied - need basic auth with secret key" };
throw {
status: 401,
message: "Access denied - need basic auth with secret key",
};
}
return regularAuth;
@@ -499,7 +515,10 @@ async function verifyAdminApiKeyAuth(req: NextApiRequest) {
// 3. x-langfuse-project-id: <project-id>
if (env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION) {
throw { status: 403, message: "Admin API key auth not available on Langfuse Cloud" };
throw {
status: 403,
message: "Admin API key auth not available on Langfuse Cloud",
};
}
const adminApiKey = env.ADMIN_API_KEY;
@@ -508,8 +527,14 @@ async function verifyAdminApiKeyAuth(req: NextApiRequest) {
// Timing-safe comparison
const isValid =
crypto.timingSafeEqual(Buffer.from(bearerToken), Buffer.from(adminApiKey)) &&
crypto.timingSafeEqual(Buffer.from(adminApiKeyHeader), Buffer.from(adminApiKey));
crypto.timingSafeEqual(
Buffer.from(bearerToken),
Buffer.from(adminApiKey),
) &&
crypto.timingSafeEqual(
Buffer.from(adminApiKeyHeader),
Buffer.from(adminApiKey),
);
if (!isValid) throw { status: 401, message: "Invalid admin API key" };
@@ -635,6 +660,7 @@ return opentelemetry.context.with(ctx, async () => {
```
**Baggage includes:**
- `userId` - User ID from session
- `projectId` - Project ID from input
- `headers` - Request headers for trace propagation
@@ -676,7 +702,9 @@ const baseProcedure = withOtelTracingProcedure.use(withErrorHandling);
const authedProcedure = baseProcedure.use(enforceUserIsAuthed);
// Add project scoping
const projectProcedure = authedProcedure.use(enforceUserIsAuthedAndProjectMember);
const projectProcedure = authedProcedure.use(
enforceUserIsAuthedAndProjectMember,
);
```
### Using Procedures in Routers
@@ -692,7 +720,7 @@ export const tracesRouter = createTRPCRouter({
projectId: z.string(),
page: z.number().optional(),
limit: z.number().optional(),
})
}),
)
.query(async ({ input, ctx }) => {
// ctx.session.projectId is guaranteed to exist
@@ -713,7 +741,7 @@ export const tracesRouter = createTRPCRouter({
traceId: z.string(),
projectId: z.string(),
timestamp: z.date().nullish(),
})
}),
)
.query(async ({ input, ctx }) => {
// ctx.trace is guaranteed to exist (fetched by middleware)
@@ -729,11 +757,12 @@ Middleware executes in the order it's chained:
```typescript
protectedProjectProcedure
.use(withErrorHandling) // 1. Wraps entire execution
.use(enforceUserIsAuthed) // 2. Validates session exists
.use(enforceUserIsAuthedAndProjectMember) // 3. Validates project membership
.input(schema) // 4. Validates input
.query(async ({ input, ctx }) => { // 5. Executes query
.use(withErrorHandling) // 1. Wraps entire execution
.use(enforceUserIsAuthed) // 2. Validates session exists
.use(enforceUserIsAuthedAndProjectMember) // 3. Validates project membership
.input(schema) // 4. Validates input
.query(async ({ input, ctx }) => {
// 5. Executes query
// ...
});
```
@@ -744,22 +773,21 @@ Each middleware can enrich the context:
```typescript
// After enforceUserIsAuthed:
ctx.session.user // NonNullable<User>
ctx.session.user; // NonNullable<User>
// After enforceUserIsAuthedAndProjectMember:
ctx.session.projectId // string
ctx.session.projectRole // Role (OWNER | ADMIN | MEMBER | VIEWER)
ctx.session.orgId // string
ctx.session.orgRole // Role
ctx.session.projectId; // string
ctx.session.projectRole; // Role (OWNER | ADMIN | MEMBER | VIEWER)
ctx.session.orgId; // string
ctx.session.orgRole; // Role
// After enforceTraceAccess:
ctx.trace // TraceRecord (pre-fetched)
ctx.trace; // TraceRecord (pre-fetched)
```
---
**Related Files:**
- [../AGENTS.md](../AGENTS.md) - Main backend development guidelines
- [../SKILL.md](../SKILL.md) - Main backend development guidelines
- [architecture-overview.md](architecture-overview.md) - System architecture
- [../AGENTS.md](../AGENTS.md) - Error handling patterns and traceException guidance
@@ -7,6 +7,7 @@ Complete guide to routing and separation of concerns in Langfuse's Next.js + tRP
- [Architecture Overview](#architecture-overview)
- [tRPC Routers](#trpc-routers)
- [Public REST API Routes](#public-rest-api-routes)
- [Fern API Definitions](#fern-api-definitions)
- [Service Layer](#service-layer)
- [Repository Layer](#repository-layer)
- [Separation of Concerns](#separation-of-concerns)
@@ -48,6 +49,7 @@ Langfuse uses a **layered architecture** with clear separation of concerns:
### Key Principles
**Entry Points (Routes/Procedures):**
- ✅ Define routing and procedure signatures
- ✅ Handle authentication/authorization (via middleware)
- ✅ Validate input (Zod schemas)
@@ -55,12 +57,14 @@ Langfuse uses a **layered architecture** with clear separation of concerns:
- ✅ Return responses
**Entry Points should NEVER:**
- ❌ Contain business logic
- ❌ Access database directly
- ❌ Perform complex data transformations
- ❌ Make direct repository calls (use services)
**Services:**
- ✅ Contain business logic
- ✅ Orchestrate multiple operations
- ✅ Call repositories or Prisma/ClickHouse
@@ -68,6 +72,7 @@ Langfuse uses a **layered architecture** with clear separation of concerns:
- ❌ Should NOT know about HTTP, tRPC, or request/response objects
**Repositories:**
- ✅ Complex database queries
- ✅ Data transformation (DB → domain models)
- ✅ ClickHouse query builders
@@ -88,7 +93,10 @@ tRPC routers define type-safe procedures for the internal UI. Each router groups
```typescript
import { z } from "zod/v4";
import { createTRPCRouter, protectedProjectProcedure } from "@/src/server/api/trpc";
import {
createTRPCRouter,
protectedProjectProcedure,
} from "@/src/server/api/trpc";
import { paginationZod, singleFilter, orderBy } from "@langfuse/shared";
import {
getScoresUiTable,
@@ -187,6 +195,7 @@ export const scoresRouter = createTRPCRouter({
```
**Key Points:**
- Use appropriate procedure type (`protectedProjectProcedure`, `authenticatedProcedure`, etc.)
- Define input schema with Zod (`.input()`)
- Use `.query()` for reads, `.mutation()` for writes
@@ -255,6 +264,7 @@ web/src/pages/api/public/
```
**Dynamic routes:**
- `[param].ts` → Single dynamic segment (e.g., `/api/public/scores/[scoreId].ts`)
- `[...param].ts` → Catch-all route (e.g., `/api/public/[...path].ts`)
@@ -345,12 +355,63 @@ export default withMiddlewares({
```
**Key Points:**
- Use `withMiddlewares` for all public API routes (provides CORS, error handling, OpenTelemetry)
- Use `createAuthedProjectAPIRoute` for authenticated endpoints (handles auth, rate limiting, validation)
- Define separate handlers for each HTTP method
- Input/output validated with Zod schemas
- Delegate to services for business logic
### Versioned API Type Location
When a feature has versioned public API types, place them in `packages/shared/`
under the feature's `interfaces/api/` folder, one subdirectory per version.
The scores feature (`packages/shared/src/features/scores/interfaces/api/`) is
the canonical example — the `GetScoresQueryV1` / `GetScoresResponseV1` symbols
imported from `@langfuse/shared` originate there.
Do not create a flat file (e.g. `<domain>-api-v2.ts`) and do not place
versioned types in `web/src/features/public-api/types/`.
```
packages/shared/src/features/<domain>/interfaces/api/
├── v1/
│ ├── schemas.ts # Zod schemas for request/response shapes
│ ├── endpoints.ts # Composed request/response types
│ └── validation.ts # Cross-field validation helpers
├── v2/
│ └── ...
└── vN/
└── ...
```
### Fern API Definitions
When modifying public API types in `web/src/features/public-api/types/` or under
`packages/shared/src/features/<domain>/interfaces/api/`, update the matching
Fern API definitions in `fern/apis/server/definition/`.
**Zod to Fern Type Mapping:**
| Zod Type | Fern Type | Example |
| -------------- | ----------------------- | --------------------------------------------------------- |
| `.nullish()` | `optional<nullable<T>>` | `z.string().nullish()` -> `optional<nullable<string>>` |
| `.nullable()` | `nullable<T>` | `z.string().nullable()` -> `nullable<string>` |
| `.optional()` | `optional<T>` | `z.string().optional()` -> `optional<string>` |
| Always present | `T` | `z.string()` -> `string` |
Add a source comment at the top of each Fern type that references the
TypeScript source:
```yaml
# Source: web/src/features/public-api/types/traces.ts - APITrace
Trace:
properties:
id: string
name:
type: nullable<string>
```
### Simple Public Routes
For routes that don't need authentication:
@@ -433,6 +494,7 @@ export class ScoresApiService {
```
**Key Points:**
- Services contain business logic, not routing logic
- Services should NOT import tRPC or Next.js types
- Services can call repositories, Prisma, ClickHouse directly
@@ -442,6 +504,7 @@ export class ScoresApiService {
### Where to Put Services
**Feature-specific services:**
```
web/src/features/
├── datasets/
@@ -456,6 +519,7 @@ web/src/features/
```
**Shared services:**
```
packages/shared/src/server/services/
├── SlackService.ts
@@ -497,7 +561,7 @@ import { convertClickhouseToDomain } from "./traces_converters";
*/
export const getTracesByIds = async (
projectId: string,
traceIds: string[]
traceIds: string[],
): Promise<TraceRecordReadType[]> => {
const rows = await queryClickhouse<TraceRecordReadType>({
query: `
@@ -519,7 +583,7 @@ export const getTracesByIds = async (
* Upsert trace to ClickHouse
*/
export const upsertTrace = async (
trace: TraceRecordInsertType
trace: TraceRecordInsertType,
): Promise<void> => {
await upsertClickhouse({
table: "traces",
@@ -536,6 +600,7 @@ export const upsertTrace = async (
```
**Key Points:**
- Use `queryClickhouse` for SELECT queries
- Use `upsertClickhouse` for INSERT/UPDATE
- Use `commandClickhouse` for DDL (ALTER TABLE, etc.)
@@ -546,12 +611,14 @@ export const upsertTrace = async (
### When to Use Repositories
**Use repositories for:**
- Complex ClickHouse queries with CTEs, joins, aggregations
- Queries used in multiple places (DRY principle)
- Data transformation from DB types to domain models
- Streaming large result sets
**Use direct Prisma/ClickHouse for:**
- Simple CRUD operations
- One-off queries
- Prototyping (can refactor to repository later)
@@ -645,9 +712,7 @@ export async function createScoreWithValidation({
```typescript
// packages/shared/src/server/repositories/scores.ts
export const upsertScore = async (
score: ScoreInsertType
): Promise<void> => {
export const upsertScore = async (score: ScoreInsertType): Promise<void> => {
// ✅ Pure data access - no business logic
await upsertClickhouse({
table: "scores",
@@ -719,6 +784,7 @@ export const scoresRouter = createTRPCRouter({
```
**Why it's bad:**
- Business logic tied to tRPC (can't reuse in public API)
- Hard to test (need to mock tRPC context)
- No separation of concerns
@@ -817,9 +883,7 @@ export const upsertScore = async (
```typescript
// ✅ GOOD: Pure data access, no business logic
export const upsertScore = async (
score: ScoreInsertType
): Promise<void> => {
export const upsertScore = async (score: ScoreInsertType): Promise<void> => {
await upsertClickhouse({
table: "scores",
records: [score],
@@ -838,7 +902,7 @@ export const upsertScore = async (
**Related Files:**
- [../AGENTS.md](../AGENTS.md) - Main backend development guidelines
- [../SKILL.md](../SKILL.md) - Main backend development guidelines
- [architecture-overview.md](architecture-overview.md) - System architecture
- [middleware-guide.md](middleware-guide.md) - Middleware patterns
- [database-patterns.md](database-patterns.md) - Database access patterns
@@ -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,75 +632,19 @@ 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/`
---
**Related Files:**
- [../AGENTS.md](../AGENTS.md) - Main guide
- [../SKILL.md](../SKILL.md) - Main guide
- [routing-and-controllers.md](routing-and-controllers.md) - Controllers that use services
- [database-patterns.md](database-patterns.md) - Prisma and repository patterns
- [testing-guide.md](testing-guide.md) - Testing service and repository code
@@ -4,26 +4,66 @@ 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 |
| Worker | Vitest | `worker/src/__tests__/` | Queue processors and streams |
| Test Type | Framework | Location | Purpose |
| ----------- | --------- | ---------------------------------------- | ----------------------------------- |
| 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";
@@ -63,6 +103,7 @@ describe("Dataset API", () => {
```
**Key Points:**
- Uses `makeZodVerifiedAPICall` for type-safe API testing
- Tests HTTP status codes and response validation
- Tests both success and error cases
@@ -73,7 +114,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 {
@@ -123,7 +164,9 @@ describe("Event Repository Tests", () => {
// Test the service function
const result = await getObservationsWithModelDataFromEventsTable({
projectId,
filter: [{ type: "string", column: "id", operator: "=", value: generationId }],
filter: [
{ type: "string", column: "id", operator: "=", value: generationId },
],
limit: 1000,
offset: 0,
});
@@ -163,18 +206,24 @@ describe("Event Repository Tests", () => {
const result = await getObservationsWithModelDataFromEventsTable({
projectId,
filter: [
{ type: "stringOptions", column: "type", operator: "any of", value: ["GENERATION"] }
{
type: "stringOptions",
column: "type",
operator: "any of",
value: ["GENERATION"],
},
],
limit: 1000,
offset: 0,
});
expect(result.every(o => o.type === "GENERATION")).toBe(true);
expect(result.every((o) => o.type === "GENERATION")).toBe(true);
});
});
```
**Key Points:**
- Tests service/repository functions directly
- Uses ClickHouse and Prisma test data
- Always cleanup test data after tests
@@ -186,7 +235,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";
@@ -205,16 +254,20 @@ async function prepare() {
user: {
id: "user-1",
name: "Demo User",
organizations: [{
id: org.id,
name: org.name,
role: "OWNER",
projects: [{
id: project.id,
role: "ADMIN",
name: project.name,
}],
}],
organizations: [
{
id: org.id,
name: org.name,
role: "OWNER",
projects: [
{
id: project.id,
role: "ADMIN",
name: project.name,
},
],
},
],
},
};
@@ -287,13 +340,17 @@ describe("automations trpc", () => {
...session,
user: {
...session.user!,
organizations: [{
...session.user!.organizations[0],
projects: [{
...session.user!.organizations[0].projects[0],
role: "VIEWER", // VIEWER can't create automations
}],
}],
organizations: [
{
...session.user!.organizations[0],
projects: [
{
...session.user!.organizations[0].projects[0],
role: "VIEWER", // VIEWER can't create automations
},
],
},
],
},
};
@@ -325,6 +382,7 @@ describe("automations trpc", () => {
```
**Key Points:**
- Uses `prepare()` helper to set up test context
- Creates authenticated caller with `appRouter.createCaller`
- Tests both success and permission error cases
@@ -457,6 +515,7 @@ describe("batch export test suite", () => {
```
**Key Points:**
- Uses vitest (not Jest) for worker tests
- Tests stream functions with async iteration
- Creates isolated test data per test
@@ -464,92 +523,21 @@ 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
Common targeted forms:
# 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
```
- Web server tests: `pnpm --filter web run test <file-or-pattern>`
- Web client tests: `pnpm --filter web run test-client <file-or-pattern>`
- Worker tests: `pnpm --filter worker run test <file-or-pattern>`
---
**Related Files:**
- [../AGENTS.md](../AGENTS.md) - Main backend guidelines
- [../SKILL.md](../SKILL.md) - Main backend guidelines
- [architecture-overview.md](architecture-overview.md) - Architecture patterns
- [services-and-repositories.md](services-and-repositories.md) - Service and repository examples
File diff suppressed because it is too large Load Diff
@@ -12,23 +12,24 @@ npx skills add ClickHouse/clickhouse-agent-skills
**28 atomic rules** organized by prefix:
| Prefix | Count | Coverage |
|--------|-------|----------|
| `schema-pk-*` | 4 | PRIMARY KEY selection, cardinality ordering |
| `schema-types-*` | 5 | Data types, LowCardinality, Nullable |
| `schema-partition-*` | 4 | Partitioning strategy, lifecycle management |
| `schema-json-*` | 1 | JSON type usage |
| `query-join-*` | 5 | JOIN algorithms, filtering, alternatives |
| `query-index-*` | 1 | Data skipping indices |
| `query-mv-*` | 2 | Incremental and refreshable MVs |
| `insert-batch-*` | 1 | Batch sizing (10K-100K rows) |
| `insert-async-*` | 2 | Async inserts, data formats |
| `insert-mutation-*` | 2 | Mutation avoidance |
| `insert-optimize-*` | 1 | OPTIMIZE FINAL avoidance |
| Prefix | Count | Coverage |
| -------------------- | ----- | ------------------------------------------- |
| `schema-pk-*` | 4 | PRIMARY KEY selection, cardinality ordering |
| `schema-types-*` | 5 | Data types, LowCardinality, Nullable |
| `schema-partition-*` | 4 | Partitioning strategy, lifecycle management |
| `schema-json-*` | 1 | JSON type usage |
| `query-join-*` | 5 | JOIN algorithms, filtering, alternatives |
| `query-index-*` | 1 | Data skipping indices |
| `query-mv-*` | 2 | Incremental and refreshable MVs |
| `insert-batch-*` | 1 | Batch sizing (10K-100K rows) |
| `insert-async-*` | 2 | Async inserts, data formats |
| `insert-mutation-*` | 2 | Mutation avoidance |
| `insert-optimize-*` | 1 | OPTIMIZE FINAL avoidance |
## Trigger Phrases
This skill activates when you:
- "Create a table for..."
- "Optimize this query..."
- "Design a schema for..."
@@ -38,13 +39,13 @@ This skill activates when you:
## Files
| File | Purpose |
|------|---------|
| `SKILL.md` | Quick reference and decision frameworks |
| `AGENTS.md` | Complete rule reference (auto-generated) |
| `rules/*.md` | Individual rule definitions |
| File | Purpose |
| ------------ | -------------------------------------------------------- |
| `SKILL.md` | Review workflow, quick reference, and rule-selection entrypoint |
| `rules/*.md` | Individual rule definitions |
## Related Documentation
All rules link to official ClickHouse documentation:
- [ClickHouse Best Practices](https://clickhouse.com/docs/best-practices)
@@ -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.
---
@@ -48,11 +59,13 @@ When performing a formal review of schemas, queries, or data ingestion:
9. `rules/schema-partition-lifecycle.md` - Partitioning purpose
**Check for:**
- [ ] PRIMARY KEY / ORDER BY column order (low-to-high cardinality)
- [ ] Data types match actual data ranges
- [ ] 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)
@@ -65,6 +78,7 @@ When performing a formal review of schemas, queries, or data ingestion:
5. `rules/schema-pk-filter-on-orderby.md` - Filter alignment with ORDER BY
**Check for:**
- [ ] Filters use ORDER BY prefix columns
- [ ] JOINs filter tables before joining (not after)
- [ ] Correct JOIN algorithm for table sizes
@@ -81,6 +95,7 @@ When performing a formal review of schemas, queries, or data ingestion:
5. `rules/insert-optimize-avoid-final.md` - OPTIMIZE TABLE risks
**Check for:**
- [ ] Batch size 10K-100K rows per INSERT
- [ ] No ALTER TABLE UPDATE for frequent changes
- [ ] ReplacingMergeTree or CollapsingMergeTree for update patterns
@@ -117,19 +132,19 @@ Structure your response as follows:
## Rule Categories by Priority
| Priority | Category | Impact | Prefix | Rule Count |
|----------|----------|--------|--------|------------|
| 1 | Primary Key Selection | CRITICAL | `schema-pk-` | 4 |
| 2 | Data Type Selection | CRITICAL | `schema-types-` | 5 |
| 3 | JOIN Optimization | CRITICAL | `query-join-` | 5 |
| 4 | Insert Batching | CRITICAL | `insert-batch-` | 1 |
| 5 | Mutation Avoidance | CRITICAL | `insert-mutation-` | 2 |
| 6 | Partitioning Strategy | HIGH | `schema-partition-` | 4 |
| 7 | Skipping Indices | HIGH | `query-index-` | 1 |
| 8 | Materialized Views | HIGH | `query-mv-` | 2 |
| 9 | Async Inserts | HIGH | `insert-async-` | 2 |
| 10 | OPTIMIZE Avoidance | HIGH | `insert-optimize-` | 1 |
| 11 | JSON Usage | MEDIUM | `schema-json-` | 1 |
| Priority | Category | Impact | Prefix | Rule Count |
| -------- | --------------------- | -------- | ------------------- | ---------- |
| 1 | Primary Key Selection | CRITICAL | `schema-pk-` | 4 |
| 2 | Data Type Selection | CRITICAL | `schema-types-` | 5 |
| 3 | JOIN Optimization | CRITICAL | `query-join-` | 5 |
| 4 | Insert Batching | CRITICAL | `insert-batch-` | 1 |
| 5 | Mutation Avoidance | CRITICAL | `insert-mutation-` | 2 |
| 6 | Partitioning Strategy | HIGH | `schema-partition-` | 4 |
| 7 | Skipping Indices | HIGH | `query-index-` | 1 |
| 8 | Materialized Views | HIGH | `query-mv-` | 2 |
| 9 | Async Inserts | HIGH | `insert-async-` | 2 |
| 10 | OPTIMIZE Avoidance | HIGH | `insert-optimize-` | 1 |
| 11 | JSON Usage | MEDIUM | `schema-json-` | 1 |
---
@@ -224,11 +239,3 @@ Each rule file in `rules/` contains:
- **Incorrect example**: Anti-pattern with explanation
- **Correct example**: Best practice with explanation
- **Additional context**: Trade-offs, when to apply, references
---
## Full Compiled Document
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.
+5
View File
@@ -23,6 +23,11 @@ feature.
`clickhouse-best-practices` skill.
- If the review touches backend code, also use the shared
`backend-dev-guidelines` skill where relevant.
- If the change accepts a user-supplied URL, adds outbound HTTP, introduces a
new integration, or touches secrets, RBAC, or redirect handling, also use
the shared [`security-review`](../security-review/SKILL.md) skill. Run its
[`references/checklist.md`](../security-review/references/checklist.md)
before signoff.
## Review Priorities
@@ -41,6 +41,22 @@ This is the canonical shared review checklist for Langfuse.
- `top-banner-offset` / `pt-banner-offset` - For sticky/fixed/absolute positioning and padding
- `h-screen-with-banner` / `min-h-screen-with-banner` - For full-height containers accounting for banners
## Security
- For changes that accept a user-supplied URL, host, `endpoint`, `baseURL`,
or webhook target, or that issue a new outbound HTTP request, run the
shared [`security-review`](../../security-review/SKILL.md) skill and treat
its [`outbound-url-validation.md`](../../security-review/references/outbound-url-validation.md)
defenses (save-time validation + use-time / connection-time validation +
redirect-time validation) as required. Plain `fetch(<userUrl>)` or SDK
init with `endpoint: <userUrl>` without one of the canonical validators
(`validateLlmConnectionBaseURL`, `validateWebhookURL`,
`validateBlobStorageEndpoint`, or a new wrapper around
`validateOutboundUrlHost`) is a finding.
- For changes that add a new integration, secret-bearing field, redirect
follower, or RBAC scope, run the rest of the
[`security-review/references/checklist.md`](../../security-review/references/checklist.md).
## JavaScript / TypeScript Style
- use concat instead of spread to avoid stack overflow with large arrays
@@ -0,0 +1,74 @@
---
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 [`weekly-production-review`](../weekly-production-review/SKILL.md) when
the user asks for a weekly engineering overview of production bugs, pages,
and incidents.
- 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>"`.
@@ -41,7 +41,7 @@ Use this skill when a change affects what users see or do in the browser.
6. If the page changed materially, inspect the resulting UI state and compare
it against the intended behavior from the task or existing patterns.
7. If the browser session fails, inspect traces and artifacts under
`.playwright-mcp/`.
`/tmp/playwright-mcp`.
## Output Expectations
@@ -0,0 +1,74 @@
---
name: frontend-large-feature-architecture
description: |
Use when building, changing, or refactoring large Langfuse frontend features,
virtualized lists, large tables, controller components, local feature state,
Zustand stores, row selection, high-frequency UI state, or
rendering-performance issues.
---
# Frontend Large Feature Architecture
Use this skill when building, changing, or refactoring a large frontend
surface.
In this skill, "controller" means a component or hook that owns feature logic:
data fetching, view state, table/list state, effects, actions, and expensive
rendering. The problem is not the name; it is one place owning too many
changing responsibilities.
## Big Feature Rules
When a feature grows, split rendering from logic. Most components should be
view-only. Data preparation should be pure. Complex user actions should live in
external async functions or local-store actions. Effects are integration
boundaries, not the normal way to derive state.
For the full rules, read
[`references/big-feature-rules.md`](references/big-feature-rules.md).
## Required Model
- The page/view owns lifecycle and creates feature-scoped dependencies.
- Server/query state stays in tRPC/React Query; route state stays in the
router/filter hooks.
- High-frequency feature UI state belongs in a per-mount local vanilla Zustand
store. Create it with lazy `useState`, not `useMemo`.
- Global stores are only for truly cross-feature, cross-route product state.
- React context may provide a stable store or action owner. Do not put
frequently changing state directly in provider values.
- Rendered rows/cells/items should be view-only or narrow containers. Put
effects, subscriptions, data loading, and workflows outside expensive views.
- Shared `src/components/*` exports should stay context-free or receive
explicit props. Put view-scoped Zustand consumers in `src/features/*`.
- Large feature folders should have a concise `README.md` owner map.
- Migrate real features through small PRs that improve one state boundary,
action workflow, data-preparation seam, or render boundary.
## Local Store Default
Prefer a local vanilla Zustand store for large or high-frequency feature state.
The store is created by the page/view and destroyed on unmount.
Use selectors that return primitives or stable references. If a component needs
multiple values, use shallow selector helpers or split subscriptions so one
changing field does not rerender unrelated UI.
Complex user workflows should live in `actions/*.ts` files or store actions.
The component wires hooks and passes dependencies; the action owns the workflow.
## When To Read References
- For the core big-feature rules and migration reality, read
[`references/big-feature-rules.md`](references/big-feature-rules.md).
- For virtualized lists, translated DOM, row measurement, and scroll rerenders,
read [`references/virtualized-lists.md`](references/virtualized-lists.md).
- For local feature stores and splitting large components, read
[`references/local-feature-state.md`](references/local-feature-state.md).
- For feature `README.md` owner maps, read
[`references/feature-readmes.md`](references/feature-readmes.md).
- For a step-by-step migration from a controller component to a managed feature
pattern, read
[`references/controller-migration.md`](references/controller-migration.md).
This is the default reference for traces/observations tables, sessions,
experiments, prompts, evals, datasets, and other controller-heavy surfaces.
@@ -0,0 +1,7 @@
interface:
display_name: "Frontend Feature Architecture"
short_description: "Build, change, or refactor React features"
default_prompt: "Use $frontend-large-feature-architecture to build, change, or refactor this large frontend surface with clear state ownership, stable subscriptions, and view-only render boundaries."
policy:
allow_implicit_invocation: true
@@ -0,0 +1,98 @@
# Big Feature Rules
Use these rules when a feature keeps growing and the instinct is to add one
more state variable, prop, callback, or effect to the same component.
Here, "controller" means a component or hook that owns feature logic: state,
effects, data loading, subscriptions, actions, and derived data. Some
controller code is necessary. The failure mode is one controller owning too many
changing responsibilities and waking too much UI.
## Ownership Baseline
- The page/view owns lifecycle and creates feature-scoped dependencies.
- Server/query state stays in tRPC/React Query.
- Route state stays in router/filter hooks.
- High-frequency local UI state belongs in a per-mount feature store.
- Global stores are only for product state shared across routes or features.
- Pure data preparation turns fetched data into UI data before rendering.
- Complex workflows live in `actions/*.ts` or store actions.
- Effects are integration boundaries, not ordinary state derivation.
- Expensive rows/cells/items should be view-only behind narrow containers.
## Hard Rules
1. Growth usually means React rendering and feature logic need to be separated.
A bigger component is not an architecture.
2. Most components should be view-only. They render what they are given, or
they read a small selected value from a context-available feature store.
3. Complicated data preparation belongs in separate pure functions. Backend
data should flow one way: fetched data -> compiled UI data -> render.
4. User actions that trigger complex logic should live outside render
components as named async functions or store actions. If the action needs a
lot of context, pass the feature store instance and let the action read what
it needs.
5. A feature with both local store state and React Query data needs an explicit
bridge in a custom hook or named action.
6. Prefer view-scoped local feature stores over global stores for page-instance
state. Filters, selected rows, expanded rows, lazy load state, drawers, and
local actions usually belong to one mounted page instance.
7. Create local store instances with a lazy `useState` initializer, not
`useMemo`. The store is a per-mount instance, not a render-time derived
value.
8. Effects should not be normal state mutators. Before adding an effect, try
pure data preparation, a store action, or an explicit event handler.
9. Do not copy existing large Langfuse feature components as examples. Treat
them as legacy unless they follow this skill.
10. Prefer small reliable PRs over a large rewrite. Each PR should improve one
state boundary, action workflow, data-preparation seam, or render boundary,
then update the feature README or migration note with the next slice.
## Migration Reality
Most large frontend features are not yet in the ideal shape. Traces,
observations, experiments, prompts, evals, datasets, and session views all have
some controller-heavy surfaces. Do not copy an existing large component just
because it works today. Treat it as a migration candidate and move it one
state/action/data-preparation boundary at a time.
For the step-by-step path, read `controller-migration.md`.
## Small PR Policy
A good big-feature PR is intentionally incomplete. It should change one
semantic interaction and keep behavior stable: one selection boundary, one
action workflow, one pure data-preparation helper, or one render boundary.
Do not bundle file reorganization, state extraction, visual changes, and
behavior fixes in the same PR unless the user explicitly asks for that scope.
When a reorganization is needed, prefer a rename-only PR first or a later
follow-up PR.
## Effects And Actions
Effects are for subscriptions, observers, imperative third-party APIs,
one-time initialization, and cleanup. Keep them in containers or feature hooks,
not view components. If an effect writes state repeatedly, make the store action
idempotent.
Complex actions should be callable without rendering a component. Put workflows
in `actions/*.ts` or named store actions. Components wire hooks and pass
dependencies; actions own the workflow.
```ts
await applyBulkAction({
store,
queryClient,
projectId,
});
```
Actions must not call React hooks. Pass hook results, query helpers, the local
store instance, or narrow callback dependencies into the action. If an action
needs substantial data preparation, export a pure helper next to it so the
transformation can be tested independently.
Do not pass twenty props through the tree so a button can do feature-level work,
and do not leave complex workflows inline in a page controller because that
controller happened to have all dependencies in scope.
@@ -0,0 +1,123 @@
# Controller Migration Guide
Use this when a frontend surface has grown into one component or hook that owns
data fetching, route glue, filters, table/list state, selection, drawers,
actions, and expensive rendering.
The target is clear ownership, not "everything in a store." Start from the
ownership baseline in `big-feature-rules.md`.
Most existing features are not there yet. Migrate in narrow slices and keep
behavior stable.
## Realistic Migration Strategy
Do not start by designing the perfect final feature. Start by making the next
change safer than the previous one.
For each migration PR, write down:
- the current controller problem being targeted
- the single state/action/data-preparation/render boundary being improved
- what behavior must remain unchanged
- what instrumentation or tests prove the slice worked
- what still remains spread across the feature
- the next recommended atomic slice
This is how large features become managed features without review-hostile
rewrites. The feature README is the living owner map; update it in place as the
feature moves forward.
## Step-by-Step Path
1. **Map the controller.** List every state group, query, derived value, effect,
callback, action, and expensive child render owned by the component.
2. **Classify state.** Separate server/query, route, persisted browser,
high-frequency local UI, derived view data, imperative integration, and
one-off modal/form state.
3. **Instrument one symptom.** Pick a concrete interaction such as row
selection, scroll, filter change, drawer open, form step change, or
saved-view change. Measure what rerenders, remounts, refetches, or
recalculates.
4. **Choose one boundary.** Start with the smallest high-value boundary:
selection, lazy-row state, batch action workflow, filter-target mapping,
column/view state, wizard step state, or pure data preparation. Do not
migrate everything at once.
5. **Choose the lightest tool.** A pure helper or action extraction may be the
right first PR. Add a local store only when selective subscriptions or
per-mount persistence are needed.
6. **Create a local store instance when needed.** Use lazy `useState` in the
page/view:
```ts
const [store] = useState(() => createFeatureStore(initialState));
```
Provide only this stable store instance through context.
7. **Move mutations into named actions.** Put state-changing logic in store
actions or external action functions. Keep components responsible for user
events, not workflows.
8. **Split containers from views.** Containers may subscribe, call hooks, or
fetch data. Views should render props or tiny selected values.
9. **Move data preparation out of render.** Put expensive or complicated
transformations in pure functions. Backend data should flow into compiled UI
data, then into rendering.
10. **Bridge query state explicitly.** If local store decisions depend on React
Query data, use a named feature hook or action to express that relationship.
11. **Isolate imperative integration.** Virtualizers, observers, keyboard
listeners, third-party DOM mutation handling, and timers belong in narrow
integration hooks.
12. **Update the feature README.** Record what this PR improved, desired
boundaries, known spread state, and the next extraction target.
13. **Remove debug instrumentation.** Temporary logs are useful during
migration, but should not survive the slice.
14. **Repeat.** Each slice should make one semantic interaction narrower and
easier to reason about.
## Feature-Specific First Slices
Use the feature's current shape to pick the first slice:
- **Traces and observations tables**: start with row selection, select-all,
batch actions, or expensive cell wrappers.
- **Session detail and session events**: isolate virtualization, lazy-row load
state, and dynamic measurement from rendered row content.
- **Experiment result tables**: start with selected-row state, filter-target
mapping, run/evaluation batch actions, or pure helpers for comparison column
construction.
- **Experiment creation wizards**: separate submitted form data from display
state such as active step, selected prompt labels, schema display names, and
evaluator selection.
- **Prompt management**: split prompt detail route/query state, label/version
selection, prompt history data preparation, and mutation workflows.
- **Eval template and evaluator forms**: extract form defaults, model/provider
preparation, validation helpers, and submit workflows before adding a store.
- **Datasets and dataset runs**: isolate active-cell/compare-field state,
table selection, run comparison preparation, and upload/import workflows.
If a feature already has hooks for part of this work, treat them as partial
migration, not proof that the whole surface is healthy.
## Acceptance Criteria
- A local state change wakes only components that selected that state.
- Page components no longer rebuild columns, filters, row wrappers, and action
callbacks for unrelated row-level changes.
- Expensive rows/cells are view-only behind narrow containers.
- Effects are integration boundaries or one-time initialization, not ordinary
data derivation.
- Complex workflows can be called without rendering the page.
- The feature README tells the next developer what has improved, what remains
spread, and what the next small PR should target.
Avoid:
- Replacing a giant component with a giant global store.
- Moving all state at once without measured acceptance criteria.
- Treating memoization as the architecture.
- Putting provider-coupled store hooks into shared `src/components/*` exports.
- Leaving a workflow inline in the page because the page had every dependency in
scope.
- Using the README as a victory statement while hiding remaining controller
state. Be explicit about remaining debt.
@@ -0,0 +1,89 @@
# Feature READMEs
Large frontend feature folders should have a short `README.md` that acts as an
owner map for humans and agents. Prefer `README.md` over `FEATURE.md` to match
the existing `web/src/features/*` convention. Use `FEATURE.md` only if a folder
already has a user-facing or generated README.
The README is not a changelog. It should describe durable boundaries and point
to deeper migration notes.
## Required Sections
- **Surface**: what product surface the folder owns.
- **Entry Points**: route files or parent components that mount the feature,
and the page/view lifecycle owner files they call.
- **Structure**: what each subfolder owns. Use root `components/` only for
components reused across surfaces inside the feature. Use surface folders such
as `detail/` for page controllers, local stores, `actions/`, integration
hooks, and surface-private containers.
- **External Consumers**: other features that import these components. This
keeps shared exports context-free and prevents accidental provider coupling.
- **State Ownership**: where server/query state, route state, local feature
state, global product state, DOM integration state, and view-only props live.
- **Performance And Stability Boundaries**: which interactions are
high-frequency or externally unstable, and which components are allowed to
rerender or measure because of them.
- **Migration State**: what has already been improved, what state/actions are
still spread, and the next one or two atomic slices.
- **Development Context**: agent skills, migration notes, or issue docs to read
before extending the feature.
## State Ownership Rules
Use the ownership baseline in `big-feature-rules.md`. The README only needs to
name where each state category lives in this feature and where known spread
state remains.
## Performance And Stability Map
Every large feature README should name the high-frequency interactions that
must stay narrow. Typical examples:
- scroll and virtualization updates
- row selection, hover, expansion, and lazy-loading
- filter, saved-view, and column-state changes
- drawers, peek navigation, and keyboard navigation
- browser translation or other third-party DOM mutation
- resize and dynamic row measurement
For each interaction, state which boundary should update. The page component
should not rerun expensive data preparation, recreate column/config objects, or
rerender unchanged expensive cells for unrelated state changes.
## Current-State Honesty
If a feature is mid-migration, say so. The README should make the desired
boundaries clear while naming known spread state as debt. Do not present a
partially migrated feature as the final pattern.
Use an update-in-place style rather than a changelog. For example:
- **Improved in current shape**: local store owns row selection; export action
moved to `actions/exportFeatureData.ts`; row view no longer subscribes to
filter state.
- **Still spread**: saved-view state remains in the page controller; filter
option preparation is still inline; mutation workflows still close over page
hooks.
- **Next slice**: extract filter-option preparation into pure helpers; move
batch action workflow into an action file; split route/query glue from view
components.
This makes small PRs reviewable while keeping the feature migration plan
visible. Do not wait for a perfect reorganization before documenting the
current state.
## PR-Scale Guidance
Feature README updates should match the PR size:
- For a small state/action extraction, add or update only the relevant
migration-state bullets.
- For a new feature folder structure, include the owner map and external
consumers before moving logic into the folder.
- For a rename-only PR, document intended structure but avoid changing behavior.
- For behavior PRs, avoid unrelated file moves unless they are required for the
boundary being improved.
The README should help the next contributor avoid falling back into the same
large component, not argue that the migration is complete.
@@ -0,0 +1,162 @@
# Local Feature State
Large frontend features should not let one component or hook own everything.
That shape makes one checkbox, hover, or row-selection change rerun the code
that also builds filters, columns, data wrappers, expensive cells, drawers,
actions, and routing glue.
The goal is to make each state change wake only the UI and actions that
semantically depend on it.
## Default Pattern
Start from the ownership baseline in `big-feature-rules.md`, then add a local
vanilla Zustand store only when state is high-frequency, shared across multiple
subtrees in the mounted feature, or must survive row/item remounts.
Use this shape:
1. The page/view creates one store instance with lazy `useState`.
2. Context provides only the stable store instance.
3. Components subscribe to the smallest useful slice.
4. Mutations live in named store actions.
5. Complex user workflows live in `actions/*.ts` or store actions.
6. Expensive cells/rows stay view-only behind narrow containers.
7. Large feature roots have a short `README.md` owner map.
## Feature README
For a concrete owner-map template, read `feature-readmes.md`. Do not use the
README as a changelog; record durable ownership facts and the next migration
slice.
## Why Local, Not Global
Langfuse pages are often local-state heavy: filters, saved views, selected rows,
expanded rows, drawers, peek navigation, lazy row load state, and view-local
actions. Those states usually belong to one mounted page instance, not the whole
application.
Use local feature stores for this state. Create the store in the page/view and
destroy it on unmount. This keeps multiple mounted instances independent, avoids
cross-route state leaks, and makes ownership visible.
Global state is reserved for product state that is genuinely shared across
features or routes. Do not promote state globally to avoid prop drilling or to
make a large component smaller.
Do not add a store just to make a PR look architectural. If the immediate
problem is an inline export workflow, duplicated filter option shaping, or a
large column builder, first extract an action or pure helper. Use the store when
state needs selective subscriptions or must survive row remounts within one
mounted feature instance.
## Creating The Store
Prefer lazy `useState` for local store instances:
```ts
const [store] = useState(() =>
createFeatureStore({
initialProjectId: projectId,
}),
);
```
This expresses the real lifecycle: one store instance for the committed mounted
view. The unused setter is acceptable. `useMemo` is the wrong default because it
is a render-time cache for derived values, not an ownership boundary for an
external store instance. A local store is stateful infrastructure, so treat its
identity as state.
`useRef` can also hold a stable instance, but prefer `useState` unless a ref is
needed for imperative setup. Keep store creation pure; sync changing route/query
inputs into named store actions such as `resetForFeature(...)` or `init(...)`.
## Independent Actions
Complex workflows should be independent functions. Put surface-specific actions
in `actions/*.ts`, or make them named store actions when they are tightly coupled
to local feature state.
The component wires hooks, route params, stores, analytics, and query helpers.
The action owns the workflow: refetching through callbacks, reading the passed
store if needed, calling pure helpers, performing browser side effects, and
emitting analytics.
Actions must not call React hooks. If a workflow needs a lot of context, pass
the local store instance or a small dependency object rather than threading long
prop chains through view components.
Example:
```ts
await exportFeatureData({
capture,
fetchDetails,
projectId,
refetchSummary,
selectedIds,
});
```
For substantial data shaping, export a pure helper next to the action so the
transformation can be tested without rendering the page.
## Store Shape
Use immutable plain objects for keyed state that must be selector-friendly:
```ts
type FeatureStoreState = {
selectedIds: Record<string, true>;
activeId: string | null;
actions: {
toggleSelected: (id: string, selected: boolean) => void;
setActiveId: (id: string | null) => void;
};
};
```
Avoid mutating `Set` or `Map` in place. If you use them, replace the whole
instance when updating.
## Anti-Patterns
- A table/list component owns selection, filters, columns, routing, peek state,
batch actions, local dialogs, and expensive rendered cells.
- Context provider `value` changes on row selection, hover, scroll, active row,
expanded row, or other high-frequency state.
- Local feature state is promoted to a global store even though it only belongs
to one mounted page instance.
- `useMemo` is used to own a local external store instance.
- A shared `src/components/*` component calls a feature-scoped store hook. That
silently breaks other callers that do not mount the feature provider.
- Memoization is the only fix. `memo` helps, but it does not fix a bad state
boundary.
- A callback depends on an inline config object and changes identity on every
render.
- Components subscribe to large objects when they only need a boolean.
- `useEffect` derives ordinary UI state from fetched data.
- A component passes a long chain of feature context through props just so a
button can perform an action.
- A page component keeps a complex async workflow inline because all the hooks
happen to be in scope there.
## Migration Steps
1. Instrument first: identify which semantic state change causes broad renders.
2. Choose the smallest useful boundary: store state, action workflow, pure data
preparation, or imperative integration hook.
3. Extract that one state group into a local store only when selective
subscriptions are needed.
4. Replace broad props with selector subscriptions at the smallest UI boundary.
5. Move related mutations into named actions.
6. Stabilize callbacks and data wrappers.
7. Move expensive data preparation into pure functions.
8. Move complex user actions into store actions or external functions under
`actions/*.ts`.
9. Update the feature README with what improved, what remains spread, and the
next atomic slice.
10. Remove debug instrumentation.
11. Repeat for the next state group.
@@ -0,0 +1,81 @@
# Virtualized Lists
Virtualized lists are render-boundary infrastructure. They should calculate
which item shells are visible and position those shells. They should not make
each row own feature state, effects, subscriptions, data loading, and workflows.
Langfuse uses `@tanstack/react-virtual` for virtualization. Find current
callsites before changing a virtualized surface, then apply the same
state-boundary rules as any large feature.
## Smartness Trap
The broken shape is:
- virtualizer rerenders on scroll
- parent recreates callbacks, config, row wrappers, or data objects
- row components receive changed props even though the semantic row did not
change
- row-local effects/load state reset or refire
- dynamic measurement observes DOM changed by an external mutator such as Google
Translate
- measurement updates virtualizer state, which rerenders the same rows again
That is not a small memoization bug. It is leaked state ownership.
The fix is to make scroll and measurement state update the smallest possible
integration boundary. If scrolling changes a virtual item offset, unchanged row
content should not receive new semantic props, refire effects, or recreate
expensive derived data.
## Google Translate DOM Behavior
Google Translate mutates rendered DOM after React has committed it. It can wrap
text nodes, replace text, and change element dimensions outside React's data
flow. React and TanStack Virtual do not know whether the changed DOM represents
stable translated content or a transient mutation.
Do not opt product UI out of translation with `translate="no"` unless product
explicitly chooses that. Langfuse must work under browser translation.
## Measurement Rules
- Always put the correct `data-index` on the row element TanStack treats as the
item.
- Do not combine live `measureElement` with externally mutated translated DOM
in text-heavy rows.
- Prefer fixed estimates plus overscan for simple rows.
- For dynamic text-heavy rows, use controlled measurement:
- `ResizeObserver` reads the row shell.
- Debounce commits.
- Do not commit while actively scrolling.
- Round heights to avoid sub-pixel churn.
- Call `virtualizer.resizeItem(index, height)`.
- If a row alternates between two heights repeatedly, clamp to a minimum
height that still allows later legitimate growth.
## Row Rules
- The virtualizer owns positioning only.
- State that must survive remounts lives outside the row instance.
- Expensive row content should be a memoized view component.
- Narrow row containers may subscribe to local store slices and queries.
- View components should receive stable props and perform no effects.
- Feature-scoped row containers belong under `src/features/*`; shared
`src/components/*` row exports should be context-free.
- Scrolling may rerender the virtualizer. It should not rerender unchanged
expensive row content.
- Do not use a global store to preserve row state across virtualization. Use a
view-scoped store owned by the mounted list/page instance.
## Migration Steps
1. Add temporary logs to identify whether scroll causes remounts, prop changes,
measurement loops, or query refetches.
2. Remove logs before shipping.
3. Move row-local state that must survive virtualization into a local store.
4. Stabilize callbacks and config objects passed to rows.
5. Replace live `measureElement` with fixed estimates or controlled measurement.
6. Move row logic into pure helpers or feature-local containers.
7. Verify with browser translation enabled, horizontal resize, and small
vertical scroll deltas.
+51
View File
@@ -0,0 +1,51 @@
---
name: git-workflow
description: |
Langfuse repo Git, GitHub, commit, branch, pull request, issue search,
release, and production-promotion workflow. Use when staging, committing,
pushing, opening PRs, searching GitHub issues, or changing release/promotion
behavior.
---
# Git Workflow
Use this skill for repo-specific Git, GitHub, pull request, and release
operations.
## Safety
- Inspect `git status` before staging or committing.
- Do not stage unrelated working-tree changes.
- Do not revert unrelated working-tree changes.
- Do not use destructive commands such as `git reset --hard` or
`git checkout --` unless explicitly requested.
- Keep commits focused and atomic.
- Never add secrets or credentials to the repo.
## Commits and Pull Requests
- Commit messages and PR titles must follow Conventional Commits:
`type(scope): description` or `type: description`.
- Use `feat` for new features and `fix` for bug fixes.
- Use a scope when it clarifies the affected area, for example
`fix(api): handle missing trace id`.
- Mark breaking changes with `!` in the type/scope or a `BREAKING CHANGE:`
footer.
- PR titles are validated by `.github/workflows/validate-pr-title.yml`.
- In PR descriptions, list impacted packages and executed verification
commands.
## GitHub
- Use `gh search issues` for GitHub issue search.
- Prefer non-interactive Git and GitHub commands where possible.
- Keep PRs narrow enough to review without unrelated refactors.
## Release
- Release workflow is managed at root with `pnpm run release`.
- Promote `main` to `production` via
`.github/workflows/promote-main-to-production.yml` or
`pnpm run release:cloud`.
- Do not change release/versioning flow without updating this skill and the
impacted package guides.
+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 a calling workflow 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."
@@ -0,0 +1,74 @@
---
name: pnpm-upgrade-package
description: >-
Upgrade pnpm workspace dependencies to target/latest versions:
direct/transitive bumps, release-age checks, temporary overrides,
minimumReleaseAgeExclude, lockfile/dedupe verification.
---
# PNPM Upgrade Package
Use this skill for interactive dependency bumps in Langfuse.
## Read Order
- Use this `SKILL.md` for the end-to-end workflow.
- Run the main helper once at the start of the upgrade:
`node .agents/skills/pnpm-upgrade-package/scripts/check-release-age-window.mjs <package> [targetVersion]`
## Apply This Skill
- Ask for the package name if the user did not provide one.
- Ask for the target version if the user did not provide one.
- Run the main helper once as the first analysis step and use that single
output for scope, exclusion decisions, and the final bump.
- If the target package is not directly declared anywhere, run
`pnpm why -r <package>` to find which direct dependency brings it in, then
inspect whether the current top-level parent already allows the requested
transitive version via its dependency range.
- If the current parent range already covers the requested transitive version,
prefer a lockfile refresh / reinstall path over bumping the parent manifest.
- 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 pnpm will not move an already-allowed transitive version, a scoped
`overrides` entry in `pnpm-workspace.yaml` may be used as a temporary
resolution tool. Before finishing, prove whether the override is still
required: remove it, run `pnpm install`, then run `pnpm dedupe`. Inspect the
diff after each generated change. If the target version remains without the
override, do not keep the override; keep or restore it only when pnpm reverts
or drifts from the requested version without 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, run `pnpm dedupe`. Always inspect the
diff after dedupe and revert that generated attempt if it introduces
unrelated churn.
- 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
current `minimumReleaseAge` window.
- Ask before adding `minimumReleaseAgeExclude` entries for the target package,
exact dependency companions from `dependencies` or `optionalDependencies`, or
locally installed exact peer dependencies.
- Finish with `pnpm why -r <package>` to confirm that only the intended version
remains in the workspace.
## Quick Commands
- Analysis pass:
`node .agents/skills/pnpm-upgrade-package/scripts/check-release-age-window.mjs <package> <targetVersion>`
- Transitive provenance / final graph verification:
`pnpm why -r <package>`
- Inspect a current parent manifest on the registry:
`npm view <parent>@<installedVersion> dependencies peerDependencies optionalDependencies --json`
- Optional lockfile cleanup:
`pnpm dedupe`
- Bump in the root workspace:
`pnpm -w up <package>@<version>`
- Bump in one workspace:
`pnpm --filter web up <package>@<version>`
- Bump everywhere that should move together:
`pnpm -r up <package>@<version>`
- Verify temporary override removal:
remove the override, then run `pnpm install` and `pnpm dedupe`
@@ -0,0 +1,4 @@
interface:
display_name: "PNPM Upgrade Package"
short_description: "Interactive pnpm package bump workflow"
default_prompt: "Use $pnpm-upgrade-package to upgrade a package in this pnpm workspace, asking me for the package or version if I did not provide them."
@@ -0,0 +1,418 @@
#!/usr/bin/env node
import { join } from "node:path";
import {
entryCoversVersion,
findLocalPackageReferences,
formatWorkspaceReference,
getRootPnpmControls,
readWorkspaceConfig,
} from "./lib/workspace-utils.mjs";
const args = process.argv.slice(2);
const asJson = args.includes("--json");
const positional = args.filter((arg) => !arg.startsWith("--"));
const packageName = positional[0];
const requestedTargetVersion = positional[1] ?? null;
if (!packageName) {
console.error(
"Usage: node .agents/skills/pnpm-upgrade-package/scripts/check-release-age-window.mjs <package> [targetVersion] [--json]",
);
process.exit(1);
}
const repoRoot = process.cwd();
const workspaceConfig = readWorkspaceConfig(join(repoRoot, "pnpm-workspace.yaml"));
const minimumReleaseAgeMinutes = workspaceConfig.minimumReleaseAge ?? 0;
const thresholdMs = Date.now() - minimumReleaseAgeMinutes * 60 * 1000;
const REGISTRY_FETCH_TIMEOUT_MS = 30_000;
const registryCache = new Map();
const workspaceReferenceCache = new Map();
const getWorkspaceReferences = (name) => {
if (!workspaceReferenceCache.has(name)) {
workspaceReferenceCache.set(name, findLocalPackageReferences(repoRoot, name));
}
return workspaceReferenceCache.get(name);
};
function printSectionHeader(title) {
console.log("");
console.log(title);
}
function isPrerelease(version) {
return version.includes("-");
}
function isExactVersion(spec) {
return /^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/.test(spec.trim());
}
function getMatchingExcludeEntries(name, version) {
return workspaceConfig.minimumReleaseAgeExclude.filter((entry) =>
entryCoversVersion(entry, name, version),
);
}
async function fetchRegistryPackage(name) {
if (registryCache.has(name)) return registryCache.get(name);
const abortController = new AbortController();
const timeoutId = setTimeout(
() => abortController.abort(),
REGISTRY_FETCH_TIMEOUT_MS,
);
timeoutId.unref?.();
try {
const response = await fetch(
`https://registry.npmjs.org/${encodeURIComponent(name)}`,
{
headers: {
accept: "application/json",
"user-agent": "langfuse-pnpm-upgrade-package-skill",
},
signal: abortController.signal,
},
);
if (!response.ok) {
throw new Error(
`Failed to fetch ${name} from npm registry: ${response.status}`,
);
}
const metadata = await response.json();
registryCache.set(name, metadata);
return metadata;
} catch (error) {
if (error?.name === "AbortError") {
throw new Error(
`Timed out fetching ${name} from npm registry after ${REGISTRY_FETCH_TIMEOUT_MS}ms`,
{ cause: error },
);
}
throw error;
} finally {
clearTimeout(timeoutId);
}
}
function getInstallability(metadata, name, version) {
const publishedAt = metadata.time?.[version] ?? null;
const publishedAtMs = publishedAt ? Date.parse(publishedAt) : null;
const matchingExcludeEntries = getMatchingExcludeEntries(name, version);
const isYoungerThanMinimumReleaseAge =
publishedAtMs != null ? publishedAtMs > thresholdMs : null;
const isInstallableWithoutNewExclude =
isYoungerThanMinimumReleaseAge == null
? null
: !isYoungerThanMinimumReleaseAge || matchingExcludeEntries.length > 0;
return {
name,
version,
publishedAt,
isYoungerThanMinimumReleaseAge,
isInstallableWithoutNewExclude,
matchingExcludeEntries,
suggestedExclude:
isInstallableWithoutNewExclude === false ? `${name}@${version}` : null,
};
}
function selectLatestInstallableVersion(metadata, name) {
const times = metadata.time ?? {};
return (
Object.keys(metadata.versions ?? {})
.filter((version) => times[version] && !isPrerelease(version))
.sort((left, right) => Date.parse(times[right]) - Date.parse(times[left]))
.map((version) => getInstallability(metadata, name, version))
.find((candidate) => candidate.isInstallableWithoutNewExclude) ?? null
);
}
function collectManifestEntries(manifest, fields) {
const merged = new Map();
for (const field of fields) {
for (const [name, spec] of Object.entries(manifest[field] ?? {})) {
const key = `${name}:${spec}`;
const entry = merged.get(key);
if (entry) {
entry.fields.push(field);
continue;
}
merged.set(key, { name, spec, fields: [field] });
}
}
return [...merged.values()].sort((left, right) =>
left.name.localeCompare(right.name),
);
}
async function analyzeManifestEntries(entries, { includeWorkspace = false } = {}) {
const exact = [];
const range = [];
for (const entry of entries) {
const workspaceReferences = includeWorkspace
? getWorkspaceReferences(entry.name)
: null;
if (!isExactVersion(entry.spec)) {
range.push({
...entry,
...(includeWorkspace ? { workspaceReferences } : {}),
});
continue;
}
const metadata = await fetchRegistryPackage(entry.name);
const installability = getInstallability(metadata, entry.name, entry.spec);
exact.push({
...entry,
...installability,
...(includeWorkspace
? {
workspaceReferences,
isInstalledInWorkspace: workspaceReferences.length > 0,
}
: {}),
suggestedExclude:
includeWorkspace && workspaceReferences.length === 0
? null
: installability.suggestedExclude,
});
}
return { exact, range };
}
function printWorkspaceReferences(title, references) {
printSectionHeader(title);
if (references.length === 0) {
console.log("- none");
return;
}
for (const reference of references) {
console.log(`- ${formatWorkspaceReference(reference)}`);
}
}
function printRootPnpmControls(rootPnpm) {
printSectionHeader("Root pnpm controls:");
if (
rootPnpm.overrideMatches.length === 0 &&
rootPnpm.patchedDependencyMatches.length === 0
) {
console.log("- none");
return;
}
for (const match of rootPnpm.overrideMatches) {
console.log(`- override ${match.selector}: ${match.value}`);
}
for (const match of rootPnpm.patchedDependencyMatches) {
console.log(`- patched dependency ${match.selector}: ${match.value}`);
}
}
function printVersionEntries(title, entries, { includeWorkspace = false } = {}) {
printSectionHeader(title);
if (entries.length === 0) {
console.log("- none");
return;
}
for (const entry of entries) {
const status =
entry.isInstallableWithoutNewExclude == null
? "unknown"
: entry.isInstallableWithoutNewExclude
? "installable now"
: "needs exclude";
console.log(
`- ${entry.name}@${entry.version} (${status}; via ${entry.fields.join(", ")})`,
);
if (entry.publishedAt) {
console.log(` published at: ${entry.publishedAt}`);
}
if (includeWorkspace) {
console.log(
` installed in workspace: ${entry.isInstalledInWorkspace ? "yes" : "no"}`,
);
for (const reference of entry.workspaceReferences) {
console.log(` workspace reference: ${formatWorkspaceReference(reference)}`);
}
}
if (entry.matchingExcludeEntries.length > 0) {
console.log(
` matching exclude entries: ${entry.matchingExcludeEntries.join(", ")}`,
);
}
if (entry.suggestedExclude) {
console.log(` suggested exclude: ${entry.suggestedExclude}`);
}
}
}
function printRangeEntries(title, entries) {
printSectionHeader(title);
if (entries.length === 0) {
console.log("- none");
return;
}
for (const entry of entries) {
console.log(
`- ${entry.name}: ${entry.spec} (manual review; via ${entry.fields.join(", ")})`,
);
for (const reference of entry.workspaceReferences ?? []) {
console.log(` workspace reference: ${formatWorkspaceReference(reference)}`);
}
}
}
const packageMetadata = await fetchRegistryPackage(packageName);
const latestVersion = packageMetadata["dist-tags"]?.latest ?? null;
const targetVersion = requestedTargetVersion ?? latestVersion;
if (!targetVersion) {
console.error(`Could not resolve a target version for ${packageName}.`);
process.exit(1);
}
if (!packageMetadata.versions?.[targetVersion]) {
console.error(`Version ${targetVersion} was not found for ${packageName}.`);
process.exit(1);
}
const packageWorkspaceReferences = getWorkspaceReferences(packageName);
const rootPnpm = getRootPnpmControls(repoRoot, packageName);
const latestInstallableWithoutNewExclude = selectLatestInstallableVersion(
packageMetadata,
packageName,
);
const targetInstallability = getInstallability(
packageMetadata,
packageName,
targetVersion,
);
const targetManifest = packageMetadata.versions[targetVersion];
const dependencyCompanions = await analyzeManifestEntries(
collectManifestEntries(targetManifest, [
"dependencies",
"optionalDependencies",
]),
);
const peerDependencies = await analyzeManifestEntries(
collectManifestEntries(targetManifest, ["peerDependencies"]),
{ includeWorkspace: true },
);
const result = {
packageName,
targetVersion,
targetWasExplicitlyProvided: requestedTargetVersion != null,
packageWorkspaceReferences,
rootPnpm,
minimumReleaseAgeMinutes,
thresholdIso: new Date(thresholdMs).toISOString(),
latestRegistryVersion: latestVersion,
latestRegistryPublishedAt:
latestVersion != null ? packageMetadata.time?.[latestVersion] ?? null : null,
latestInstallableWithoutNewExclude,
targetPublishedAt: targetInstallability.publishedAt,
targetIsYoungerThanMinimumReleaseAge:
targetInstallability.isYoungerThanMinimumReleaseAge,
targetIsInstallableWithoutNewExclude:
targetInstallability.isInstallableWithoutNewExclude,
matchingPackageExcludeEntries: targetInstallability.matchingExcludeEntries,
suggestedPackageExclude: targetInstallability.suggestedExclude,
exactDependencyCompanions: dependencyCompanions.exact,
rangeDependencyCompanions: dependencyCompanions.range,
exactPeerDependencies: peerDependencies.exact,
rangePeerDependencies: peerDependencies.range,
};
if (asJson) {
console.log(JSON.stringify(result, null, 2));
process.exit(0);
}
console.log(`Package: ${packageName}`);
console.log(
`Target version: ${targetVersion}${
requestedTargetVersion
? ""
: " (resolved latest; still ask before bumping if version was omitted)"
}`,
);
printWorkspaceReferences(
"Target package workspace references:",
packageWorkspaceReferences,
);
printRootPnpmControls(rootPnpm);
printSectionHeader("Release-age window:");
console.log(`minimumReleaseAge: ${minimumReleaseAgeMinutes} minutes`);
console.log(`Threshold: ${result.thresholdIso}`);
console.log(`Latest registry version: ${result.latestRegistryVersion ?? "unknown"}`);
if (result.latestRegistryPublishedAt) {
console.log(`Latest registry published at: ${result.latestRegistryPublishedAt}`);
}
if (latestInstallableWithoutNewExclude) {
console.log(
`Latest installable without new exclude: ${latestInstallableWithoutNewExclude.version} (${latestInstallableWithoutNewExclude.publishedAt})`,
);
} else {
console.log("Latest installable without new exclude: none found");
}
console.log(`Target published at: ${result.targetPublishedAt ?? "unknown"}`);
console.log(
`Target installable without new exclude: ${
result.targetIsInstallableWithoutNewExclude == null
? "unknown"
: result.targetIsInstallableWithoutNewExclude
? "yes"
: "no"
}`,
);
if (result.matchingPackageExcludeEntries.length > 0) {
console.log("Matching package exclude entries:");
for (const entry of result.matchingPackageExcludeEntries) {
console.log(`- ${entry}`);
}
} else {
console.log("Matching package exclude entries: none");
}
if (result.suggestedPackageExclude) {
console.log(`Suggested package exclude: ${result.suggestedPackageExclude}`);
}
printVersionEntries(
"Exact dependency companions (dependencies + optionalDependencies):",
result.exactDependencyCompanions,
);
printRangeEntries(
"Range dependency companions (dependencies + optionalDependencies):",
result.rangeDependencyCompanions,
);
printVersionEntries("Exact peer dependencies:", result.exactPeerDependencies, {
includeWorkspace: true,
});
printRangeEntries("Range peer dependencies:", result.rangePeerDependencies);
@@ -0,0 +1,244 @@
import { existsSync, readFileSync, readdirSync } from "node:fs";
import { join, relative } from "node:path";
const packageFields = [
"dependencies",
"devDependencies",
"peerDependencies",
"optionalDependencies",
];
export function formatWorkspaceReference(reference) {
const label = reference.workspaceName
? `${reference.path} (${reference.workspaceName})`
: reference.path;
const specs = reference.matches
.map((match) => `${match.field}: ${match.spec}`)
.join(", ");
return `${label} -> ${specs}`;
}
export function readJson(path) {
return JSON.parse(readFileSync(path, "utf8"));
}
function stripInlineComment(line) {
let quote = null;
let escaped = false;
for (let index = 0; index < line.length; index += 1) {
const char = line[index];
if (escaped) {
escaped = false;
continue;
}
if (quote) {
if (char === "\\") {
escaped = true;
continue;
}
if (char === quote) {
quote = null;
}
continue;
}
if (char === "'" || char === '"') {
quote = char;
continue;
}
if (char === "#") {
return line.slice(0, index).trimEnd();
}
}
return line;
}
function unquoteYamlScalar(value) {
return value.trim().replace(/^['"]|['"]$/g, "");
}
export function readWorkspaceConfig(path) {
const raw = readFileSync(path, "utf8");
const lines = raw.split(/\r?\n/);
let minimumReleaseAge = 0;
const minimumReleaseAgeExclude = [];
const overrides = {};
const patchedDependencies = {};
let activeBlock = null;
for (const line of lines) {
const uncommented = stripInlineComment(line);
const trimmed = uncommented.trim();
if (!trimmed || trimmed.startsWith("#")) continue;
const ageMatch = trimmed.match(/^minimumReleaseAge:\s*(\d+)\s*$/);
if (ageMatch) {
minimumReleaseAge = Number(ageMatch[1]);
activeBlock = null;
continue;
}
if (/^minimumReleaseAgeExclude:\s*$/.test(trimmed)) {
activeBlock = "minimumReleaseAgeExclude";
continue;
}
if (/^overrides:\s*$/.test(trimmed)) {
activeBlock = "overrides";
continue;
}
if (/^patchedDependencies:\s*$/.test(trimmed)) {
activeBlock = "patchedDependencies";
continue;
}
if (/^\S/.test(uncommented)) {
activeBlock = null;
continue;
}
if (activeBlock === "minimumReleaseAgeExclude") {
const excludeMatch = uncommented.match(/^\s*-\s+(.+?)\s*$/);
if (excludeMatch) {
minimumReleaseAgeExclude.push(unquoteYamlScalar(excludeMatch[1]));
}
continue;
}
if (activeBlock === "overrides" || activeBlock === "patchedDependencies") {
const entryMatch = uncommented.match(/^\s+(.+?):\s+(.+?)\s*$/);
if (!entryMatch) continue;
const selector = unquoteYamlScalar(entryMatch[1]);
const value = unquoteYamlScalar(entryMatch[2]);
if (activeBlock === "overrides") {
overrides[selector] = value;
} else {
patchedDependencies[selector] = value;
}
}
}
return {
minimumReleaseAge,
minimumReleaseAgeExclude,
overrides,
patchedDependencies,
};
}
export function collectPackageJsonPaths(repoRoot) {
const paths = [
"package.json",
"web/package.json",
"worker/package.json",
"ee/package.json",
];
const packagesRoot = join(repoRoot, "packages");
if (!existsSync(packagesRoot)) return paths;
const stack = [packagesRoot];
while (stack.length > 0) {
const current = stack.pop();
for (const entry of readdirSync(current, { withFileTypes: true })) {
if (
entry.name === "node_modules" ||
entry.name === "dist" ||
entry.name === ".git"
) {
continue;
}
const nextPath = join(current, entry.name);
if (entry.isDirectory()) {
stack.push(nextPath);
continue;
}
if (entry.isFile() && entry.name === "package.json") {
paths.push(relative(repoRoot, nextPath));
}
}
}
return [...new Set(paths)];
}
export function matchesPackageSelector(selector, wantedPackage) {
if (selector === wantedPackage) return true;
if (selector.startsWith(`${wantedPackage}@`)) return true;
if (selector.endsWith(`>${wantedPackage}`)) return true;
if (selector.includes(`>${wantedPackage}@`)) return true;
if (selector.endsWith("/*")) {
const prefix = selector.slice(0, -1);
return wantedPackage.startsWith(prefix);
}
return false;
}
export function entryCoversVersion(entry, wantedPackage, wantedVersion) {
if (entry === wantedPackage) return true;
if (entry.endsWith("/*")) {
const prefix = entry.slice(0, -1);
return wantedPackage.startsWith(prefix);
}
if (!entry.startsWith(`${wantedPackage}@`)) return false;
return entry
.slice(wantedPackage.length + 1)
.split("||")
.map((part) => part.trim())
.includes(wantedVersion);
}
export function findLocalPackageReferences(repoRoot, wantedPackage) {
const results = [];
for (const packageJsonPath of collectPackageJsonPaths(repoRoot)) {
const json = readJson(join(repoRoot, packageJsonPath));
const matches = [];
for (const field of packageFields) {
if (json[field]?.[wantedPackage]) {
matches.push({ field, spec: json[field][wantedPackage] });
}
}
if (matches.length > 0) {
results.push({
path: packageJsonPath,
workspaceName: json.name ?? null,
matches,
});
}
}
return results;
}
export function getRootPnpmControls(repoRoot, packageName) {
const workspaceConfig = readWorkspaceConfig(
join(repoRoot, "pnpm-workspace.yaml"),
);
return {
overrideMatches: Object.entries(workspaceConfig.overrides)
.filter(([selector]) => matchesPackageSelector(selector, packageName))
.map(([selector, value]) => ({ selector, value })),
patchedDependencyMatches: Object.entries(
workspaceConfig.patchedDependencies,
)
.filter(([selector]) => matchesPackageSelector(selector, packageName))
.map(([selector, value]) => ({ selector, value })),
};
}
+101
View File
@@ -0,0 +1,101 @@
---
name: security-review
description: Security review patterns for Langfuse. Use during code review, design, or planning whenever a change accepts user-supplied URLs, host/endpoint/baseURL fields, secrets, cross-tenant data, new outbound HTTP requests, new integrations (webhooks, blob storage, LLM connections, image proxies), redirect-following behavior, or new auth/permission scopes. Covers SSRF/outbound URL validation today and is intentionally extensible to other recurring security findings (tenant isolation, secret handling, redirect mishandling, file upload, RBAC scope drift).
---
# Security Review
Use this skill when reviewing or planning code that touches a security-sensitive
surface in Langfuse. It collects the recurring findings the team has seen in
external security reports so that future agents catch them at design and review
time rather than after the fact.
## When to Apply
Apply this skill when the change touches any of:
- a user-supplied URL, host, endpoint, `baseURL`, or webhook target
- a new outbound HTTP request (`fetch`, `axios`, AWS SDK client init with a
custom `endpoint`, OpenAI/Anthropic/Bedrock client init with a custom
`baseURL`, etc.)
- a new integration form under Settings -> Integrations or any
admin-configurable network destination
- a new tRPC procedure or public API route that mutates project-scoped data or
changes who can access it
- secrets, API keys, signing secrets, or encryption-at-rest fields
- redirect-following or cross-origin header handling
- file uploads, image proxies, or other binary data flowing in or out
Apply this skill during **plan mode** when designing a new integration so the
correct validation surfaces land in the plan, not in a follow-up CVE.
## How to Read This Skill
1. Open [references/checklist.md](references/checklist.md) and run the mental
sweep against the change.
2. For each bullet that fires, open the matching topic reference.
| Topic | Open when | File |
| --- | --- | --- |
| SSRF and outbound URL validation | The change accepts or fetches a user-supplied URL, host, or endpoint | [references/outbound-url-validation.md](references/outbound-url-validation.md) |
The catalog is intentionally short today. New topic files are added as new
finding classes recur (see "Extending This Skill").
## Output Expectations (Review Mode)
When this skill is used during code review:
- List findings first, ordered by severity, with file and line references.
- For each finding, name the canonical helper or known-good call site the
author should copy.
- For SSRF-class findings, point at [references/outbound-url-validation.md](references/outbound-url-validation.md)
rather than re-deriving the fix.
- Call out missing **negative tests** (private-IP, cross-tenant, missing-scope)
as findings, not as nice-to-haves.
## Output Expectations (Design / Plan Mode)
When this skill is used while planning:
- Restate which surfaces the new feature exposes (forms, public API routes,
worker entrypoints).
- For each surface that matches a checklist trigger, name the validator or
helper that must be invoked and at which layer (save-time, use-time,
connection-time, redirect-time).
- Treat "we will validate later" as a design defect: validation belongs in the
same change that introduces the surface.
## Extending This Skill
Add a new `references/<topic>.md` whenever a security finding recurs across
features or PR reviews. Keep each reference narrow and concrete:
1. Threat in plain language (one paragraph).
2. Canonical helpers in this repo, with paths.
3. Known-good call sites that can be copied.
4. Required defenses (save-time, use-time, transport-time, etc.).
5. Anti-patterns to flag in review.
Then add a one-line trigger to [references/checklist.md](references/checklist.md)
pointing at the new topic file, and add a row to the table above.
Candidates for future references (do not add until a real finding recurs):
- Tenant isolation (`projectId` filters across Prisma and ClickHouse)
- Secret handling and encryption-at-rest read paths
- Redirect mishandling and sensitive-header propagation
- File upload validation and content-type sniffing
- RBAC scope drift on new tRPC/public API endpoints
- Signed URL scoping (expiry, path, method)
- Public API rate limiting and auth boundary checks
## Integration With Other Skills
- The shared `code-review` skill should defer here for any change that matches
the triggers above; see [code-review/SKILL.md](../code-review/SKILL.md).
- The shared `backend-dev-guidelines` skill should defer here when adding
outbound HTTP, integration config, or URL-accepting procedures; see
[backend-dev-guidelines/SKILL.md](../backend-dev-guidelines/SKILL.md).
- Confirmed issues with reproduction evidence go through `linear-bug-triage`
for Linear handoff.
@@ -0,0 +1,74 @@
# Security Review Checklist
Run this list mentally on every relevant change. For each bullet, either find
that it does not apply, or confirm the listed mitigation is present in the
diff. Each bullet links to the topic reference that owns the detail.
## User-Supplied URLs and Outbound Requests
- Does the change accept a URL, host, `endpoint`, `baseURL`, webhook target,
or any field that becomes the destination of an outbound HTTP request?
- If yes, see [outbound-url-validation.md](outbound-url-validation.md).
- Required: a save-time validator on the mutation/API route **and** use-time
or connection-time validation when the request is issued, including
redirects. Raw `fetch(userUrl)` or SDK init with `endpoint: userUrl`
without that wiring is a finding.
- Does the change add a new "integration" (Settings -> Integrations, new
webhook destination, new storage backend, new LLM provider, new image
proxy) that lets an admin configure a host?
- If yes, see [outbound-url-validation.md](outbound-url-validation.md).
- Required: new env-driven allowlist trio (host / IPs / IP segments) for
self-hosted, plus strict Cloud enforcement.
- Does the change follow redirects on a user-controlled URL with plain
`fetch()`?
- If yes, see [outbound-url-validation.md](outbound-url-validation.md)
(Redirect Handling). Required: `fetchWithSecureRedirects` with the
matching validator.
## Tenant Isolation
- Does every new Prisma query on a project-scoped table include `projectId`
in the `where` clause?
- Does every new ClickHouse query on a project-scoped table include
`project_id = {projectId: String}`?
- Does every new tRPC procedure use `protectedProjectProcedure` (or
equivalent) and call `throwIfNoProjectAccess` with the right scope?
- Does every new public API route go through `withMiddlewares` +
`createAuthedProjectAPIRoute` (or the equivalent organization variant) with
the right scope?
These are enforced today by the repo-wide review checklist in
[../../code-review/references/review-checklist.md](../../code-review/references/review-checklist.md);
restate them here so the security sweep stays self-contained.
## Secrets and Credentials
- Are new secrets stored encrypted at rest (e.g. via `encrypt` / `decrypt`
from `@langfuse/shared/encryption`) and never returned through `get`/list
endpoints?
- Are secrets omitted from API responses (`select` / `omit` in Prisma) and
from logs?
- Are new env vars added to `.env*.example` files and validated via the
package `env.mjs/ts`, not read from `process.env` directly?
## Audit Logging
- Do sensitive mutations (integration config changes, role or scope changes,
exports, deletions) emit `auditLog` with the right `action` and
`resourceType`? Match existing wiring in
`web/src/features/blobstorage-integration/blobstorage-integration-router.ts`
and similar routers.
## Negative Tests
- Does the change include tests that prove **blocked** inputs are rejected
(private IPs, cross-tenant IDs, missing scope, http: on Cloud)? Missing
negative coverage on a security-sensitive surface is a finding.
## Extending
When a new finding class starts to recur in PR reviews or security reports,
add a one-line bullet here pointing at a new `references/<topic>.md`. Do not
restate the detail in this checklist; keep it as the trigger surface.
@@ -0,0 +1,165 @@
# Outbound URL Validation (SSRF)
## Threat
Any Langfuse code path that issues an outbound HTTP request to a URL derived
from user input (mutation form, public API field, integration config, image
proxy) can be coerced into Server-Side Request Forgery. The high-value
internal targets in Langfuse's deployment topology include:
- Cloud instance metadata services (`169.254.169.254`, IMDSv2 endpoints)
- Internal Postgres, ClickHouse, Redis, S3/MinIO, queue admin UIs
- Loopback admin interfaces (`127.0.0.1`, `localhost`, Docker API on
`2375/2376`)
- Kubernetes API server and other in-cluster control planes
- Any RFC1918 / RFC6598 / IPv6 ULA range routable from the pod or container
Even when the surface requires `integrations:CRUD` or admin scope, the
attacker model assumes the credentialed user is malicious or compromised;
SSRF lets them pivot from app-level admin to network-level access that the
deployment topology otherwise denies.
## Canonical Helpers
All under `packages/shared/src/server/outbound-url/`:
- [`parseOutboundUrl(urlString)`](../../../../packages/shared/src/server/outbound-url/validation.ts)
— safe parse. Rejects embedded credentials, invalid encoding, and bad URL
syntax. **Use this instead of `new URL(...)` for any user-supplied URL.**
- [`validateOutboundUrlHost({ url, whitelist, logContext, shouldSkipDnsCheckForLiteralIps })`](../../../../packages/shared/src/server/outbound-url/validation.ts)
— checks hostname blocklist, IP literal blocklist, and forward DNS
resolution against blocked CIDRs (defends DNS rebinding by resolving every
A/AAAA plus the local `getaddrinfo` view).
- [`addSecureOutboundConnectionValidation(options, ...)`](../../../../packages/shared/src/server/outbound-url/connection.ts)
— attaches connect-time IP validation to a `fetch` request so the TCP peer
is re-validated after DNS resolution, not just at save time.
- [`fetchWithSecureRedirects(...)`](../../../../packages/shared/src/server/outbound-url/fetch.ts)
— manual redirect handling. Validates each `Location` hop with the
caller-supplied validator and strips sensitive headers (`Authorization`,
`Cookie`, signing headers) on cross-origin redirects.
Surface-specific wrappers (reuse these rather than rolling your own):
- LLM base URL:
[`validateLlmConnectionBaseURL`](../../../../packages/shared/src/server/llm/baseUrlValidation.ts)
- Webhook URL:
[`validateWebhookURL`](../../../../packages/shared/src/server/webhooks/validation.ts)
- Blob storage endpoint:
[`validateBlobStorageEndpoint`](../../../../packages/shared/src/server/services/blobStorageEndpointValidation.ts)
and the companion
[`blobStorageEndpointConnectionValidationOptions`](../../../../packages/shared/src/server/services/blobStorageEndpointValidation.ts)
for connect-time enforcement through `StorageServiceFactory`.
## Required Defenses
Every outbound-URL surface MUST apply **all three** of:
1. **Save-time validation** in the mutation, tRPC procedure, or public API
route that persists the URL. Reject the write if validation fails.
2. **Use-time / connection-time validation** when the request is actually
issued (worker job, lazy validation endpoint, processor). DNS can change
between save and use; the SDK that ultimately makes the call may resolve a
different IP than the save-time check did. Plumb
`addSecureOutboundConnectionValidation` (or the SDK's equivalent hook)
through the request.
3. **Redirect-time validation** if the request can be redirected. Plain
`fetch()` defaults to `redirect: 'follow'` and will silently chase a
redirect into the loopback range. Use `fetchWithSecureRedirects` with the
matching validator instead.
## Known-Good Call Sites (Copy These)
- LLM base URL save:
`web/src/features/llm-api-key/server/router.ts` (`update` mutation calls
`validateLlmConnectionBaseURL` before persisting).
- LLM base URL through public API:
`web/src/pages/api/public/llm-connections/index.ts`.
- Webhook URL save + use:
`packages/shared/src/server/webhooks/validation.ts` is wired into both the
automation form and the worker-side webhook sender.
- Blob storage endpoint:
`web/src/features/blobstorage-integration/blobstorage-integration-router.ts`
(`validate` mutation calls `validateBlobStorageEndpoint`); connection-time
enforcement flows through `StorageServiceFactory.getInstance({
connectionValidation: blobStorageEndpointConnectionValidationOptions() })`.
## When Adding a New Outbound URL Surface
1. Identify the user-input layers: form mutation, public API route, env import,
anywhere the URL can be supplied by a tenant.
2. Decide whether an existing wrapper fits. If yes, reuse it. If not, add a
new wrapper under `packages/shared/src/server/...` that delegates to
`validateOutboundUrlHost` so blocklist behavior, DNS rebinding handling,
and credential checks stay centralized.
3. Define the env allowlist trio for self-hosted users who legitimately point
at private network targets (mirroring
`LANGFUSE_WEBHOOK_WHITELISTED_HOST/IPS/IP_SEGMENTS`,
`LANGFUSE_LLM_CONNECTION_WHITELISTED_HOST/IPS/IP_SEGMENTS`, and
`LANGFUSE_BLOB_STORAGE_ENDPOINT_WHITELISTED_HOST/IPS/IP_SEGMENTS`). Do
not share another surface's allowlist; each surface keeps its own.
4. Add the env vars to `.env*.example` and the package `env.mjs/ts`.
5. Call the wrapper from:
- every tRPC mutation that writes the URL,
- every public API route that accepts the URL,
- the worker/processor that issues the request (use connect-time
validation if the underlying SDK does not expose a host-validation hook).
6. If the request can redirect, use `fetchWithSecureRedirects` with the same
wrapper as the validator.
7. Add server-side tests that prove blocked targets fail validation:
`127.0.0.1`, `169.254.169.254`, an RFC1918 literal, an RFC1918 hostname
(DNS rebinding), `http://` on Cloud, and a URL containing
`user:pass@host`.
## Anti-Patterns to Flag in Review
- `fetch(<user-supplied-url>)` (or `axios`, `got`, etc.) without an upstream
call to a `validate*URL` helper, or without
`addSecureOutboundConnectionValidation` on the request options.
- A tRPC mutation that persists a `host` / `endpoint` / `baseURL` / `webhookUrl`
field without invoking the matching validator before the write.
- `StorageServiceFactory.getInstance({ endpoint })` (or any SDK client init
that takes a user-controlled URL) without `connectionValidation` plumbed
through.
- Custom URL parsing via `new URL(userInput)` instead of
`parseOutboundUrl(userInput)`. The latter rejects embedded credentials and
bad encoding, both of which are recurring SSRF/credential-leak vectors.
- Following redirects with `fetch(url)` (default `redirect: 'follow'`) on a
user-controlled URL. Switch to `fetchWithSecureRedirects`.
- A new integration UI that validates only on the client side. Save-time
validation must run server-side.
- Save-time validation present, use-time validation missing (or vice versa).
Both layers are required; the worker may issue the request hours after the
save and DNS will have moved.
## Env Allowlist Behavior
- Cloud (`NEXT_PUBLIC_LANGFUSE_CLOUD_REGION` set) forces strict mode.
Allowlist env vars are ignored on Cloud, and HTTPS is enforced for surfaces
that require it (LLM base URL, blob storage endpoint).
- Self-hosted reads the per-surface env trio:
- `LANGFUSE_LLM_CONNECTION_WHITELISTED_HOST/IPS/IP_SEGMENTS`
- `LANGFUSE_WEBHOOK_WHITELISTED_HOST/IPS/IP_SEGMENTS`
- `LANGFUSE_BLOB_STORAGE_ENDPOINT_WHITELISTED_HOST/IPS/IP_SEGMENTS`
- Blob storage endpoint validation is **opt-in** today on self-hosted (the
helper is a no-op until the operator configures one of the allowlist env
vars). There is a `TODO(next major)` in
`blobStorageEndpointValidation.ts` to flip the default; until then, do not
rely on blob storage validation for new surfaces — wire your own wrapper
that defaults to strict.
## Negative Tests (Required)
A change that adds a new outbound URL surface MUST include server-side tests
that assert each of the following fails validation:
- Loopback literal (`http://127.0.0.1`, `http://[::1]`)
- Cloud metadata literal (`http://169.254.169.254`)
- RFC1918 literal (`http://10.0.0.1`)
- Hostname that resolves to a private IP (DNS rebinding sanity check)
- URL with embedded credentials (`http://user:pass@host`)
- `http://` on Cloud (where HTTPS is required)
- An empty allowlist permits no internal targets on self-hosted
Pattern reference:
`worker/src/__tests__/llm-base-url-validation.test.ts` and the test files
adjacent to the wrappers above.
+456
View File
@@ -0,0 +1,456 @@
---
name: skill-creator
description: Guide for creating effective skills. This skill should be used when users want to create a new skill (or update an existing skill) that extends Codex's capabilities with specialized knowledge, workflows, or tool integrations.
metadata:
short-description: Create or update Codex skills
---
# Skill Creator
This skill provides guidance for creating effective skills.
## About Skills
Skills are modular, self-contained folders that extend Codex's capabilities by providing
specialized knowledge, workflows, and tools. Think of them as "onboarding guides" for specific
domains or tasks—they transform Codex from a general-purpose agent into a specialized agent
equipped with procedural knowledge that no model can fully possess.
### What Skills Provide
1. Specialized workflows - Multi-step procedures for specific domains
2. Tool integrations - Instructions for working with specific file formats or APIs
3. Domain expertise - Company-specific knowledge, schemas, business logic
4. Bundled resources - Scripts, references, and assets for complex and repetitive tasks
## Core Principles
### Concise is Key
The context window is a public good. Skills share the context window with everything else Codex needs: system prompt, conversation history, other Skills' metadata, and the actual user request.
**Default assumption: Codex is already very smart.** Only add context Codex doesn't already have. Challenge each piece of information: "Does Codex really need this explanation?" and "Does this paragraph justify its token cost?"
Prefer concise examples over verbose explanations.
### Set Appropriate Degrees of Freedom
Match the level of specificity to the task's fragility and variability:
**High freedom (text-based instructions)**: Use when multiple approaches are valid, decisions depend on context, or heuristics guide the approach.
**Medium freedom (pseudocode or scripts with parameters)**: Use when a preferred pattern exists, some variation is acceptable, or configuration affects behavior.
**Low freedom (specific scripts, few parameters)**: Use when operations are fragile and error-prone, consistency is critical, or a specific sequence must be followed.
Think of Codex as exploring a path: a narrow bridge with cliffs needs specific guardrails (low freedom), while an open field allows many routes (high freedom).
### Require Human Review for Ticket Writes
For skills that create Linear tickets, update Linear tickets, or add evidence to
existing tickets, require human review before any write. The skill must present
all findings in a table, ask the human which findings to create or update in
Linear, and wait for an explicit selection before making changes.
Use this table structure unless the domain needs additional columns:
| ID | Finding | Evidence | Impact / Scope | Existing Ticket Match | Proposed Linear Action | Confidence | Human Decision |
| --- | --- | --- | --- | --- | --- | --- | --- |
| F1 | Concise symptom or bug claim | Measured counts, deltas, links, traces, logs, or "No measurements found" | Affected env, service, route, customer segment, or blast radius supported by evidence | Existing issue key/link, duplicate candidate, or "None found" | Create new ticket, add evidence comment, update status/labels, or no action | High/medium/low plus one short reason | Leave blank for the human to choose |
In the skill instructions, state that Codex must not create tickets, comment on
tickets, edit ticket fields, or add evidence until the human chooses one or more
row IDs and actions. If the human asks for an automated sweep, still pause at
this review table before writing to Linear.
### Protect Validation Integrity
You may use subagents during iteration to validate whether a skill works on realistic tasks or whether a suspected problem is real. This is most useful when you want an independent pass on the skill's behavior, outputs, or failure modes after a revision. Only do this when it is possible to start new subagents.
When using subagents for validation, treat that as an evaluation surface. The goal is to learn whether the skill generalizes, not whether another agent can reconstruct the answer from leaked context.
Prefer raw artifacts such as example prompts, outputs, diffs, logs, or traces. Give the minimum task-local context needed to perform the validation. Avoid passing the intended answer, suspected bug, intended fix, or your prior conclusions unless the validation explicitly requires them.
### Require Valid Markdown Output
When a skill instructs Codex to return Markdown, require the final output to be
valid Markdown, not just Markdown-like text.
In the skill instructions, explicitly require Codex to:
- use valid Markdown syntax for headings, lists, links, tables, and code fences;
- include a space after list markers such as `-`, `*`, and `1.`;
- close every Markdown link and parenthesis correctly;
- avoid malformed tables, dangling backticks, or partially opened fenced code
blocks;
- prefer plain paragraphs over complex formatting when the structure would be
fragile.
If a skill produces structured reports, include a short output-format section
that says the response must be valid Markdown and should be checked for basic
syntax mistakes before returning it.
### Anatomy of a Skill
Every skill consists of a required SKILL.md file and optional bundled resources:
```
skill-name/
├── SKILL.md (required)
│ ├── YAML frontmatter metadata (required)
│ │ ├── name: (required)
│ │ └── description: (required)
│ └── Markdown instructions (required)
├── agents/ (recommended)
│ └── openai.yaml - UI metadata for skill lists and chips
└── Bundled Resources (optional)
├── scripts/ - Executable code (Python/Bash/etc.)
├── references/ - Documentation intended to be loaded into context as needed
└── assets/ - Files used in output (templates, icons, fonts, etc.)
```
#### SKILL.md (required)
Every SKILL.md consists of:
- **Frontmatter** (YAML): Contains `name` and `description` fields. These are the only fields that Codex reads to determine when the skill gets used, thus it is very important to be clear and comprehensive in describing what the skill is, and when it should be used.
- **Body** (Markdown): Instructions and guidance for using the skill. Only loaded AFTER the skill triggers (if at all).
#### Agents metadata (recommended)
- UI-facing metadata for skill lists and chips
- Read references/openai_yaml.md before generating values and follow its descriptions and constraints
- Create: human-facing `display_name`, `short_description`, and `default_prompt` by reading the skill
- Generate deterministically by passing the values as `--interface key=value` to `scripts/generate_openai_yaml.py` or `scripts/init_skill.py`
- On updates: validate `agents/openai.yaml` still matches SKILL.md; regenerate if stale
- Only include other optional interface fields (icons, brand color) if explicitly provided
- See references/openai_yaml.md for field definitions and examples
#### Bundled Resources (optional)
##### Scripts (`scripts/`)
Executable code (Python/Bash/etc.) for tasks that require deterministic reliability or are repeatedly rewritten.
- **When to include**: When the same code is being rewritten repeatedly or deterministic reliability is needed
- **Example**: `scripts/rotate_pdf.py` for PDF rotation tasks
- **Benefits**: Token efficient, deterministic, may be executed without loading into context
- **Note**: Scripts may still need to be read by Codex for patching or environment-specific adjustments
##### References (`references/`)
Documentation and reference material intended to be loaded as needed into context to inform Codex's process and thinking.
- **When to include**: For documentation that Codex should reference while working
- **Examples**: `references/finance.md` for financial schemas, `references/mnda.md` for company NDA template, `references/policies.md` for company policies, `references/api_docs.md` for API specifications
- **Use cases**: Database schemas, API documentation, domain knowledge, company policies, detailed workflow guides
- **Benefits**: Keeps SKILL.md lean, loaded only when Codex determines it's needed
- **Best practice**: If files are large (>10k words), include grep search patterns in SKILL.md
- **Avoid duplication**: Information should live in either SKILL.md or references files, not both. Prefer references files for detailed information unless it's truly core to the skill—this keeps SKILL.md lean while making information discoverable without hogging the context window. Keep only essential procedural instructions and workflow guidance in SKILL.md; move detailed reference material, schemas, and examples to references files.
##### Assets (`assets/`)
Files not intended to be loaded into context, but rather used within the output Codex produces.
- **When to include**: When the skill needs files that will be used in the final output
- **Examples**: `assets/logo.png` for brand assets, `assets/slides.pptx` for PowerPoint templates, `assets/frontend-template/` for HTML/React boilerplate, `assets/font.ttf` for typography
- **Use cases**: Templates, images, icons, boilerplate code, fonts, sample documents that get copied or modified
- **Benefits**: Separates output resources from documentation, enables Codex to use files without loading them into context
#### What to Not Include in a Skill
A skill should only contain essential files that directly support its functionality. Do NOT create extraneous documentation or auxiliary files, including:
- README.md
- INSTALLATION_GUIDE.md
- QUICK_REFERENCE.md
- CHANGELOG.md
- etc.
The skill should only contain the information needed for an AI agent to do the job at hand. It should not contain auxiliary context about the process that went into creating it, setup and testing procedures, user-facing documentation, etc. Creating additional documentation files just adds clutter and confusion.
### Progressive Disclosure Design Principle
Skills use a three-level loading system to manage context efficiently:
1. **Metadata (name + description)** - Always in context (~100 words)
2. **SKILL.md body** - When skill triggers (<5k words)
3. **Bundled resources** - As needed by Codex (Unlimited because scripts can be executed without reading into context window)
#### Progressive Disclosure Patterns
Keep SKILL.md body to the essentials and under 500 lines to minimize context bloat. Split content into separate files when approaching this limit. When splitting out content into other files, it is very important to reference them from SKILL.md and describe clearly when to read them, to ensure the reader of the skill knows they exist and when to use them.
**Key principle:** When a skill supports multiple variations, frameworks, or options, keep only the core workflow and selection guidance in SKILL.md. Move variant-specific details (patterns, examples, configuration) into separate reference files.
**Pattern 1: High-level guide with references**
```markdown
# PDF Processing
## Quick start
Extract text with pdfplumber:
[code example]
## Advanced features
- **Form filling**: See [FORMS.md](FORMS.md) for complete guide
- **API reference**: See [REFERENCE.md](REFERENCE.md) for all methods
- **Examples**: See [EXAMPLES.md](EXAMPLES.md) for common patterns
```
Codex loads FORMS.md, REFERENCE.md, or EXAMPLES.md only when needed.
**Pattern 2: Domain-specific organization**
For Skills with multiple domains, organize content by domain to avoid loading irrelevant context:
```
bigquery-skill/
├── SKILL.md (overview and navigation)
└── reference/
├── finance.md (revenue, billing metrics)
├── sales.md (opportunities, pipeline)
├── product.md (API usage, features)
└── marketing.md (campaigns, attribution)
```
When a user asks about sales metrics, Codex only reads sales.md.
Similarly, for skills supporting multiple frameworks or variants, organize by variant:
```
cloud-deploy/
├── SKILL.md (workflow + provider selection)
└── references/
├── aws.md (AWS deployment patterns)
├── gcp.md (GCP deployment patterns)
└── azure.md (Azure deployment patterns)
```
When the user chooses AWS, Codex only reads aws.md.
**Pattern 3: Conditional details**
Show basic content, link to advanced content:
```markdown
# DOCX Processing
## Creating documents
Use docx-js for new documents. See [DOCX-JS.md](DOCX-JS.md).
## Editing documents
For simple edits, modify the XML directly.
**For tracked changes**: See [REDLINING.md](REDLINING.md)
**For OOXML details**: See [OOXML.md](OOXML.md)
```
Codex reads REDLINING.md or OOXML.md only when the user needs those features.
**Important guidelines:**
- **Avoid deeply nested references** - Keep references one level deep from SKILL.md. All reference files should link directly from SKILL.md.
- **Structure longer reference files** - For files longer than 100 lines, include a table of contents at the top so Codex can see the full scope when previewing.
## Skill Creation Process
Skill creation involves these steps:
1. Understand the skill with concrete examples
2. Plan reusable skill contents (scripts, references, assets)
3. Initialize the skill (run init_skill.py)
4. Edit the skill (implement resources and write SKILL.md)
5. Validate the skill (run quick_validate.py)
6. Iterate based on real usage and forward-test complex skills.
Follow these steps in order, skipping only if there is a clear reason why they are not applicable.
### Skill Naming
- Use lowercase letters, digits, and hyphens only; normalize user-provided titles to hyphen-case (e.g., "Plan Mode" -> `plan-mode`).
- When generating names, generate a name under 64 characters (letters, digits, hyphens).
- Prefer short, verb-led phrases that describe the action.
- Namespace by tool when it improves clarity or triggering (e.g., `gh-address-comments`, `linear-address-issue`).
- Name the skill folder exactly after the skill name.
### Step 1: Understanding the Skill with Concrete Examples
Skip this step only when the skill's usage patterns are already clearly understood. It remains valuable even when working with an existing skill.
To create an effective skill, clearly understand concrete examples of how the skill will be used. This understanding can come from either direct user examples or generated examples that are validated with user feedback.
For example, when building an image-editor skill, relevant questions include:
- "What functionality should the image-editor skill support? Editing, rotating, anything else?"
- "Can you give some examples of how this skill would be used?"
- "I can imagine users asking for things like 'Remove the red-eye from this image' or 'Rotate this image'. Are there other ways you imagine this skill being used?"
- "What would a user say that should trigger this skill?"
- "Where should I create this skill? If you do not have a preference, I will place it in `$CODEX_HOME/skills` (or `~/.codex/skills` when `CODEX_HOME` is unset) so Codex can discover it automatically."
To avoid overwhelming users, avoid asking too many questions in a single message. Start with the most important questions and follow up as needed for better effectiveness.
Conclude this step when there is a clear sense of the functionality the skill should support.
### Step 2: Planning the Reusable Skill Contents
To turn concrete examples into an effective skill, analyze each example by:
1. Considering how to execute on the example from scratch
2. Identifying what scripts, references, and assets would be helpful when executing these workflows repeatedly
Example: When building a `pdf-editor` skill to handle queries like "Help me rotate this PDF," the analysis shows:
1. Rotating a PDF requires re-writing the same code each time
2. A `scripts/rotate_pdf.py` script would be helpful to store in the skill
Example: When designing a `frontend-webapp-builder` skill for queries like "Build me a todo app" or "Build me a dashboard to track my steps," the analysis shows:
1. Writing a frontend webapp requires the same boilerplate HTML/React each time
2. An `assets/hello-world/` template containing the boilerplate HTML/React project files would be helpful to store in the skill
Example: When building a `big-query` skill to handle queries like "How many users have logged in today?" the analysis shows:
1. Querying BigQuery requires re-discovering the table schemas and relationships each time
2. A `references/schema.md` file documenting the table schemas would be helpful to store in the skill
To establish the skill's contents, analyze each concrete example to create a list of the reusable resources to include: scripts, references, and assets.
### Step 3: Initializing the Skill
At this point, it is time to actually create the skill.
Skip this step only if the skill being developed already exists. In this case, continue to the next step.
Before running `init_skill.py`, ask where the user wants the skill created. If they do not specify a location, default to `$CODEX_HOME/skills`; when `CODEX_HOME` is unset, fall back to `~/.codex/skills` so the skill is auto-discovered.
When creating a new skill from scratch, always run the `init_skill.py` script. The script conveniently generates a new template skill directory that automatically includes everything a skill requires, making the skill creation process much more efficient and reliable.
Usage:
```bash
scripts/init_skill.py <skill-name> --path <output-directory> [--resources scripts,references,assets] [--examples]
```
Examples:
```bash
scripts/init_skill.py my-skill --path "${CODEX_HOME:-$HOME/.codex}/skills"
scripts/init_skill.py my-skill --path "${CODEX_HOME:-$HOME/.codex}/skills" --resources scripts,references
scripts/init_skill.py my-skill --path ~/work/skills --resources scripts --examples
```
The script:
- Creates the skill directory at the specified path
- Generates a SKILL.md template with proper frontmatter and TODO placeholders
- Creates `agents/openai.yaml` using agent-generated `display_name`, `short_description`, and `default_prompt` passed via `--interface key=value`
- Optionally creates resource directories based on `--resources`
- Optionally adds example files when `--examples` is set
After initialization, customize the SKILL.md and add resources as needed. If you used `--examples`, replace or delete placeholder files.
Generate `display_name`, `short_description`, and `default_prompt` by reading the skill, then pass them as `--interface key=value` to `init_skill.py` or regenerate with:
```bash
scripts/generate_openai_yaml.py <path/to/skill-folder> --interface key=value
```
Only include other optional interface fields when the user explicitly provides them. For full field descriptions and examples, see references/openai_yaml.md.
### Step 4: Edit the Skill
When editing the (newly-generated or existing) skill, remember that the skill is being created for another instance of Codex to use. Include information that would be beneficial and non-obvious to Codex. Consider what procedural knowledge, domain-specific details, or reusable assets would help another Codex instance execute these tasks more effectively.
After substantial revisions, or if the skill is particularly tricky, you should use subagents to forward-test the skill on realistic tasks or artifacts. When doing so, pass the artifact under validation rather than your diagnosis of what is wrong, and keep the prompt generic enough that success depends on transferable reasoning rather than hidden ground truth.
#### Start with Reusable Skill Contents
To begin implementation, start with the reusable resources identified above: `scripts/`, `references/`, and `assets/` files. Note that this step may require user input. For example, when implementing a `brand-guidelines` skill, the user may need to provide brand assets or templates to store in `assets/`, or documentation to store in `references/`.
Added scripts must be tested by actually running them to ensure there are no bugs and that the output matches what is expected. If there are many similar scripts, only a representative sample needs to be tested to ensure confidence that they all work while balancing time to completion.
If you used `--examples`, delete any placeholder files that are not needed for the skill. Only create resource directories that are actually required.
#### Update SKILL.md
**Writing Guidelines:** Always use imperative/infinitive form.
##### Frontmatter
Write the YAML frontmatter with `name` and `description`:
- `name`: The skill name
- `description`: This is the primary triggering mechanism for your skill, and helps Codex understand when to use the skill.
- Include both what the Skill does and specific triggers/contexts for when to use it.
- Include all "when to use" information here - Not in the body. The body is only loaded after triggering, so "When to Use This Skill" sections in the body are not helpful to Codex.
- Example description for a `docx` skill: "Comprehensive document creation, editing, and analysis with support for tracked changes, comments, formatting preservation, and text extraction. Use when Codex needs to work with professional documents (.docx files) for: (1) Creating new documents, (2) Modifying or editing content, (3) Working with tracked changes, (4) Adding comments, or any other document tasks"
Do not include any other fields in YAML frontmatter.
##### Body
Write instructions for using the skill and its bundled resources.
If the skill expects Markdown output, add an explicit output-format section that
requires valid Markdown syntax in the final response.
### Step 5: Validate the Skill
Once development of the skill is complete, validate the skill folder to catch basic issues early:
```bash
scripts/quick_validate.py <path/to/skill-folder>
```
The validation script checks YAML frontmatter format, required fields, and naming rules. If validation fails, fix the reported issues and run the command again.
### Step 6: Iterate
After testing the skill, you may detect the skill is complex enough that it requires forward-testing; or users may request improvements.
User testing often this happens right after using the skill, with fresh context of how the skill performed.
**Forward-testing and iteration workflow:**
1. Use the skill on real tasks
2. Notice struggles or inefficiencies
3. Identify how SKILL.md or bundled resources should be updated
4. Implement changes and test again
5. Forward-test if it is reasonable and appropriate
## Forward-testing
To forward-test, launch subagents as a way to stress test the skill with minimal context.
Subagents should *not* know that they are being asked to test the skill. They should be treated as
an agent asked to perform a task by the user. Prompts to subagents should look like:
`Use $skill-x at /path/to/skill-x to solve problem y`
Not:
`Review the skill at /path/to/skill-x; pretend a user asks you to...`
Decision rule for forward-testing:
- Err on the side of forward-testing
- Ask for approval if you think there's a risk that forward-testing would:
* take a long time,
* require additional approvals from the user, or
* modify live production systems
In these cases, show the user your proposed prompt and request (1) a yes/no decision, and
(2) any suggested modifictions.
Considerations when forward-testing:
- use fresh threads for independent passes
- pass the skill, and a request in a similar way the user would.
- pass raw artifacts, not your conclusions
- avoid showing expected answers or intended fixes
- rebuild context from source artifacts after each iteration
- review the subagent's output and reasoning and emitted artifacts
- avoid leaving artifacts the agent can find on disk between iterations;
clean up subagents' artifacts to avoid additional contamination.
If forward-testing only succeeds when subagents see leaked context, tighten the skill or the
forward-testing setup before trusting the result.
@@ -0,0 +1,6 @@
interface:
display_name: "Skill Creator"
short_description: "Create or update Codex skills"
icon_small: "./assets/skill-creator-small.svg"
icon_large: "./assets/skill-creator.png"
default_prompt: "Use $skill-creator to create or refine a concise Codex skill."
@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" viewBox="0 0 20 20">
<path fill="#0D0D0D" d="M12.03 4.113a3.612 3.612 0 0 1 5.108 5.108l-6.292 6.29c-.324.324-.56.561-.791.752l-.235.176c-.205.14-.422.261-.65.36l-.229.093a4.136 4.136 0 0 1-.586.16l-.764.134-2.394.4c-.142.024-.294.05-.423.06-.098.007-.232.01-.378-.026l-.149-.05a1.081 1.081 0 0 1-.521-.474l-.046-.093a1.104 1.104 0 0 1-.075-.527c.01-.129.035-.28.06-.422l.398-2.394c.1-.602.162-.987.295-1.35l.093-.23c.1-.228.22-.445.36-.65l.176-.235c.19-.232.428-.467.751-.79l6.292-6.292Zm-5.35 7.232c-.35.35-.534.535-.66.688l-.11.147a2.67 2.67 0 0 0-.24.433l-.062.154c-.08.22-.124.462-.232 1.112l-.398 2.394-.001.001h.003l2.393-.399.717-.126a2.63 2.63 0 0 0 .394-.105l.154-.063a2.65 2.65 0 0 0 .433-.24l.147-.11c.153-.126.339-.31.688-.66l4.988-4.988-3.227-3.226-4.987 4.988Zm9.517-6.291a2.281 2.281 0 0 0-3.225 0l-.364.362 3.226 3.227.363-.364c.89-.89.89-2.334 0-3.225ZM4.583 1.783a.3.3 0 0 1 .294.241c.117.585.347 1.092.707 1.48.357.385.859.668 1.549.783a.3.3 0 0 1 0 .592c-.69.115-1.192.398-1.549.783-.315.34-.53.77-.657 1.265l-.05.215a.3.3 0 0 1-.588 0c-.117-.585-.347-1.092-.707-1.48-.357-.384-.859-.668-1.549-.783a.3.3 0 0 1 0-.592c.69-.115 1.192-.398 1.549-.783.36-.388.59-.895.707-1.48l.015-.05a.3.3 0 0 1 .279-.19Z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

+202
View File
@@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
@@ -0,0 +1,49 @@
# openai.yaml fields (full example + descriptions)
`agents/openai.yaml` is an extended, product-specific config intended for the machine/harness to read, not the agent. Other product-specific config can also live in the `agents/` folder.
## Full example
```yaml
interface:
display_name: "Optional user-facing name"
short_description: "Optional user-facing description"
icon_small: "./assets/small-400px.png"
icon_large: "./assets/large-logo.svg"
brand_color: "#3B82F6"
default_prompt: "Optional surrounding prompt to use the skill with"
dependencies:
tools:
- type: "mcp"
value: "github"
description: "GitHub MCP server"
transport: "streamable_http"
url: "https://api.githubcopilot.com/mcp/"
policy:
allow_implicit_invocation: true
```
## Field descriptions and constraints
Top-level constraints:
- Quote all string values.
- Keep keys unquoted.
- For `interface.default_prompt`: generate a helpful, short (typically 1 sentence) example starting prompt based on the skill. It must explicitly mention the skill as `$skill-name` (e.g., "Use $skill-name-here to draft a concise weekly status update.").
- `interface.display_name`: Human-facing title shown in UI skill lists and chips.
- `interface.short_description`: Human-facing short UI blurb (2564 chars) for quick scanning.
- `interface.icon_small`: Path to a small icon asset (relative to skill dir). Default to `./assets/` and place icons in the skill's `assets/` folder.
- `interface.icon_large`: Path to a larger logo asset (relative to skill dir). Default to `./assets/` and place icons in the skill's `assets/` folder.
- `interface.brand_color`: Hex color used for UI accents (e.g., badges).
- `interface.default_prompt`: Default prompt snippet inserted when invoking the skill.
- `dependencies.tools[].type`: Dependency category. Only `mcp` is supported for now.
- `dependencies.tools[].value`: Identifier of the tool or dependency.
- `dependencies.tools[].description`: Human-readable explanation of the dependency.
- `dependencies.tools[].transport`: Connection type when `type` is `mcp`.
- `dependencies.tools[].url`: MCP server URL when `type` is `mcp`.
- `policy.allow_implicit_invocation`: When false, the skill is not injected into
the model context by default, but can still be invoked explicitly via `$skill`.
Defaults to true.
@@ -0,0 +1,226 @@
#!/usr/bin/env python3
"""
OpenAI YAML Generator - Creates agents/openai.yaml for a skill folder.
Usage:
generate_openai_yaml.py <skill_dir> [--name <skill_name>] [--interface key=value]
"""
import argparse
import re
import sys
from pathlib import Path
ACRONYMS = {
"GH",
"MCP",
"API",
"CI",
"CLI",
"LLM",
"PDF",
"PR",
"UI",
"URL",
"SQL",
}
BRANDS = {
"openai": "OpenAI",
"openapi": "OpenAPI",
"github": "GitHub",
"pagerduty": "PagerDuty",
"datadog": "DataDog",
"sqlite": "SQLite",
"fastapi": "FastAPI",
}
SMALL_WORDS = {"and", "or", "to", "up", "with"}
ALLOWED_INTERFACE_KEYS = {
"display_name",
"short_description",
"icon_small",
"icon_large",
"brand_color",
"default_prompt",
}
def yaml_quote(value):
escaped = value.replace("\\", "\\\\").replace('"', '\\"').replace("\n", "\\n")
return f'"{escaped}"'
def format_display_name(skill_name):
words = [word for word in skill_name.split("-") if word]
formatted = []
for index, word in enumerate(words):
lower = word.lower()
upper = word.upper()
if upper in ACRONYMS:
formatted.append(upper)
continue
if lower in BRANDS:
formatted.append(BRANDS[lower])
continue
if index > 0 and lower in SMALL_WORDS:
formatted.append(lower)
continue
formatted.append(word.capitalize())
return " ".join(formatted)
def generate_short_description(display_name):
description = f"Help with {display_name} tasks"
if len(description) < 25:
description = f"Help with {display_name} tasks and workflows"
if len(description) < 25:
description = f"Help with {display_name} tasks with guidance"
if len(description) > 64:
description = f"Help with {display_name}"
if len(description) > 64:
description = f"{display_name} helper"
if len(description) > 64:
description = f"{display_name} tools"
if len(description) > 64:
suffix = " helper"
max_name_length = 64 - len(suffix)
trimmed = display_name[:max_name_length].rstrip()
description = f"{trimmed}{suffix}"
if len(description) > 64:
description = description[:64].rstrip()
if len(description) < 25:
description = f"{description} workflows"
if len(description) > 64:
description = description[:64].rstrip()
return description
def read_frontmatter_name(skill_dir):
skill_md = Path(skill_dir) / "SKILL.md"
if not skill_md.exists():
print(f"[ERROR] SKILL.md not found in {skill_dir}")
return None
content = skill_md.read_text()
match = re.match(r"^---\n(.*?)\n---", content, re.DOTALL)
if not match:
print("[ERROR] Invalid SKILL.md frontmatter format.")
return None
frontmatter_text = match.group(1)
import yaml
try:
frontmatter = yaml.safe_load(frontmatter_text)
except yaml.YAMLError as exc:
print(f"[ERROR] Invalid YAML frontmatter: {exc}")
return None
if not isinstance(frontmatter, dict):
print("[ERROR] Frontmatter must be a YAML dictionary.")
return None
name = frontmatter.get("name", "")
if not isinstance(name, str) or not name.strip():
print("[ERROR] Frontmatter 'name' is missing or invalid.")
return None
return name.strip()
def parse_interface_overrides(raw_overrides):
overrides = {}
optional_order = []
for item in raw_overrides:
if "=" not in item:
print(f"[ERROR] Invalid interface override '{item}'. Use key=value.")
return None, None
key, value = item.split("=", 1)
key = key.strip()
value = value.strip()
if not key:
print(f"[ERROR] Invalid interface override '{item}'. Key is empty.")
return None, None
if key not in ALLOWED_INTERFACE_KEYS:
allowed = ", ".join(sorted(ALLOWED_INTERFACE_KEYS))
print(f"[ERROR] Unknown interface field '{key}'. Allowed: {allowed}")
return None, None
overrides[key] = value
if key not in ("display_name", "short_description") and key not in optional_order:
optional_order.append(key)
return overrides, optional_order
def write_openai_yaml(skill_dir, skill_name, raw_overrides):
overrides, optional_order = parse_interface_overrides(raw_overrides)
if overrides is None:
return None
display_name = overrides.get("display_name") or format_display_name(skill_name)
short_description = overrides.get("short_description") or generate_short_description(display_name)
if not (25 <= len(short_description) <= 64):
print(
"[ERROR] short_description must be 25-64 characters "
f"(got {len(short_description)})."
)
return None
interface_lines = [
"interface:",
f" display_name: {yaml_quote(display_name)}",
f" short_description: {yaml_quote(short_description)}",
]
for key in optional_order:
value = overrides.get(key)
if value is not None:
interface_lines.append(f" {key}: {yaml_quote(value)}")
agents_dir = Path(skill_dir) / "agents"
agents_dir.mkdir(parents=True, exist_ok=True)
output_path = agents_dir / "openai.yaml"
output_path.write_text("\n".join(interface_lines) + "\n")
print(f"[OK] Created agents/openai.yaml")
return output_path
def main():
parser = argparse.ArgumentParser(
description="Create agents/openai.yaml for a skill directory.",
)
parser.add_argument("skill_dir", help="Path to the skill directory")
parser.add_argument(
"--name",
help="Skill name override (defaults to SKILL.md frontmatter)",
)
parser.add_argument(
"--interface",
action="append",
default=[],
help="Interface override in key=value format (repeatable)",
)
args = parser.parse_args()
skill_dir = Path(args.skill_dir).resolve()
if not skill_dir.exists():
print(f"[ERROR] Skill directory not found: {skill_dir}")
sys.exit(1)
if not skill_dir.is_dir():
print(f"[ERROR] Path is not a directory: {skill_dir}")
sys.exit(1)
skill_name = args.name or read_frontmatter_name(skill_dir)
if not skill_name:
sys.exit(1)
result = write_openai_yaml(skill_dir, skill_name, args.interface)
if result:
sys.exit(0)
sys.exit(1)
if __name__ == "__main__":
main()
+400
View File
@@ -0,0 +1,400 @@
#!/usr/bin/env python3
"""
Skill Initializer - Creates a new skill from template
Usage:
init_skill.py <skill-name> --path <path> [--resources scripts,references,assets] [--examples] [--interface key=value]
Examples:
init_skill.py my-new-skill --path skills/public
init_skill.py my-new-skill --path skills/public --resources scripts,references
init_skill.py my-api-helper --path skills/private --resources scripts --examples
init_skill.py custom-skill --path /custom/location
init_skill.py my-skill --path skills/public --interface short_description="Short UI label"
"""
import argparse
import re
import sys
from pathlib import Path
from generate_openai_yaml import write_openai_yaml
MAX_SKILL_NAME_LENGTH = 64
ALLOWED_RESOURCES = {"scripts", "references", "assets"}
SKILL_TEMPLATE = """---
name: {skill_name}
description: [TODO: Complete and informative explanation of what the skill does and when to use it. Include WHEN to use this skill - specific scenarios, file types, or tasks that trigger it.]
---
# {skill_title}
## Overview
[TODO: 1-2 sentences explaining what this skill enables]
## Structuring This Skill
[TODO: Choose the structure that best fits this skill's purpose. Common patterns:
**1. Workflow-Based** (best for sequential processes)
- Works well when there are clear step-by-step procedures
- Example: DOCX skill with "Workflow Decision Tree" -> "Reading" -> "Creating" -> "Editing"
- Structure: ## Overview -> ## Workflow Decision Tree -> ## Step 1 -> ## Step 2...
**2. Task-Based** (best for tool collections)
- Works well when the skill offers different operations/capabilities
- Example: PDF skill with "Quick Start" -> "Merge PDFs" -> "Split PDFs" -> "Extract Text"
- Structure: ## Overview -> ## Quick Start -> ## Task Category 1 -> ## Task Category 2...
**3. Reference/Guidelines** (best for standards or specifications)
- Works well for brand guidelines, coding standards, or requirements
- Example: Brand styling with "Brand Guidelines" -> "Colors" -> "Typography" -> "Features"
- Structure: ## Overview -> ## Guidelines -> ## Specifications -> ## Usage...
**4. Capabilities-Based** (best for integrated systems)
- Works well when the skill provides multiple interrelated features
- Example: Product Management with "Core Capabilities" -> numbered capability list
- Structure: ## Overview -> ## Core Capabilities -> ### 1. Feature -> ### 2. Feature...
Patterns can be mixed and matched as needed. Most skills combine patterns (e.g., start with task-based, add workflow for complex operations).
Delete this entire "Structuring This Skill" section when done - it's just guidance.]
## [TODO: Replace with the first main section based on chosen structure]
[TODO: Add content here. See examples in existing skills:
- Code samples for technical skills
- Decision trees for complex workflows
- Concrete examples with realistic user requests
- References to scripts/templates/references as needed]
## Resources (optional)
Create only the resource directories this skill actually needs. Delete this section if no resources are required.
### scripts/
Executable code (Python/Bash/etc.) that can be run directly to perform specific operations.
**Examples from other skills:**
- PDF skill: `fill_fillable_fields.py`, `extract_form_field_info.py` - utilities for PDF manipulation
- DOCX skill: `document.py`, `utilities.py` - Python modules for document processing
**Appropriate for:** Python scripts, shell scripts, or any executable code that performs automation, data processing, or specific operations.
**Note:** Scripts may be executed without loading into context, but can still be read by Codex for patching or environment adjustments.
### references/
Documentation and reference material intended to be loaded into context to inform Codex's process and thinking.
**Examples from other skills:**
- Product management: `communication.md`, `context_building.md` - detailed workflow guides
- BigQuery: API reference documentation and query examples
- Finance: Schema documentation, company policies
**Appropriate for:** In-depth documentation, API references, database schemas, comprehensive guides, or any detailed information that Codex should reference while working.
### assets/
Files not intended to be loaded into context, but rather used within the output Codex produces.
**Examples from other skills:**
- Brand styling: PowerPoint template files (.pptx), logo files
- Frontend builder: HTML/React boilerplate project directories
- Typography: Font files (.ttf, .woff2)
**Appropriate for:** Templates, boilerplate code, document templates, images, icons, fonts, or any files meant to be copied or used in the final output.
---
**Not every skill requires all three types of resources.**
"""
EXAMPLE_SCRIPT = '''#!/usr/bin/env python3
"""
Example helper script for {skill_name}
This is a placeholder script that can be executed directly.
Replace with actual implementation or delete if not needed.
Example real scripts from other skills:
- pdf/scripts/fill_fillable_fields.py - Fills PDF form fields
- pdf/scripts/convert_pdf_to_images.py - Converts PDF pages to images
"""
def main():
print("This is an example script for {skill_name}")
# TODO: Add actual script logic here
# This could be data processing, file conversion, API calls, etc.
if __name__ == "__main__":
main()
'''
EXAMPLE_REFERENCE = """# Reference Documentation for {skill_title}
This is a placeholder for detailed reference documentation.
Replace with actual reference content or delete if not needed.
Example real reference docs from other skills:
- product-management/references/communication.md - Comprehensive guide for status updates
- product-management/references/context_building.md - Deep-dive on gathering context
- bigquery/references/ - API references and query examples
## When Reference Docs Are Useful
Reference docs are ideal for:
- Comprehensive API documentation
- Detailed workflow guides
- Complex multi-step processes
- Information too lengthy for main SKILL.md
- Content that's only needed for specific use cases
## Structure Suggestions
### API Reference Example
- Overview
- Authentication
- Endpoints with examples
- Error codes
- Rate limits
### Workflow Guide Example
- Prerequisites
- Step-by-step instructions
- Common patterns
- Troubleshooting
- Best practices
"""
EXAMPLE_ASSET = """# Example Asset File
This placeholder represents where asset files would be stored.
Replace with actual asset files (templates, images, fonts, etc.) or delete if not needed.
Asset files are NOT intended to be loaded into context, but rather used within
the output Codex produces.
Example asset files from other skills:
- Brand guidelines: logo.png, slides_template.pptx
- Frontend builder: hello-world/ directory with HTML/React boilerplate
- Typography: custom-font.ttf, font-family.woff2
- Data: sample_data.csv, test_dataset.json
## Common Asset Types
- Templates: .pptx, .docx, boilerplate directories
- Images: .png, .jpg, .svg, .gif
- Fonts: .ttf, .otf, .woff, .woff2
- Boilerplate code: Project directories, starter files
- Icons: .ico, .svg
- Data files: .csv, .json, .xml, .yaml
Note: This is a text placeholder. Actual assets can be any file type.
"""
def normalize_skill_name(skill_name):
"""Normalize a skill name to lowercase hyphen-case."""
normalized = skill_name.strip().lower()
normalized = re.sub(r"[^a-z0-9]+", "-", normalized)
normalized = normalized.strip("-")
normalized = re.sub(r"-{2,}", "-", normalized)
return normalized
def title_case_skill_name(skill_name):
"""Convert hyphenated skill name to Title Case for display."""
return " ".join(word.capitalize() for word in skill_name.split("-"))
def parse_resources(raw_resources):
if not raw_resources:
return []
resources = [item.strip() for item in raw_resources.split(",") if item.strip()]
invalid = sorted({item for item in resources if item not in ALLOWED_RESOURCES})
if invalid:
allowed = ", ".join(sorted(ALLOWED_RESOURCES))
print(f"[ERROR] Unknown resource type(s): {', '.join(invalid)}")
print(f" Allowed: {allowed}")
sys.exit(1)
deduped = []
seen = set()
for resource in resources:
if resource not in seen:
deduped.append(resource)
seen.add(resource)
return deduped
def create_resource_dirs(skill_dir, skill_name, skill_title, resources, include_examples):
for resource in resources:
resource_dir = skill_dir / resource
resource_dir.mkdir(exist_ok=True)
if resource == "scripts":
if include_examples:
example_script = resource_dir / "example.py"
example_script.write_text(EXAMPLE_SCRIPT.format(skill_name=skill_name))
example_script.chmod(0o755)
print("[OK] Created scripts/example.py")
else:
print("[OK] Created scripts/")
elif resource == "references":
if include_examples:
example_reference = resource_dir / "api_reference.md"
example_reference.write_text(EXAMPLE_REFERENCE.format(skill_title=skill_title))
print("[OK] Created references/api_reference.md")
else:
print("[OK] Created references/")
elif resource == "assets":
if include_examples:
example_asset = resource_dir / "example_asset.txt"
example_asset.write_text(EXAMPLE_ASSET)
print("[OK] Created assets/example_asset.txt")
else:
print("[OK] Created assets/")
def init_skill(skill_name, path, resources, include_examples, interface_overrides):
"""
Initialize a new skill directory with template SKILL.md.
Args:
skill_name: Name of the skill
path: Path where the skill directory should be created
resources: Resource directories to create
include_examples: Whether to create example files in resource directories
Returns:
Path to created skill directory, or None if error
"""
# Determine skill directory path
skill_dir = Path(path).resolve() / skill_name
# Check if directory already exists
if skill_dir.exists():
print(f"[ERROR] Skill directory already exists: {skill_dir}")
return None
# Create skill directory
try:
skill_dir.mkdir(parents=True, exist_ok=False)
print(f"[OK] Created skill directory: {skill_dir}")
except Exception as e:
print(f"[ERROR] Error creating directory: {e}")
return None
# Create SKILL.md from template
skill_title = title_case_skill_name(skill_name)
skill_content = SKILL_TEMPLATE.format(skill_name=skill_name, skill_title=skill_title)
skill_md_path = skill_dir / "SKILL.md"
try:
skill_md_path.write_text(skill_content)
print("[OK] Created SKILL.md")
except Exception as e:
print(f"[ERROR] Error creating SKILL.md: {e}")
return None
# Create agents/openai.yaml
try:
result = write_openai_yaml(skill_dir, skill_name, interface_overrides)
if not result:
return None
except Exception as e:
print(f"[ERROR] Error creating agents/openai.yaml: {e}")
return None
# Create resource directories if requested
if resources:
try:
create_resource_dirs(skill_dir, skill_name, skill_title, resources, include_examples)
except Exception as e:
print(f"[ERROR] Error creating resource directories: {e}")
return None
# Print next steps
print(f"\n[OK] Skill '{skill_name}' initialized successfully at {skill_dir}")
print("\nNext steps:")
print("1. Edit SKILL.md to complete the TODO items and update the description")
if resources:
if include_examples:
print("2. Customize or delete the example files in scripts/, references/, and assets/")
else:
print("2. Add resources to scripts/, references/, and assets/ as needed")
else:
print("2. Create resource directories only if needed (scripts/, references/, assets/)")
print("3. Update agents/openai.yaml if the UI metadata should differ")
print("4. Run the validator when ready to check the skill structure")
print(
"5. Forward-test complex skills with realistic user requests to ensure they work as intended"
)
return skill_dir
def main():
parser = argparse.ArgumentParser(
description="Create a new skill directory with a SKILL.md template.",
)
parser.add_argument("skill_name", help="Skill name (normalized to hyphen-case)")
parser.add_argument("--path", required=True, help="Output directory for the skill")
parser.add_argument(
"--resources",
default="",
help="Comma-separated list: scripts,references,assets",
)
parser.add_argument(
"--examples",
action="store_true",
help="Create example files inside the selected resource directories",
)
parser.add_argument(
"--interface",
action="append",
default=[],
help="Interface override in key=value format (repeatable)",
)
args = parser.parse_args()
raw_skill_name = args.skill_name
skill_name = normalize_skill_name(raw_skill_name)
if not skill_name:
print("[ERROR] Skill name must include at least one letter or digit.")
sys.exit(1)
if len(skill_name) > MAX_SKILL_NAME_LENGTH:
print(
f"[ERROR] Skill name '{skill_name}' is too long ({len(skill_name)} characters). "
f"Maximum is {MAX_SKILL_NAME_LENGTH} characters."
)
sys.exit(1)
if skill_name != raw_skill_name:
print(f"Note: Normalized skill name from '{raw_skill_name}' to '{skill_name}'.")
resources = parse_resources(args.resources)
if args.examples and not resources:
print("[ERROR] --examples requires --resources to be set.")
sys.exit(1)
path = args.path
print(f"Initializing skill: {skill_name}")
print(f" Location: {path}")
if resources:
print(f" Resources: {', '.join(resources)}")
if args.examples:
print(" Examples: enabled")
else:
print(" Resources: none (create as needed)")
print()
result = init_skill(skill_name, path, resources, args.examples, args.interface)
if result:
sys.exit(0)
else:
sys.exit(1)
if __name__ == "__main__":
main()
+101
View File
@@ -0,0 +1,101 @@
#!/usr/bin/env python3
"""
Quick validation script for skills - minimal version
"""
import re
import sys
from pathlib import Path
import yaml
MAX_SKILL_NAME_LENGTH = 64
def validate_skill(skill_path):
"""Basic validation of a skill"""
skill_path = Path(skill_path)
skill_md = skill_path / "SKILL.md"
if not skill_md.exists():
return False, "SKILL.md not found"
content = skill_md.read_text()
if not content.startswith("---"):
return False, "No YAML frontmatter found"
match = re.match(r"^---\n(.*?)\n---", content, re.DOTALL)
if not match:
return False, "Invalid frontmatter format"
frontmatter_text = match.group(1)
try:
frontmatter = yaml.safe_load(frontmatter_text)
if not isinstance(frontmatter, dict):
return False, "Frontmatter must be a YAML dictionary"
except yaml.YAMLError as e:
return False, f"Invalid YAML in frontmatter: {e}"
allowed_properties = {"name", "description", "license", "allowed-tools", "metadata"}
unexpected_keys = set(frontmatter.keys()) - allowed_properties
if unexpected_keys:
allowed = ", ".join(sorted(allowed_properties))
unexpected = ", ".join(sorted(unexpected_keys))
return (
False,
f"Unexpected key(s) in SKILL.md frontmatter: {unexpected}. Allowed properties are: {allowed}",
)
if "name" not in frontmatter:
return False, "Missing 'name' in frontmatter"
if "description" not in frontmatter:
return False, "Missing 'description' in frontmatter"
name = frontmatter.get("name", "")
if not isinstance(name, str):
return False, f"Name must be a string, got {type(name).__name__}"
name = name.strip()
if name:
if not re.match(r"^[a-z0-9-]+$", name):
return (
False,
f"Name '{name}' should be hyphen-case (lowercase letters, digits, and hyphens only)",
)
if name.startswith("-") or name.endswith("-") or "--" in name:
return (
False,
f"Name '{name}' cannot start/end with hyphen or contain consecutive hyphens",
)
if len(name) > MAX_SKILL_NAME_LENGTH:
return (
False,
f"Name is too long ({len(name)} characters). "
f"Maximum is {MAX_SKILL_NAME_LENGTH} characters.",
)
description = frontmatter.get("description", "")
if not isinstance(description, str):
return False, f"Description must be a string, got {type(description).__name__}"
description = description.strip()
if description:
if "<" in description or ">" in description:
return False, "Description cannot contain angle brackets (< or >)"
if len(description) > 1024:
return (
False,
f"Description is too long ({len(description)} characters). Maximum is 1024 characters.",
)
return True, "Skill is valid!"
if __name__ == "__main__":
if len(sys.argv) != 2:
print("Usage: python quick_validate.py <skill_directory>")
sys.exit(1)
valid, message = validate_skill(sys.argv[1])
print(message)
sys.exit(0 if valid else 1)
+61
View File
@@ -0,0 +1,61 @@
---
name: storybook
description: Use when writing or reviewing Storybook stories (`.stories.tsx`) for React components.
---
# Storybook Component Stories
## Which Components Can Have Stories?
Only create stories for components that **do not** do any of the following:
- Depend on context.
- Fetch data via any private API, including Langfuses own tRPC API.
Stories should follow the `ComponentName.stories.tsx` filename pattern. Components covered by stories should have exactly one exported or public component.
If this is not the case, suggest splitting up the file that includes the component to be covered first.
Be mindful of the breadth a story covers. A story should show a component in isolation. Page-level compositions should be rare and intentional.
## What to Do If a Component Violates the Criteria
Suggest abstracting a presentational component that does not violate the criteria and receives relevant data via props.
Make sure the props are well-defined using TypeScript.
Keep the existing component, but update it to use the newly created component for rendering. These presentational components are easier to test and easier to reuse.
## How Stories Should Be Written
- Use "CSF Next" format by default.
- Cover only the relevant component by default.
- Avoid custom render functions by default.
- Use `satisfies` and typed Storybook metadata so invalid args, decorators, and play functions are type-checked.
- Use play functions to test user-relevant interactions after render, not to compensate for complex setup or hidden dependencies.
- Name stories after the state they represent, not the implementation. Also do not include the component name in the story name.
- Prefer: `Default`, `Empty`, `WithLongName`, `Error`, `Disabled`, `Loading`
- Avoid: `Test1`, `CustomRenderExample`, `ButtonWithLongNameAndIcon`
- Set callbacks up as Storybook Actions by default:
```ts
import { fn } from "storybook/test";
```
- Avoid large fixtures.
- Use the smallest meaningful data shape needed to render the state.
- If fixtures are required and may be shared, check whether a reusable helper function exists. Otherwise, create one for defining the fixture.
## Variant and Design Showcase Stories
If a component has many variants, and the point of the story is to showcase the design of a component rather than its functionality, stories may render the component multiple times.
For example, a `Button` with a `size: "sm" | "md" | "lg"` prop may have a story that shows three buttons side by side.
If the button also has a `variant: "primary" | "secondary"` prop, consider using a matrix-like UI that showcases all possible combinations.
These compositional stories **should not** contain Storybook play functions. They should also not allow the Storybook user to customize the predefined args, such as `size` and `variant`, via Storybook args. Having an arg for non-bound props, such as `text`, may be acceptable.
## Additional Information
- We do not use MSW and are not planning to add it.
@@ -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
@@ -0,0 +1,286 @@
---
name: weekly-production-review
description: |
Prepare Langfuse weekly production reviews that explain what broke, what was
fixed, what remains open, and where alerting or tracking needs cleanup. Use
when asked for a production review, "what broke last week", fixed/open bugs,
Datadog alerted monitors/pages, status-page incidents, incident.io incidents,
or an engineering-team overview that combines Linear, Datadog, and customer
incident signals.
---
# Weekly Production Review
Use this skill to produce a source-grounded, event-centric production review.
The report should help engineering understand the week, not just list tool
output.
## Scope
- Default "last week" to the previous Monday through Sunday in the user's
timezone. State both local and UTC query windows.
- Cover all production environments unless the user narrows scope:
`prod-us`, `prod-eu`, `prod-hipaa`, and `prod-jp`.
- Keep the first pass read-only. Do not create or update Linear issues,
comments, incident.io records, follow-ups, alerts, or Datadog monitors unless
the user explicitly asks after reviewing the findings.
- For chat-only reviews, avoid creating report artifacts or local analysis
workspaces unless a required tool workflow explicitly does so or the user asks
for a file. If incident.io analysis tooling requires a local playbook
workspace, mention it briefly inline when relevant and keep production systems
unchanged.
- Write `No measurements found` when a requested signal cannot be queried or
measured.
## Related Skills
- Use [`datadog-query-recipes`](../datadog-query-recipes/SKILL.md) for
production Datadog query shapes and environment/site routing.
- Use [`linear-bug-triage`](../linear-bug-triage/SKILL.md) only after a human
explicitly approves a Linear write-back.
## Workflow
1. Confirm the review window and timezone. If the user says "last week", use the
previous calendar week, not a rolling seven-day window.
2. Gather customer-facing incidents from the public status page and incident.io
if available. Prefer incident.io for internal accepted incidents and
follow-ups; use the status page as the customer-facing source of truth.
3. Gather Datadog alert/page signals for the window. Use incident.io alerts or
escalations when they represent pages; use Datadog monitor/event data when
available. First build the exhaustive alert universe by paginating through
Datadog events until no more results remain for the window; do not rely on a
truncated first page, sampled titles, or a few spot checks. Cover all prod
envs in scope even when one site is noisy or one env looks quiet. After the
full pass, group repeated firings of the same monitor instead of counting
every notification as a separate event.
4. Gather Linear bugs from the `bug` label first. Include all `bug`-labeled
tickets created, updated, completed, or still-open with production evidence
during the window. Inspect likely production bugs with issue details and
comments when status, owner, or evidence is unclear. Use Linear as the
source of truth for deduplication and follow-up ownership: search existing
issues, comments, and linked source URLs before treating a signal as new.
5. Classify each bug and alert. Separate production breakage from staging,
self-hosted, internal-only, duplicate, canceled, test, or monitor-noise
signals.
6. Build the event/evidence model below. One event row can cite multiple
status incidents, Datadog pages, Linear bugs, and follow-ups. One evidence
row can also be classified as non-production, noise, or no-action.
7. Synthesize the event-centric view first, then keep the raw source tables as
evidence sections. Lead with conclusions, not tool output.
## Event and Evidence Model
The report has one main engineering view and three evidence sections:
- `Event-Centric View`: one row per distinct production issue to discuss in
engineering review. This is the main narrative and the event count.
- `Customer Incident Table`: customer-facing incident records from the status
page or incident.io. These can support an event row but are not a separate
event count.
- `Linear Bug Table`: the `bug`-labeled issue universe touched by the window.
This explains fixed/open bug counts and dedupe decisions.
- `Datadog Alert/Page Signals`: monitor and page clusters. These are evidence
and measurement, not production events by themselves.
Use Linear as the source of truth for deduplication across weeks and workflows.
Before reporting a bug, security finding, cost concern, or alert as new, search
Linear for matching issue keys, titles, source URLs, and comments. If an
existing issue covers it, link to that issue and mark the review row as already
tracked instead of reporting it again as fresh work.
Anchor each event row with the most durable reference available:
- Use an incident.io incident when there is customer impact, status-page
communication, coordinated response, or post-incident follow-up.
- Use a Linear bug when production behavior broke but the issue did not become
an incident.
- Use an explicit alert disposition when the signal is `expected/test`,
`monitor noise`, or `unknown/no measurements` and no incident or Linear bug
should be created yet.
Treat Datadog as evidence, not ownership. Treat the public status page as the
customer-facing record, not the engineering source of truth. Keep source links
in the evidence section where they originated and cite the relevant evidence in
the event row.
For a healthy review, each real production event should satisfy one of:
```text
Event anchor = incident.io incident
OR event anchor = Linear production bug
OR event anchor = explicit alert disposition
```
### Proposed Link Titles
When proposing or later creating links, use short stable titles:
- `Datadog monitor: <monitor name>`
- `Datadog logs: <env/service/symptom>`
- `Datadog spans: <env/route/symptom>`
- `Datadog trace: <trace id or route>`
- `Status incident: <status title>`
- `incident.io: <INC reference>`
- `Linear follow-up: <issue key>`
Do not write any of these links unless the user explicitly asks for changes
after reviewing the report.
## Output Table Rules
Use the tables defined below as the default output contract. Keep the table
names, column names, and section order stable across runs so reviewers can scan
the same shape every week. Do not add extra source-specific tables unless the
user asks or the existing columns cannot represent an important finding.
If a section has no rows, keep the section and write `No rows found` or
`No measurements found` with the query/source that was checked. If a row is
unclear, classify it as `unclear` or `unknown/no measurements` instead of
dropping it.
## Linear Bug Table
Start from all Linear tickets with the `bug` label that were touched by the
window. Do not rely only on text searches for `prod`, `incident`, or `Datadog`;
those searches are useful for enrichment but are not the source universe. This
table is the Linear source inventory, not the event count. Multiple Linear bugs
can support one event row, and one bug can be classified as non-production,
duplicate, canceled, or no-action.
Use this table for the bug section:
| Linear | Title | Summary | Owner | Status | Touched Last Week Because | Production Evidence | Classification | Counted? |
| ------ | ----- | ------- | ----- | ------ | ------------------------- | ------------------- | -------------- | -------- |
Column rules:
- `Linear`: the issue key linked to Linear, such as `LFE-123`.
- `Title`: the Linear issue title in a separate column. Do not collapse the
title into the `Linear` link because reviewers need to scan IDs and titles
independently.
- `Summary`: one operational sentence based on issue body, comments, and
evidence. Avoid fix guesses.
- `Owner`: assignee if present; otherwise owning team if clear; otherwise
`Unassigned`.
- `Status`: the Linear status or state, plus completion timing when useful
such as `Done May 18`, `Todo`, `Triage`, or `Canceled`.
- `Touched Last Week Because`: `created`, `updated`, `completed`, or
`open production bug`.
- `Production Evidence`: prod env, customer impact, status incident, Datadog
link, measured logs/spans/errors, or `No measurements found`.
- `Classification`: use one of `production/customer-impacting`,
`internal-only`, `self-hosted`, `staging/dev`, `duplicate/canceled/no-action`,
or `unclear`.
- `Counted?`: `yes` only when the bug label and production/customer-impacting
evidence support including it in fixed/open production bug counts. Do not use
this field as the production event count.
For headline counts, report fixed and open production bugs separately from the
total number of bug-labeled tickets reviewed.
## Datadog Alert/Page Signals
Use this table as the Datadog evidence layer. It answers "what alerted or
paged?" and then links each alert cluster to an event row or an explicit
disposition.
| Monitor/Page Signal | Env | Count / Window | Why It Alerted | Verdict | Linked Event |
| ------------------- | --- | -------------: | -------------- | ------- | ------------ |
The Datadog table is monitor-centric, not the primary narrative. Use these
verdicts:
- `customer incident`
- `confirmed bug`
- `infra/dependency`
- `expected/test`
- `monitor noise`
- `unknown/no measurements`
Group repeated pages by monitor name or ID, environment, service/team, and
trigger reason. Exclude or clearly mark SLO/burn-rate monitors, test monitors,
and maintenance-window noise when the review is about actionable breakage.
The table must still account for every production Datadog alert cluster found
in the full event pass, including clusters later classified as `expected/test`,
`monitor noise`, or `unknown/no measurements`.
Before finalizing the review, perform a completeness check:
1. Compare the final Datadog table against the full paginated event sweep.
2. Confirm every production monitor title seen during the window appears in the
table or is explicitly excluded as non-prod.
3. If a known title is missing, add it before writing the narrative summary.
`Linked Event` should be the event row name, incident.io reference, Linear issue
key, or explicit disposition. Use `expected/test`, `monitor noise`,
`unknown/no measurements`, or `non-prod` when no engineering event should be
created. Do not leave a real production page as `none` unless the next action is
to classify the alert.
## Event-Centric View
Use this as the main engineering narrative:
| Event | Impact | Sources | State | Owner / Team | Next Action |
| ----- | ------ | ------- | ----- | ------------ | ----------- |
The event-centric view answers "what actually broke and what should engineering
discuss?" It deduplicates the evidence sections into production issues. Combine
related status incidents, Datadog pages, Linear bugs, and follow-ups into one
row when the evidence supports it. If correlation is inferential, say so.
Good event rows:
- Name the affected product surface or system behavior.
- State impact only as far as sources support it.
- Use the incident.io reference, Linear issue key, or alert disposition in the
event name or sources.
- Link source IDs such as status incident IDs, incident.io references, Datadog
monitor IDs, and Linear issue keys.
- Mark state as `fixed`, `mitigated`, `open`, `monitoring`, `noise`, or
`unknown`.
- Prefer a concrete next action: fix owner, monitor tuning, correlation cleanup,
close stale ticket, or no action.
## Customer Incident Table
Use this section for public status-page incidents and accepted incident.io
incidents. This table preserves the customer-facing incident record and its
timing; it does not replace the event-centric view.
| Incident | Severity / Status | Start / End / Duration | Impact | Linked Sources |
| -------- | ----------------- | ---------------------- | ------ | -------------- |
Lead each incident summary with its reference or URL. Preserve uncertainty when
status-page timezone, severity, linked alerts, or Linear follow-ups are missing.
## Executive Summary
Start the final report with:
- Review window and environments checked.
- Number of customer-facing incidents.
- Number of Datadog alert/page clusters, plus noisy/test clusters if relevant.
- Number of `bug`-labeled Linear tickets reviewed.
- Production bug count split by fixed and open.
- Highest open risk and why.
Then present sections in this order:
1. Event-Centric View.
2. Customer Incident Table.
3. Linear Bug Table.
4. Datadog Alert/Page Signals.
## Output Format
Return valid Markdown only.
- Use valid Markdown syntax for headings, bullets, links, and tables.
- Include a space after list markers such as `-`, `*`, and `1.`.
- Close links and parentheses correctly.
- Do not emit malformed tables, dangling backticks, or partially opened code
fences.
- If a section would be fragile to format, prefer a plain paragraph over broken
Markdown.
@@ -0,0 +1,4 @@
interface:
display_name: "Weekly Production Review"
short_description: "Summarize bugs, pages, and incidents"
default_prompt: "Use $weekly-production-review to prepare an event-centric weekly production review from Linear bugs, Datadog alert/page signals, and status-page or incident data."
+13 -4
View File
@@ -1,13 +1,22 @@
# Dev container Dockerfile
FROM --platform=${BUILDPLATFORM} golang:1.24 AS migrate-builder
ARG TARGETOS
ARG TARGETARCH
ENV CGO_ENABLED=0 \
GOBIN=/out \
GOOS=${TARGETOS} \
GOARCH=${TARGETARCH}
# Build only the ClickHouse migrate CLI used in this repo.
RUN /usr/local/go/bin/go install -trimpath -tags 'clickhouse' -ldflags='-s -w' \
github.com/golang-migrate/migrate/v4/cmd/migrate@v4.19.1
FROM mcr.microsoft.com/devcontainers/universal:2
# Install golang-migrate for database migrations
RUN curl -L https://github.com/golang-migrate/migrate/releases/download/v4.19.1/migrate.linux-amd64.tar.gz | tar xvz && \
chmod +x migrate && \
mv migrate /usr/local/bin/migrate
COPY --from=migrate-builder /out/migrate /usr/local/bin/migrate
# Activate the repo's pinned pnpm via Corepack
RUN corepack enable && corepack prepare pnpm@10.33.0 --activate
RUN corepack enable && corepack prepare pnpm@11.4.0 --activate
# Install Clickhouse
RUN curl https://clickhouse.com/ | sh && \
+1
View File
@@ -77,6 +77,7 @@ LANGFUSE_AZURE_SKIP_CONTAINER_CHECK=false
REDIS_HOST="127.0.0.1"
REDIS_PORT=6379
REDIS_AUTH="myredissecret"
# LANGFUSE_BULLMQ_SKIP_REDIS_VERSION_CHECK="false" # Set to true for Redis-compatible services that report a non-Redis version
# openssl rand -hex 32 used only here
ENCRYPTION_KEY=0000000000000000000000000000000000000000000000000000000000000000
+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.
#####################################################################
+2
View File
@@ -76,11 +76,13 @@ REDIS_AUTH="bitnami"
# REDIS_KEY_PREFIX="" # Optional: Prefix for Redis keys (useful for multi-tenant Redis instances)
# BullMQ queues will use this via BullMQ's native prefix option
# Cache operations will use this via ioredis keyPrefix
# LANGFUSE_BULLMQ_SKIP_REDIS_VERSION_CHECK="false" # Set to true for Redis-compatible services that report a non-Redis version
REDIS_CLUSTER_ENABLED="true"
REDIS_CLUSTER_NODES="127.0.0.1:6370,127.0.0.1:6371,127.0.0.1:6372,127.0.0.1:6373,127.0.0.1:6374,127.0.0.1:6375"
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
+44 -6
View File
@@ -62,9 +62,15 @@ NEXTAUTH_SECRET="secret"
# Langfuse Cloud Environment
NEXT_PUBLIC_LANGFUSE_CLOUD_REGION="DEV"
# Dev-only: override the legacy blob-export cutoff date for local testing.
# Set to a past date (e.g. 2020-01-01T00:00:00.000Z) to make every project
# post-cutoff, or a future date (e.g. 2099-01-01T00:00:00.000Z) to grandfather
# all projects. Must be a valid ISO 8601 datetime string. Leave unset in prod.
# NEXT_PUBLIC_LANGFUSE_BLOB_EXPORT_CUTOFF=
# Langfuse experimental features
LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES="false"
LANGFUSE_ENABLE_WEB_CALLOUTS="true"
# Salt for API key hashing
SALT="salt"
@@ -116,6 +122,7 @@ REDIS_AUTH="myredissecret"
# REDIS_KEY_PREFIX="" # Optional: Prefix for Redis keys (useful for multi-tenant Redis instances)
# BullMQ queues will use this via BullMQ's native prefix option
# Cache operations will use this via ioredis keyPrefix
# LANGFUSE_BULLMQ_SKIP_REDIS_VERSION_CHECK="false" # Set to true for Redis-compatible services that report a non-Redis version
# REDIS_SENTINEL_ENABLED="false"
# REDIS_SENTINEL_NODES="sentinel1:26379,sentinel2:26379"
# REDIS_SENTINEL_MASTER_NAME="mymaster"
@@ -134,6 +141,18 @@ NEXT_PUBLIC_LANGFUSE_RUN_NEXT_INIT="false"
# LANGFUSE_EVAL_EXECUTION_WORKER_CONCURRENCY=5
# LANGFUSE_LLM_AS_JUDGE_EXECUTION_WORKER_CONCURRENCY=5
# Code-based eval dispatchers. Local dev defaults to the insecure in-process dispatcher.
# To explicitly use the local dispatcher, set:
# LANGFUSE_CODE_EVAL_DISPATCHER=insecure-local
# To test the AWS Lambda dispatcher against Floci, set:
# LANGFUSE_CODE_EVAL_DISPATCHER=aws-lambda
# LANGFUSE_CODE_EVAL_AWS_LAMBDA_ENDPOINT=http://localhost:4566
# Override Lambda function names if your deployment uses non-default names:
# LANGFUSE_CODE_EVAL_AWS_LAMBDA_NODE_FUNCTION_NAME=code-based-eval-executor-node
# LANGFUSE_CODE_EVAL_AWS_LAMBDA_PYTHON_FUNCTION_NAME=code-based-eval-executor-python
# Override the local dispatcher execution timeout (default: 2000ms):
# LANGFUSE_CODE_EVAL_LOCAL_TIMEOUT_MS=2000
# Slack credentials for development
SLACK_CLIENT_ID=your_slack_client_id
SLACK_CLIENT_SECRET=your_slack_client_secret
@@ -145,16 +164,35 @@ LANGFUSE_AI_FEATURES_SECRET_KEY="sk-lf-1234567890"
LANGFUSE_AI_FEATURES_HOST="http://localhost:3000"
LANGFUSE_AI_FEATURES_PROJECT_ID=7a88fb47-b4e2-43b8-a06c-a5ce950dc53a
# Self-hosted only: allow internal LLM proxy hosts/IPs for LLM connection base URLs.
# LANGFUSE_LLM_CONNECTION_WHITELISTED_HOST=localhost
# LANGFUSE_LLM_CONNECTION_WHITELISTED_IPS=127.0.0.1,::1
# LANGFUSE_LLM_CONNECTION_WHITELISTED_IP_SEGMENTS=127.0.0.0/8
# Self-hosted only: allow internal hosts/IPs for user-configured blob storage endpoints.
# LANGFUSE_BLOB_STORAGE_ENDPOINT_WHITELISTED_HOST=localhost
# LANGFUSE_BLOB_STORAGE_ENDPOINT_WHITELISTED_IPS=127.0.0.1,::1
# LANGFUSE_BLOB_STORAGE_ENDPOINT_WHITELISTED_IP_SEGMENTS=127.0.0.0/8
# 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"
LANGFUSE_AWS_BEDROCK_MODEL="eu.anthropic.claude-haiku-4-5-20251001-v1:0"
LANGFUSE_IN_APP_AGENT_AWS_PROFILE="playground"
# Events table migration
LANGFUSE_ENABLE_EVENTS_TABLE_OBSERVATIONS=true
LANGFUSE_ENABLE_EVENTS_TABLE_FLAGS=true
LANGFUSE_ENABLE_EVENTS_TABLE_V2_APIS=true
LANGFUSE_EXPERIMENT_INSERT_INTO_EVENTS_TABLE=true
# V4 migration flags.
# Write target: `legacy` | `dual` | `events_only`.
LANGFUSE_MIGRATION_V4_WRITE_MODE=dual
# Gate the V4 events_full read paths (UI, tRPC, v2 APIs, observations API).
# Self-hosted `dual` deployments must set this to `true` to offer users the V4
# preview toggle; with `dual` + `false` the preview stays unavailable to users.
LANGFUSE_MIGRATION_V4_ALLOW_PREVIEW_OPT_IN=true
# OTel ingestion behaviour: `dual_write` (SDK-version dispatch) or `direct`.
LANGFUSE_MIGRATION_V4_NATIVE_OTEL_BEHAVIOUR=dual_write
# Legacy tracing UI controls
LANGFUSE_DISABLE_LEGACY_TRACING_IO_SEARCH=false
CLICKHOUSE_USE_LIGHTWEIGHT_UPDATE="true"
CLICKHOUSE_DISABLE_LAZY_MATERIALIZATION=true
+27 -1
View File
@@ -54,6 +54,7 @@ OTEL_SERVICE_NAME="langfuse"
# Enable experimental features, optional
# LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES=false
# LANGFUSE_ENABLE_WEB_CALLOUTS=false
# Auth, optional configuration
# AUTH_DOMAINS_WITH_SSO_ENFORCEMENT=domain1.com,domain2.com
@@ -69,6 +70,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 +88,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 +96,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 +141,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
@@ -191,6 +201,7 @@ LANGFUSE_ENABLE_BLOB_STORAGE_FILE_LOG=true
# REDIS_KEY_PREFIX= # Optional: Prefix for Redis keys (useful for multi-tenant Redis instances)
# BullMQ queues will use this via BullMQ's native prefix option
# Cache operations will use this via ioredis keyPrefix
# LANGFUSE_BULLMQ_SKIP_REDIS_VERSION_CHECK=false # Set to true for Redis-compatible services that report a non-Redis version
# Redis Cluster configuration (optional)
# REDIS_CLUSTER_ENABLED=false
@@ -216,6 +227,10 @@ LANGFUSE_ENABLE_BLOB_STORAGE_FILE_LOG=true
# CLICKHOUSE_USER=
# CLICKHOUSE_PASSWORD=
# CLICKHOUSE_CLUSTER_ENABLED=true
# LANGFUSE_CLICKHOUSE_DELETED_MASK_CLEANER_ENABLED=false
# LANGFUSE_CLICKHOUSE_DELETED_MASK_CLEANER_INTERVAL_MS=3600000
# LANGFUSE_CLICKHOUSE_DELETED_MASK_CLEANER_SUBMIT_TIMEOUT_MS=60000
# LANGFUSE_CLICKHOUSE_DELETED_MASK_CLEANER_CLUSTER_MODE_ENABLED=false
# Ingestion configuration
# LANGFUSE_INGESTION_QUEUE_DELAY_MS=
@@ -239,6 +254,10 @@ LANGFUSE_ENABLE_BLOB_STORAGE_FILE_LOG=true
# Valid values: core, io, scores, observations, metrics
# LANGFUSE_API_TRACEBYID_DEFAULT_FIELDS=
# Legacy tracing UI controls
# Disable input/output full-text search in legacy V3 tracing tables (metadata search remains available)
# LANGFUSE_DISABLE_LEGACY_TRACING_IO_SEARCH=false
### START Enterprise Edition Configuration
# Allowlisted users that can create new organizations, by default all users can create organizations
@@ -291,7 +310,6 @@ LANGFUSE_ENABLE_BLOB_STORAGE_FILE_LOG=true
# Plain Chat
# NEXT_PUBLIC_PLAIN_APP_ID=
# PLAIN_AUTHENTICATION_SECRET=
# PLAIN_API_KEY=
# PLAIN_CARDS_API_TOKEN=
# Pylon Support
@@ -299,6 +317,14 @@ LANGFUSE_ENABLE_BLOB_STORAGE_FILE_LOG=true
# Admin API
# ADMIN_API_KEY=
# Self-hosted only: allow internal LLM proxy hosts/IPs for LLM connection base URLs.
# LANGFUSE_LLM_CONNECTION_WHITELISTED_HOST=
# LANGFUSE_LLM_CONNECTION_WHITELISTED_IPS=
# LANGFUSE_LLM_CONNECTION_WHITELISTED_IP_SEGMENTS=
# Self-hosted only: allow internal hosts/IPs for user-configured blob storage endpoints.
# LANGFUSE_BLOB_STORAGE_ENDPOINT_WHITELISTED_HOST=
# LANGFUSE_BLOB_STORAGE_ENDPOINT_WHITELISTED_IPS=
# LANGFUSE_BLOB_STORAGE_ENDPOINT_WHITELISTED_IP_SEGMENTS=
# LANGFUSE_CACHE_MODEL_MATCH_ENABLED=
# LANGFUSE_CACHE_MODEL_MATCH_TTL_SECONDS=
+3
View File
@@ -1,2 +1,5 @@
# Currently inactive
# * @langfuse/maintainers
# Require maintainer review for GitHub configuration changes
.github/ @langfuse/maintainers
+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
@@ -0,0 +1,30 @@
name: Notify Slack Failure
description: Send a CI failure notification to a Slack Workflow webhook.
inputs:
title:
description: Slack notification header.
required: true
message:
description: Slack notification fallback text.
required: true
webhook-url:
description: Slack Workflow webhook URL.
required: true
runs:
using: composite
steps:
- name: Notify Slack
uses: slackapi/slack-github-action@45a88b9581bfab2566dc881e2cd66d334e621e2c # v3.0.3
with:
webhook: ${{ inputs.webhook-url }}
webhook-type: webhook-trigger
payload: |
title: "${{ inputs.title }}"
message: "${{ inputs.message }}"
ref: "${{ github.ref_name }}"
actor: "${{ github.actor }}"
event: "${{ github.event_name }}"
commit: "${{ github.sha }}"
workflow_url: "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
+3 -1
View File
@@ -12,7 +12,7 @@ updates:
schedule:
interval: "daily"
cooldown:
default-days: 8
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@acca2b1b2070338fb9fd1ca27ecee81d687e58e5 # v6.1.2
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@fa648b43de3d4d023bcb3f89ed6940096949c419 # v2.1.5
- 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@6853cfae8c3a7d978fbf68b5a55453395541dfbb # v1.8.5
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@a310a830f5c14e583e35d84e4e1ec7dd177c3c9c # v2.6.2
with:
task-definition: ${{ steps.render-task-definition.outputs.task-definition }}
service: ${{ inputs.environment }}-${{ inputs.service }}
+4 -4
View File
@@ -28,14 +28,14 @@ jobs:
run: docker compose -f "docker-compose.yml" up -d --build
- name: Setup pnpm
uses: pnpm/action-setup@a3252b78c470c02df07e9d59298aecedc3ccdd6d # v3
uses: pnpm/action-setup@739bfe42ca9233c5e6aca07c1a25a9d34aca49b0 # v6.0.7
with:
version: 10.33.0
version: 11.4.0
- name: Setup Node 18
- name: Setup Node 24
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: 18
node-version: 24
- name: Get pnpm store directory
id: pnpm-cache
+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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: ${{ github.event.pull_request.head.sha }}
fetch-depth: 2
persist-credentials: false
- name: Checkout Claude Code security review action
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
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@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4.36.0
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@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4.36.0
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
+31 -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) }}
@@ -107,3 +116,20 @@ jobs:
with:
service: ${{ matrix.service }}
environment: ${{ matrix.environment }}
notify-slack-on-failure:
needs: [affected-services, affected-environments, ecs-deploy]
if: failure()
runs-on: blacksmith-4vcpu-ubuntu-2404
permissions:
contents: read
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Notify Slack
uses: ./.github/actions/notify-slack-failure
with:
title: "❌ Deploy Failed"
message: "❌ Deploy failed on ${{ github.ref_name }}"
webhook-url: ${{ secrets.SLACK_CI_FAILURE_WORKFLOW_WEBHOOK_URL }}
+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@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.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
File diff suppressed because it is too large Load Diff
@@ -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 }}
+25 -3
View File
@@ -5,15 +5,37 @@ on:
tags:
- "v3.[0-9]+.[0-9]+" # Semantic version tags
permissions: {}
jobs:
release:
runs-on: ubuntu-latest
runs-on: blacksmith-4vcpu-ubuntu-2404
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 }}
notify-slack-on-failure:
needs: release
if: failure()
runs-on: blacksmith-4vcpu-ubuntu-2404
permissions:
contents: read
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Notify Slack
uses: ./.github/actions/notify-slack-failure
with:
title: "❌ Release Failed"
message: "❌ Release failed on ${{ github.ref_name }}"
webhook-url: ${{ secrets.SLACK_CI_FAILURE_WORKFLOW_WEBHOOK_URL }}
+184 -42
View File
@@ -12,23 +12,35 @@ concurrency:
group: sdk-api-spec-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: read
jobs:
generate-sdk-api-specs:
runs-on: ubuntu-latest
runs-on: blacksmith-4vcpu-ubuntu-2404
outputs:
python_has_changes: ${{ steps.prepare-python-sdk.outputs.has_changes }}
python_branch_name: ${{ steps.prepare-python-sdk.outputs.branch_name }}
typescript_has_changes: ${{ steps.prepare-typescript-sdk.outputs.has_changes }}
typescript_branch_name: ${{ steps.prepare-typescript-sdk.outputs.branch_name }}
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@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8
with:
version: 10.33.0
version: 11.4.0
# Keep this secret-bearing job cache-cold to avoid exposing Fern
# credentials to potentially poisoned shared package-manager state.
- name: Setup Node.js
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: "24"
cache: "pnpm"
package-manager-cache: false
- name: Install dependencies
run: pnpm install
@@ -39,13 +51,12 @@ jobs:
run: npx fern-api generate --api server --force
- name: Install uv
uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
with:
version: "0.11.2"
- name: Update Python SDK
env:
GH_ACCESS_TOKEN: ${{ secrets.GH_ACCESS_TOKEN }}
- name: Prepare Python SDK update
id: prepare-python-sdk
run: |
# Clone langfuse-python repository
git clone https://github.com/langfuse/langfuse-python.git ../langfuse-python
@@ -53,8 +64,6 @@ jobs:
git config user.name "langfuse-bot"
git config user.email "langfuse-bot@langfuse.com"
echo "$GH_ACCESS_TOKEN" | gh auth login --with-token
gh auth setup-git
# Delete existing API contents
rm -rf langfuse/api/*
@@ -73,30 +82,31 @@ jobs:
# Check for changes and show diff for debugging
if git diff --quiet; then
echo "No changes detected in Python SDK"
echo "has_changes=false" >> "$GITHUB_OUTPUT"
exit 0
fi
# Close existing api-spec-bot PRs
gh pr list --author langfuse-bot --state open --json number --jq '.[].number' | xargs -I {} gh pr close {}
# Get the GitHub username of the original commit author
cd ../langfuse
ORIGINAL_AUTHOR=$(gh api repos/langfuse/langfuse/commits/${GITHUB_SHA} --jq '.author.login')
cd ../langfuse-python
# Create new branch and push changes
# Create new branch and commit changes
BRANCH_NAME="api-spec-bot-${GITHUB_SHA::7}"
git checkout -b "$BRANCH_NAME"
git add .
git commit -m "feat(api): update API spec from langfuse/langfuse ${GITHUB_SHA::7}"
git push origin "$BRANCH_NAME"
git bundle create "$RUNNER_TEMP/langfuse-python-api-spec-bot.bundle" "$BRANCH_NAME"
# Create PR with original author as reviewer
gh pr create --title "feat(api): update API spec from langfuse/langfuse ${GITHUB_SHA::7}" --body "" --reviewer "$ORIGINAL_AUTHOR"
echo "has_changes=true" >> "$GITHUB_OUTPUT"
echo "branch_name=$BRANCH_NAME" >> "$GITHUB_OUTPUT"
- name: Update TypeScript SDK
env:
GH_ACCESS_TOKEN: ${{ secrets.GH_ACCESS_TOKEN }}
- name: Upload Python SDK update
if: steps.prepare-python-sdk.outputs.has_changes == 'true'
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: langfuse-python-sdk-update
path: ${{ runner.temp }}/langfuse-python-api-spec-bot.bundle
if-no-files-found: error
retention-days: 1
- name: Prepare TypeScript SDK update
id: prepare-typescript-sdk
run: |
# Clone langfuse-js repository
git clone https://github.com/langfuse/langfuse-js.git ../langfuse-js
@@ -105,8 +115,6 @@ jobs:
# Configure git
git config user.name "langfuse-bot"
git config user.email "langfuse-bot@langfuse.com"
echo "$GH_ACCESS_TOKEN" | gh auth login --with-token
gh auth setup-git
# Delete existing API contents
rm -rf packages/core/src/api/*
@@ -125,30 +133,164 @@ jobs:
# Install dependencies and format
corepack enable
corepack prepare pnpm@10.33.0 --activate
corepack prepare pnpm@11.4.0 --activate
pnpm install
pnpm format
# Check for changes and show diff for debugging
if git diff --quiet; then
echo "No changes detected in TypeScript SDK"
echo "has_changes=false" >> "$GITHUB_OUTPUT"
exit 0
fi
# Close existing api-spec-bot PRs
gh pr list --author langfuse-bot --state open --json number --jq '.[].number' | xargs -I {} gh pr close {}
# Get the GitHub username of the original commit author
cd ../langfuse
ORIGINAL_AUTHOR=$(gh api repos/langfuse/langfuse/commits/${GITHUB_SHA} --jq '.author.login')
cd ../langfuse-js
# Create new branch and push changes
# Create new branch and commit changes
BRANCH_NAME="api-spec-bot-${GITHUB_SHA::7}"
git checkout -b "$BRANCH_NAME"
git add .
git commit -m "feat(api): update API spec from langfuse/langfuse ${GITHUB_SHA::7}" --no-verify
git push origin "$BRANCH_NAME"
git bundle create "$RUNNER_TEMP/langfuse-js-api-spec-bot.bundle" "$BRANCH_NAME"
# Create PR with original author as reviewer
gh pr create --title "feat(api): update API spec from langfuse/langfuse ${GITHUB_SHA::7}" --body "" --reviewer "$ORIGINAL_AUTHOR"
echo "has_changes=true" >> "$GITHUB_OUTPUT"
echo "branch_name=$BRANCH_NAME" >> "$GITHUB_OUTPUT"
- name: Upload TypeScript SDK update
if: steps.prepare-typescript-sdk.outputs.has_changes == 'true'
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: langfuse-js-sdk-update
path: ${{ runner.temp }}/langfuse-js-api-spec-bot.bundle
if-no-files-found: error
retention-days: 1
push-python-sdk:
needs: generate-sdk-api-specs
if: needs.generate-sdk-api-specs.outputs.python_has_changes == 'true'
runs-on: blacksmith-4vcpu-ubuntu-2404
steps:
- name: Download Python SDK update
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: langfuse-python-sdk-update
path: ${{ runner.temp }}/langfuse-python-sdk-update
- name: Push Python SDK update
env:
BRANCH_NAME: ${{ needs.generate-sdk-api-specs.outputs.python_branch_name }}
GH_TOKEN: ${{ secrets.GH_ACCESS_TOKEN }}
run: |
set -euo pipefail
GIT_BIN="$(command -v git)"
GH_BIN="$(command -v gh)"
BUNDLE_PATH="$RUNNER_TEMP/langfuse-python-sdk-update/langfuse-python-api-spec-bot.bundle"
PUSH_REPO="$RUNNER_TEMP/push-langfuse-python"
mkdir -p "$PUSH_REPO"
"$GIT_BIN" -C "$PUSH_REPO" init -q
"$GIT_BIN" -C "$PUSH_REPO" fetch --no-tags "$BUNDLE_PATH" "$BRANCH_NAME:refs/heads/$BRANCH_NAME"
# Close existing api-spec-bot PRs
"$GH_BIN" pr list --repo langfuse/langfuse-python --author langfuse-bot --state open --json number --jq '.[].number' | while read -r pr_number; do
if [ -n "$pr_number" ]; then
"$GH_BIN" pr close --repo langfuse/langfuse-python "$pr_number"
fi
done
# Resolve the reviewer before pushing so a transient GitHub API failure cannot orphan a branch.
ORIGINAL_AUTHOR="$("$GH_BIN" api "repos/langfuse/langfuse/commits/${GITHUB_SHA}" --jq '.author.login // empty')"
REVIEWER_ARGS=()
if [ -n "$ORIGINAL_AUTHOR" ]; then
REVIEWER_ARGS=(--reviewer "$ORIGINAL_AUTHOR")
fi
# Push new branch and create PR from a fresh repo that never ran SDK install scripts.
GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_SYSTEM=/dev/null "$GIT_BIN" \
-C "$PUSH_REPO" \
-c credential.helper= \
-c credential.https://github.com.helper= \
-c core.hooksPath=/dev/null \
push "https://x-access-token:${GH_TOKEN}@github.com/langfuse/langfuse-python.git" "$BRANCH_NAME"
"$GH_BIN" pr create \
--repo langfuse/langfuse-python \
--head "$BRANCH_NAME" \
--title "feat(api): update API spec from langfuse/langfuse ${GITHUB_SHA::7}" \
--body "" \
"${REVIEWER_ARGS[@]}"
push-typescript-sdk:
needs: generate-sdk-api-specs
if: needs.generate-sdk-api-specs.outputs.typescript_has_changes == 'true'
runs-on: blacksmith-4vcpu-ubuntu-2404
steps:
- name: Download TypeScript SDK update
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: langfuse-js-sdk-update
path: ${{ runner.temp }}/langfuse-js-sdk-update
- name: Push TypeScript SDK update
env:
BRANCH_NAME: ${{ needs.generate-sdk-api-specs.outputs.typescript_branch_name }}
GH_TOKEN: ${{ secrets.GH_ACCESS_TOKEN }}
run: |
set -euo pipefail
GIT_BIN="$(command -v git)"
GH_BIN="$(command -v gh)"
BUNDLE_PATH="$RUNNER_TEMP/langfuse-js-sdk-update/langfuse-js-api-spec-bot.bundle"
PUSH_REPO="$RUNNER_TEMP/push-langfuse-js"
mkdir -p "$PUSH_REPO"
"$GIT_BIN" -C "$PUSH_REPO" init -q
"$GIT_BIN" -C "$PUSH_REPO" fetch --no-tags "$BUNDLE_PATH" "$BRANCH_NAME:refs/heads/$BRANCH_NAME"
# Close existing api-spec-bot PRs
"$GH_BIN" pr list --repo langfuse/langfuse-js --author langfuse-bot --state open --json number --jq '.[].number' | while read -r pr_number; do
if [ -n "$pr_number" ]; then
"$GH_BIN" pr close --repo langfuse/langfuse-js "$pr_number"
fi
done
# Resolve the reviewer before pushing so a transient GitHub API failure cannot orphan a branch.
ORIGINAL_AUTHOR="$("$GH_BIN" api "repos/langfuse/langfuse/commits/${GITHUB_SHA}" --jq '.author.login // empty')"
REVIEWER_ARGS=()
if [ -n "$ORIGINAL_AUTHOR" ]; then
REVIEWER_ARGS=(--reviewer "$ORIGINAL_AUTHOR")
fi
# Push new branch and create PR from a fresh repo that never ran SDK install scripts.
GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_SYSTEM=/dev/null "$GIT_BIN" \
-C "$PUSH_REPO" \
-c credential.helper= \
-c credential.https://github.com.helper= \
-c core.hooksPath=/dev/null \
push "https://x-access-token:${GH_TOKEN}@github.com/langfuse/langfuse-js.git" "$BRANCH_NAME"
"$GH_BIN" pr create \
--repo langfuse/langfuse-js \
--head "$BRANCH_NAME" \
--title "feat(api): update API spec from langfuse/langfuse ${GITHUB_SHA::7}" \
--body "" \
"${REVIEWER_ARGS[@]}"
notify-slack-on-failure:
needs:
- generate-sdk-api-specs
- push-python-sdk
- push-typescript-sdk
if: failure()
runs-on: blacksmith-4vcpu-ubuntu-2404
permissions:
contents: read
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Notify Slack
uses: ./.github/actions/notify-slack-failure
with:
title: "❌ SDK API Spec Generation Failed"
message: "❌ SDK API spec generation failed on ${{ github.ref_name }}"
webhook-url: ${{ secrets.SLACK_CI_FAILURE_WORKFLOW_WEBHOOK_URL }}
+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@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4.36.0
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@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4.36.0
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@eb5cf3af3ac0a1aa4c9c45633dd1ae542a27a899 # v10.3.0
with:
days-before-issue-stale: 30
days-before-issue-close: 14
+146
View File
@@ -0,0 +1,146 @@
name: Storybook Preview
on:
workflow_dispatch: {}
pull_request:
types: [opened, reopened, synchronize, labeled]
permissions: {}
concurrency:
group: storybook-preview-${{ github.event.pull_request.number || github.ref_name }}
cancel-in-progress: ${{ github.event.action != 'labeled' || github.event.label.name == 'storybook' }}
jobs:
deploy:
name: Deploy Storybook preview
runs-on: blacksmith-4vcpu-ubuntu-2404
if: >-
github.event_name == 'workflow_dispatch' ||
(contains(github.event.pull_request.labels.*.name, 'storybook') &&
github.event.pull_request.head.repo.full_name == github.repository &&
(github.event.action != 'labeled' || github.event.label.name == 'storybook'))
permissions:
contents: read
issues: write
pull-requests: write
steps:
- name: Resolve pull request number
id: resolve-pr-number
if: github.event_name == 'workflow_dispatch'
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
pr_number=$(gh api -X GET "repos/${GITHUB_REPOSITORY}/pulls" -f "head=${GITHUB_REPOSITORY%%/*}:${GITHUB_REF_NAME}" -f state=open --jq '.[0].number')
if [ -z "${pr_number}" ] || [ "${pr_number}" = "null" ]; then
echo "No open pull request found for branch ${GITHUB_REF_NAME}."
exit 1
fi
echo "pr_number=${pr_number}" >> "$GITHUB_OUTPUT"
- name: Checkout pull request
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Setup pnpm
uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8
with:
version: 11.4.0
- name: Setup Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: "24"
package-manager-cache: false
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Load default env
run: cp .env.dev.example .env
- name: Build Storybook
run: pnpm --filter web run build-storybook
- name: Deploy preview to Vercel
id: publish
env:
PR_NUMBER: ${{ github.event.pull_request.number || steps.resolve-pr-number.outputs.pr_number }}
VERCEL_ORG_ID: ${{ secrets.STORYBOOK_VERCEL_ORG_ID }}
VERCEL_PROJECT_ID: ${{ secrets.STORYBOOK_VERCEL_PROJECT_ID }}
VERCEL_TOKEN: ${{ secrets.STORYBOOK_VERCEL_TOKEN }}
run: |
set -euo pipefail
pnpm install -g vercel@54.7.0
set +e
deploy_output=$(vercel deploy web/storybook-static --yes --scope langfuse 2>&1)
deploy_status=$?
set -e
if [ "${deploy_status}" -ne 0 ]; then
printf '%s\n' "${deploy_output}"
echo "Vercel deploy failed."
exit "${deploy_status}"
fi
preview_url=$(printf '%s\n' "${deploy_output}" | grep -Eo 'https://[^[:space:]]+\.vercel\.app[^[:space:]]*' | tail -n 1 || true)
if [ -z "${preview_url}" ]; then
printf '%s\n' "${deploy_output}"
echo "Failed to determine Vercel preview URL."
exit 1
fi
preview_alias="pr-${PR_NUMBER}-storybook.langfuse.vercel.app"
vercel alias set "${preview_url}" "${preview_alias}" --scope langfuse
preview_url="https://${preview_alias}"
echo "url=${preview_url}" >> "$GITHUB_OUTPUT"
- name: Comment preview link
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
env:
PREVIEW_URL: ${{ steps.publish.outputs.url }}
PR_NUMBER: ${{ github.event.pull_request.number || steps.resolve-pr-number.outputs.pr_number }}
COMMIT_SHA: ${{ github.event.pull_request.head.sha || github.sha }}
with:
script: |
const marker = "<!-- storybook-preview -->";
const body = `${marker}\nStorybook preview: ${process.env.PREVIEW_URL}\n\nUpdated at: ${new Date().toISOString()}\nCommit: ${process.env.COMMIT_SHA}`;
const { owner, repo } = context.repo;
const issue_number = Number(process.env.PR_NUMBER);
const comments = await github.paginate(github.rest.issues.listComments, {
owner,
repo,
issue_number,
per_page: 100,
});
const previousComment = comments.find(
(comment) =>
comment.user?.login === "github-actions[bot]" &&
comment.body?.includes(marker),
);
if (previousComment) {
await github.rest.issues.updateComment({
owner,
repo,
comment_id: previousComment.id,
body,
});
} else {
await github.rest.issues.createComment({
owner,
repo,
issue_number,
body,
});
}
+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:
+33
View File
@@ -0,0 +1,33 @@
---
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
uses: zizmorcore/zizmor-action@5f14fd08f7cf1cb1609c1e344975f152c7ee938d # v0.5.6
with:
advanced-security: ${{ github.event_name == 'push' && 'true' || 'false' }}
min-severity: low
+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

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