Compare commits

...
279 Commits
Author SHA1 Message Date
Valery Meleshkin 6dc0b8a94d chore: release v3.153.0 2026-02-12 19:00:24 +01:00
Valery MeleshkinandGitHub bc1bd8b3f4 chore: remove integration queue cleanup (#12008)
chore: remove cleanup
2026-02-12 16:26:46 +00:00
Hassieb PakzadandGitHub 8475328e23 feat: add events table to integration exports (#11659) (#11968) 2026-02-12 16:16:59 +01:00
36d755f994 chore(code-mirror-ui): support settings for min/max height (#11995)
* feat: limit height for eval template text area (#11993)

feat: height limited inputs for eval template

* chore: remove minHeight="none" from various components for improved flexibility

---------

Co-authored-by: Dustin Healy <54083382+dustinhealy@users.noreply.github.com>
2026-02-12 13:29:28 +00:00
Valery MeleshkinandGitHub 59320c6168 fix: increase legacy job cleanup limits for integration queues (#12004) 2026-02-12 12:52:28 +00:00
8756ea9ed0 feat(mcp): add listPrompts updatedAt range filters (#11832)
* feat(mcp): add listPrompts updatedAt datetime range filters

* fix(mcp): error if fromUpdatedAt after toUpdatedAt

---------

Co-authored-by: Nimar <l.nimar.b@gmail.com>
2026-02-12 12:46:59 +00:00
a16db15b76 fix(data-table): reset interval after manual refresh (#11970)
feat(data-table): reset interval after manual refresh

Co-authored-by: Nimar <l.nimar.b@gmail.com>
2026-02-12 10:35:24 +00:00
NimarandGitHub 930ca3b861 chore(events-table): show v4 beta toggle only on cloud (#12001)
* chore(events-table): show v4 beta toggle only on cloud

* fix defauts
2026-02-12 10:29:07 +00:00
Valery MeleshkinandGitHub c0e3343364 fix: revert hourly-key jobId in favor of removeOnFail for integration queues (#11998)
The hourly-key approach from #11988 broke jobId deduplication, causing an
ever-growing queue where each scheduler cycle added new jobs regardless of
whether previous ones had completed. Revert to static jobId (projectId +
lastSyncAt) for proper deduplication and use removeOnFail: true so failed
jobs are immediately cleaned from Redis and don't block re-queuing.

Includes a one-time migration that drains legacy hourly-key jobs on first
scheduler run after deploy.
2026-02-12 10:24:25 +00:00
Valery MeleshkinandGitHub f5d4ccb85b fix: fail fast on posthog errors to avoid HOL blocking (#11996)
fix: fail fast on posthog errors to avoid HOL
2026-02-11 18:33:52 +00:00
marliessophieandGitHub bf1ce92a51 feat(evals-ui): add clickable trace reference badge in LLM-as-a-Judge evaluation traces (#11991) 2026-02-11 15:36:56 +00:00
3e40ec1195 feat(scores): update empty state to link to scores FAQ (#11986)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Nimar <l.nimar.b@gmail.com>
2026-02-11 15:02:15 +00:00
Thorsten SpiekerandGitHub c016ddda00 chore: give labels more space and show full Yaxis label on hover (#11989) 2026-02-11 14:41:57 +00:00
2617309493 chore: clarify webhook option in dataset run experiment modal (#11897)
Add copy to the SDK/API card description explaining that users can
configure runs via webhook using the button below.

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-11 14:29:04 +00:00
Valery MeleshkinandGitHub 6b87e1bb5f fix: prevent Mixpanel/PostHog integration jobs from permanently stalling (#11988)
Add an hourly key to the BullMQ processing jobId so that failed jobs
from a previous hour don't permanently block re-queuing of the same
project. Previously, when lastSyncAt was NULL the jobId was static,
causing a single failure to deadlock the integration forever.

Also add stalling protection (lockDuration/stalledInterval/maxStalledCount)
to the Mixpanel processing worker, [MIXPANEL]/[POSTHOG] log
prefixes, try/catch around both processing pipelines, and extract cron
patterns into named constants.
2026-02-11 14:20:16 +00:00
marliessophieandGitHub d40ecb4a5c refactor: improve text truncation and wrapping in scores table cell (#11975)
chore(ui): show full lines in aggregate scores cell
2026-02-11 13:01:00 +00:00
e74d26e174 chore(home-dashboards): replace tremor with recharts (#11916)
* fix(dashboard): prevent mutation of predefined colors in getColorsForCategories

* feat(schema): add AREA_TIME_SERIES to dashboard widget chart types

* feat(widgets): add area time series chart, optional rowLimit, and Chart props

* fix(widgets): handle AREA_TIME_SERIES in DashboardWidget rowLimit

* style(ui): replace Tremor utility classes with Tailwind equivalents

* refactor(ui): replace Tremor Card and Divider in integrations and playground

* feat(dashboard): beta toggle and Recharts in legacy dashboard cards

* fix(ee): replace Tremor in BillingUsageChart

* feat(charts): improve horizontal bar chart spacing and layout

* feat(charts): time series legend above chart, scrollable and click-to-highlight

* feat(charts): time series legend right-align, solid grid, legacy-style lines

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat(charts): tooltip legacy-style layout, right-align values, compact axis and $ for cost

Co-authored-by: Cursor <cursoragent@cursor.com>

* refactor(charts): use inline chart color template literal instead of CHART_COLORS array

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(dashboard): user chart expand with bars, shared bar chart height constants

- TabsComponent: remove h-3/4 so tab content sizes to content, chart no longer compressed
- UserChart: remove flex-1 from chart wrapper so height is bar-count based
- UserChart: use same BAR_ROW_HEIGHT/CHART_AXIS_PADDING height math as TracesBarListChart

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(dashboard): fixed-height wrappers for Recharts in tabs and score analytics

- TracesTimeSeriesChart, ModelUsageChart, LatencyChart: h-80 shrink-0 so
  legend + chart get space in tab content; wrap legacy Traces chart in same
- NumericScoreTimeSeriesChart, NumericScoreHistogram, CategoricalScoreChart:
  h-80 shrink-0 so beta charts render in Scores Analytics grid

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat(charts): add configurable bar chart value labels

* feat(charts): use consistent tooltips across all recharts

* fix: scrollable legend

* refactor(charts): move dataset run charts to recharts

* chore(charts): gate toggle behind langfuse email

* feat(charts): align color palettes

* fix: don't show data point dots and don't force load chart

* fic: revert unintended

* update

* feat(charts): add subtle_fill option for recharts

* formatting

* feat(charts): remove time from charts if aggregation is by day

* chore: address PR feedback

* chore: remove dot indicators for all home dashboards

* move migration

---------

Co-authored-by: Nimar <l.nimar.b@gmail.com>
2026-02-11 14:04:30 +01:00
Valery MeleshkinandGitHub 9b531c0b8e feat: allow advanced filters on scores v2 enpoint. thus enabling (#11987)
* feat: allow advanced filters on scores v2 enpoint. thus enabling
metadata filtering.

* chore: fern
2026-02-11 10:41:00 +00:00
Valery MeleshkinandGitHub 270f03640a feat: allow observation_id in v2 scores filtering (#11974)
* feat: allow observation_id in v2 scores filtering

* chore: fern
2026-02-10 17:15:23 +00:00
marliessophieandGitHub 8840e9c504 fix(datasets-ui): update local storage key for dataset metrics to include datasetId (#11971) 2026-02-10 15:31:03 +00:00
Valery MeleshkinandGitHub 63766cf8f3 chore: temporarily remove prod-hipaa from automatic deployment (#11967) 2026-02-10 14:48:24 +01:00
CopilotGitHubcopilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>sumermanValery Meleshkin
e5144dc401 perf: persist hasAnyTrace to PostgreSQL, propagate via session to eliminate polling (#11921)
* Initial plan

* perf: optimize hasAnyTrace with conditional polling, max_threads=1, and Redis cache

- Frontend: stop refetchInterval once hasTracingConfigured is true
- Backend: add max_threads=1 to LIMIT 1 existence check (71% row reduction)
- Backend: cache positive results in Redis with 24h TTL

Co-authored-by: sumerman <222471+sumerman@users.noreply.github.com>

* fix: revert frontend refetchInterval changes that caused CI build failure

Co-authored-by: sumerman <222471+sumerman@users.noreply.github.com>

* perf: replace Redis cache with PostgreSQL hasTraces flag and propagate to frontend

- Add `has_traces` boolean column to Project model (default false, never reverted)
- hasAnyTrace checks PG flag first, skips ClickHouse if already set
- Persist positive result to PG with conditional update (only if not already set)
- Propagate hasTraces to frontend session via auth.ts and next-auth.d.ts
- Frontend pages use session flag to skip polling entirely for established projects
- Remove Redis caching from hasAnyTrace (replaced by permanent PG flag)
- Keep max_threads=1 optimization for the ClickHouse existence check

Co-authored-by: sumerman <222471+sumerman@users.noreply.github.com>

* fix: tests and FE fixes

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: sumerman <222471+sumerman@users.noreply.github.com>
Co-authored-by: Valery Meleshkin <valeriy@langfuse.com>
2026-02-10 12:44:31 +00:00
NimarandGitHub 297bacb0be chore(events-table): add mapping of tracename to trace table too temp (#11961)
chore(events-table): add mapping of tracename to trace table too temporarily
2026-02-10 11:18:31 +00:00
Hassieb PakzadandGitHub 06fd2c0552 fix(llm-connections): opus-4.6 in playground should not set top_p to -1 (#11960) 2026-02-10 11:08:21 +00:00
Max DeichmannandGitHub adb12697f1 chore: adjust snyk depl (#11958) 2026-02-10 10:50:03 +01:00
Max DeichmannandGitHub 28426c9b3c chore: improve snyk setup (#11957)
* chore: imprve snyk workflow

* chore: imprve snyk workflow

* chore: imprve snyk workflow

* chore: imprve snyk workflow

* chore: imprve snyk workflow
2026-02-10 09:34:08 +00:00
marliessophieandGitHub a8214e379a fix(datasets): resolve version timestamp from dataset_item_version column for versioned experiments (#11944) 2026-02-09 22:24:48 +00:00
marliessophieandGitHub d131bcfe5e fix(dataset-items): api version specs (#11947) 2026-02-09 20:24:25 +00:00
ClemoandGitHub cdddb7f5b8 docs: update readme 2026-02-09 10:13:58 -08:00
Hassieb PakzadandGitHub 2c7f670451 feat(model-prices): add opus-4-6 (#11940) 2026-02-09 15:24:03 +01:00
f01271ff76 fix(ui-model-prices): allow setting price keys that are substrings of existing keys (#11939)
* fix: prevent price entry deletion when typing existing key names

- Changed React key from priceIndex to the actual price key for stable rendering
- Added check to prevent overwriting existing keys when editing
- Fixes issue where typing 'input' in 'input_document' would delete the entry

Co-authored-by: Hassieb Pakzad <hassiebp@users.noreply.github.com>

* docs: add fix summary for LFE-8160

Co-authored-by: Hassieb Pakzad <hassiebp@users.noreply.github.com>

* chore: remove fix summary markdown file

Co-authored-by: Hassieb Pakzad <hassiebp@users.noreply.github.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Hassieb Pakzad <hassiebp@users.noreply.github.com>
2026-02-09 13:45:07 +00:00
NimarandGitHub b2f034d6bb chore(events-table): add traceName column (#11928)
* chore(events-table): add traceName column

* fix type

* fix build
2026-02-09 13:16:20 +00:00
25779cf2d8 feat(evals): support observation-level evals in prompt experiments (#11935)
* feat(evals): single observation evals for prompt experiments

* push

* push

* push

* push

* push

* chore(evals): fix types

* chore: gate is beta

* feat: add default ACTIVE status filter with user interaction respect

- Add defaultFilters parameter to useSidebarFilterState hook
- Apply default filter to show only ACTIVE evaluators on /evals page
- Track user interaction with useRef to respect manual "clear all" action
- Default filter reapplies on fresh page visits but not after user clears filters
- Filter is visible in UI and can be modified by users
- Clean up unused imports in inner-evaluator-form.tsx

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

* chore: build

* chore: lint

* fix: typo in prompt=experiments in seeder and internal environments

* fix: build

---------

Co-authored-by: Hassieb Pakzad <68423100+hassiebp@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-09 13:04:22 +00:00
marliessophieandGitHub a8bc224f47 feat(evals): support default filter active status (#11929)
* feat(filters): add default filter for ACTIVE evaluators in evaluator table

* fix: link to attribute propagation section
2026-02-09 10:11:17 +00:00
ff4b03c0b7 feat(evals): add support for running llm-as-a-judge on observations (#11861)
* chore: add callout

* chore: callout variants

* feat: add remapping callouts

* fixup: add remapping wizard

* fixup: allow new eval types in eval set up

* fixup: format

* chore: force sequential consistency for dual write (#11764)

* Revert "chore: force sequential consistency for dual write (#11764)"

This reverts commit 6860a0beba.

* chore: bump nextjs from 15.5.9 to 15.5.10 (#11772)

* chore: bump turbo to 2.7.6 (#11775)

* fixup: final mapping logic

* chore: has otel sdk configured trpc route

* fix(ui): standardize callout dismiss button to always use X icon

- Remove conditional "Dismiss" text button
- Always show X icon for dismiss action
- Simplify button styling to consistent h-6 w-6 size
- Action buttons remain positioned to the left of dismiss button

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

* refactor(evals): use EvalTargetObject constants and type helpers

- Add comprehensive type helper functions in typeHelpers.ts
- Replace all string literal comparisons with EvalTargetObject constants
- Add helpers: isTraceTarget, isEventTarget, isDatasetTarget, isExperimentTarget, isTraceOrEventTarget
- Update all eval components to use constants instead of hardcoded strings
- Improves type safety and prevents typos in target comparisons

Files updated:
- web/src/features/evals/utils/typeHelpers.ts (added helper functions)
- web/src/features/evals/utils/evaluator-form-utils.ts
- web/src/features/evals/components/eval-version-callout.tsx
- web/src/features/evals/components/legacy-eval-callout.tsx
- web/src/features/evals/components/remap-eval-wizard.tsx
- web/src/features/evals/components/inner-evaluator-form.tsx
- web/src/features/evals/components/evaluator-table.tsx

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

* refactor(filters): extract severity styling logic into helper function

- Create getSeverityStyles helper function to map severity levels to CSS classes
- Replace inline ternary chains with clean lookup-based styling
- Reduces complexity in the filter column rendering logic
- Improves readability and maintainability

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

* chore: push

* chore: push

* chore: push

* refactor(evals): extract synchronized scroll logic into custom hook

- Create useSynchronizedScroll hook in hooks/useSynchronizedScroll.ts
- Simplifies RemapEvalWizard by removing inline useEffect
- Reusable hook for any dual-panel synchronized scrolling
- Improves code organization and testability

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

* fix(evals): make useSynchronizedScroll hook generic for type safety

- Add generic type parameters for left and right element types
- Allows hook to work with specific HTML element types (HTMLDivElement, etc.)
- Fixes TypeScript error when passing RefObject<HTMLDivElement>

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

* chore: push

* docs: adjust wording

* fix(ui): update disabled state styling for input, select, and textarea components

- Adjusted styles to include a muted background for disabled states in Input, Select, and Textarea components.
- Ensures better visual feedback for users interacting with disabled form elements.

* docs: wording

* chore: fix eslint

* Revert "chore: has otel sdk configured trpc route"

This reverts commit 23e65e7115e0d67915d826eb86e3d7333dc115c3.

* chore: mock if otel data or not

* chore: fix links

* fix: do not support new evaluators in prompt experiments yet

* chore: lint

* fix: add filters values for event/experiment evals

* chore: lint

* fix: add trace_name filter options

* chore: persist col.id in filter builder

* chore: Extend ObservationsTable

* chore: streamline observation evaluation filters and enhance ObservationsTable integration

* chore: push

* chore: refactor observation evaluation functions and improve filter column mapping

* chore: reorganize evaluator form utilities and constants for improved clarity and functionality

* chore: implement useEvalConfigFilterOptions hook for centralized filter management in evaluator form

* chore: enhance evaluation prompt preview and variable mapping functionality with new hooks and components

* chore: lint

* fixup: url management and detail navigation

* feat: implement URL query parameter management for target changes in useEvalConfigMappingData hook

* chore: fix detail navigation

* chore: move evaluator remapping to separate page

* chore: hide eval experience behind feature switch

* chore: fix lint

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

* chore: fix

* chore: fix test

* chore: fix test

* chore: fix test

* chore: adjust typing

* chore: types

* chore: types

* chore: fix test

* chore: tests

* chore: lint

* chore: test

* chore: test

* chore: test

* chore: fix redirect

* fix: integrate observation evaluations into variable extraction logic

* fix: add name filter options to observation evaluations

* chore: lint

* feat: add default filter for ACTIVE status on evaluators table

- Add defaultFilters parameter to useSidebarFilterState hook
- Apply default filter to show only ACTIVE evaluators on /evals page
- Default filter is applied once per project and stored in localStorage
- Filter is visible in UI and can be modified by users

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

* Revert "feat: add default filter for ACTIVE status on evaluators table"

This reverts commit c0b10fe42ca58b4696881e0e4e9bb2c999cc6608.

---------

Co-authored-by: Steffen Schmitz <steffen@langfuse.com>
Co-authored-by: Nimar <l.nimar.b@gmail.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-09 09:14:11 +00:00
Valery Meleshkin 0cfffb0726 chore: release v3.152.0 2026-02-08 17:43:25 +01:00
Valery MeleshkinandGitHub fdeec9ce54 fix: make too slow error more actionable (#11923) 2026-02-08 16:38:52 +00:00
2b8701afc4 feat: add server-side ingestion masking for OTEL traces (#11906)
* feat: add server-side ingestion masking for OTEL traces

Add an enterprise feature that allows masking/redacting sensitive data
from OTEL traces before storage. Users can configure an external HTTP
callback endpoint that receives trace data and returns masked versions.

- Add ingestion masking module with configurable callback URL, timeout,
  retry logic, and fail-open/fail-closed modes
- Add reusable isEnterpriseLicenseAvailable utility in licenseCheck
- Integrate masking into OTEL ingestion queue processing
- Add environment variables for configuration
- Add unit tests for masking functionality

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

* chore: patch tests

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-07 08:34:11 +00:00
NimarandGitHub daba490c00 fix(trace-table): auto select annotation queue if there is only one (#11569) 2026-02-06 14:13:49 +00:00
NimarandGitHub 35190a308d chore(events-table): auto switch to obs view mode if there are no roo… (#11894)
chore(events-table): auto switch to obs view mode if there are no root obs
2026-02-06 13:58:03 +00:00
Valery Meleshkin fb071efb72 chore: release v3.151.0 2026-02-06 11:45:31 +01:00
Hassieb PakzadandGitHub 61fa20380b feat(observation-api): parentObservationId is null filter (#11909) 2026-02-06 11:39:58 +01:00
3d3bbf7703 fix(public-api): add trace_id to observations query filter (#11867)
* fix(public-api): add trace_id to observations query filter

Include trace_id in the clickhouse_keys CTE and IN clause filter
to improve query performance by better utilizing ClickHouse indexes.

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

* chore: release v3.150.1-0

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-06 10:10:15 +01:00
392c4d720d chore: update annotation queue helper text to reference detailed view (#11901)
fix: update annotation queue helper text to reference detailed view

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-05 16:34:52 +00:00
NimarandGitHub 32076fbb8b fix(playground): always show tool edit button (#11905) 2026-02-05 16:05:04 +00:00
NimarandGitHub a82c0312f9 chore(events-table): properly show usage and cost on top level obs (#11900) 2026-02-05 15:26:55 +00:00
NimarandGitHub 4775b8125d chore: set defaul refresh interval for events table to 1min (#11893) 2026-02-05 10:50:13 +00:00
NimarandGitHub adb45c1ba2 chore(events-table): enable log view (#11892)
* chore(events-table): enable log view

* fix hover
2026-02-05 10:37:03 +00:00
NimarandGitHub fc20a0c768 chore: bump mcp sdk to 1.26.0 (#11890) 2026-02-05 10:01:43 +00:00
4b91c51fe5 feat: add nudging for empty states tracing (#10832)
* Add links to docs in tracing filter view when key features are not being used

- done for sessions, tags, environments

* formatting

* Adjusted empty state starting screen trace view

- removed feature boxes below video
- put the get-started steps on the page directly instead of behind a button
- adjusted copy

* Adjusted Sessions empty state screen

-  aligned layout with tracing screen

* Adjust Users view empty state

- to align with other empty state views

* Updated copy of sessions empty state screen

* Enable links in info hover popup text

- Integrated ReactMarkdown for rendering descriptions in DocPopup and Popup components, allowing for formatted text and links.
- Added links to relevant info text: tags, metadata, sessions, userId, version, and release
- Updated the info hover text on the titles in the Sessions, Traces, and Users views

* fix linting errors

* addressed comments from depthfirst bot

* fix user ID link

* refactor(doc-popup): replace markdown with JSX for hover links

Remove react-markdown/remark-gfm dependency and use JSX with inline
<a> tags instead. This simplifies the codebase by avoiding the need
for a markdown parser just for rendering links in hover popups.

- Remove MarkdownContent component from doc-popup.tsx
- Update type definitions to accept ReactNode for descriptions
- Convert markdown link syntax to JSX in page headers and table tooltips

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

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-05 09:53:16 +00:00
Valery MeleshkinandGitHub 5f4ae9d29d fix: limit should be applied in v2 observations even when cursor is not specified (#11879)
fix: limit should be applied even wheno cursor is specified
2026-02-04 19:09:49 +00:00
Valery MeleshkinandGitHub 0bb97d3ca4 fix(mcp): attempting to fix unbounded mcp memory use (#11877) 2026-02-04 17:26:51 +00:00
Valery MeleshkinandGitHub f15ae605a8 fix: an attempt to allocate a little less in our web containers and workers (#11873) 2026-02-04 15:05:47 +00:00
NimarandGitHub 14aa6fc571 feat(sessions): add v4 event based session view (#11847)
* feat(sessions): add v4 event based session view

* fix query params and lint

* add saved views

* system views

* lint fix

* fix

* fix build
2026-02-04 14:56:16 +00:00
Valery MeleshkinandGitHub 56f47ec8bb fix(prompts): optimize promptsMeta query (#11862)
Replace `(name, version) IN (SELECT MAX...)` pattern with
a subquery that runs only for paginated results instead of all
prompts.
2026-02-04 08:15:57 +00:00
Valery MeleshkinandGitHub f834b40209 fix: changing index to speed up pending_deletions lookup (#11863) 2026-02-03 23:18:18 +01:00
Valery MeleshkinandGitHub 164e7afdc6 fix: batch job-exists checks in trace upsert (#11860) 2026-02-03 17:16:50 +00:00
marliessophieandGitHub 4fecc8068d feat(experiments-ui): implement dataset version resolution for experiment runs (#11646) 2026-02-03 11:27:10 +00:00
marliessophieandGitHub 966662eebc feat(dataset-versioning): implement dataset versioning support across APIs and schemas (#11695)
* chore(job-executions): add nullable col "job_input_dataset_item_valid_from"

* feat(dataset-versioning): implement dataset versioning support across APIs and schemas

- Enhanced dataset items and dataset run items APIs to accept a version parameter for retrieving historical data.
- Updated schemas to include optional dataset version fields, allowing for precise dataset item retrieval based on timestamps.
- Added validation for version parameter to ensure datasetName is provided when specified.
- Implemented tests to verify functionality of dataset versioning in API responses and experiment runs.

* fixup: support running versioned experiments in UI

* chore(dataset-versioning): add datasetItemVersion support across schemas and components

* fix(dataset-versioning): update datasetVersion handling in forms and APIs

* chore: fix typing

* fix: test

* chore: fix

* chore: push

* chore: reorder migration

* chore: rebase

* chore: fix
2026-02-03 11:16:58 +00:00
marliessophieandGitHub e254a9e3e8 chore(job-executions): add nullable col "job_input_dataset_item_valid_from" (#11692)
* chore(job-executions): add nullable col "job_input_dataset_item_valid_from"

* chore: reorder migration
2026-02-03 10:51:23 +00:00
Valery MeleshkinandGitHub 8d9fea8e2f chore: introducing BatchTraceDeletionCleaner (#11842) 2026-02-02 17:01:43 +00:00
marliessophieandGitHub 116c63e9fa chore: Revert "feat(evals): single observation evals for prompt experiments" (#11845)
Revert "feat(evals): single observation evals for prompt experiments (#11833)"

This reverts commit afbcbaf9e9.
2026-02-02 16:29:29 +00:00
Hassieb PakzadandGitHub afbcbaf9e9 feat(evals): single observation evals for prompt experiments (#11833) 2026-02-02 16:21:56 +02:00
Valery MeleshkinandGitHub 5db7c7ef36 chore: retiring mutation monitor (#11837) 2026-02-02 12:00:23 +00:00
Valery MeleshkinandGitHub 40f507c5a3 fix: reset gauges in batch retention to prevent stale metrics (#11815) 2026-01-30 18:19:19 +00:00
marliessophieandGitHub d9e5184a33 chore: Revert "feat(ui): add single observation evals" (#11819)
Revert "feat(ui): add single observation evals (#11810)"

This reverts commit fa22296abd.
2026-01-30 17:37:17 +00:00
Lotte VerheydenandGitHub 169d6c8c9d feat: add book a call notification first 7 days (#11689)
* add book a call notification first 7 days

* fix lint errors

* Changed it to a button

* added book_a_call_clicked to  events
2026-01-30 15:05:43 +00:00
Valery MeleshkinandGitHub 33da50dd5d fix: prioritize oldest items in MediaRetentionCleaner; speedup PG query. (#11813) 2026-01-30 15:46:47 +01:00
fa22296abd feat(ui): add single observation evals (#11810)
* feat(evals): add single observation evals

* push

* push

* push

* push

* push

* push

* push

* push

* push

* push

* push

* push

* push

* push

* push

* push

* push

* push

* push

* push

* push

* push

* push

* push

* push

* push

* push

* push

* psuh

* push

* chore: add callout

* chore: callout variants

* feat: add remapping callouts

* fixup: add remapping wizard

* fixup: allow new eval types in eval set up

* fixup: format

* fixup: final mapping logic

* chore: has otel sdk configured trpc route

* fix(ui): standardize callout dismiss button to always use X icon

- Remove conditional "Dismiss" text button
- Always show X icon for dismiss action
- Simplify button styling to consistent h-6 w-6 size
- Action buttons remain positioned to the left of dismiss button

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

* refactor(evals): use EvalTargetObject constants and type helpers

- Add comprehensive type helper functions in typeHelpers.ts
- Replace all string literal comparisons with EvalTargetObject constants
- Add helpers: isTraceTarget, isEventTarget, isDatasetTarget, isExperimentTarget, isTraceOrEventTarget
- Update all eval components to use constants instead of hardcoded strings
- Improves type safety and prevents typos in target comparisons

Files updated:
- web/src/features/evals/utils/typeHelpers.ts (added helper functions)
- web/src/features/evals/utils/evaluator-form-utils.ts
- web/src/features/evals/components/eval-version-callout.tsx
- web/src/features/evals/components/legacy-eval-callout.tsx
- web/src/features/evals/components/remap-eval-wizard.tsx
- web/src/features/evals/components/inner-evaluator-form.tsx
- web/src/features/evals/components/evaluator-table.tsx

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

* refactor(filters): extract severity styling logic into helper function

- Create getSeverityStyles helper function to map severity levels to CSS classes
- Replace inline ternary chains with clean lookup-based styling
- Reduces complexity in the filter column rendering logic
- Improves readability and maintainability

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

* chore: push

* chore: push

* chore: push

* refactor(evals): extract synchronized scroll logic into custom hook

- Create useSynchronizedScroll hook in hooks/useSynchronizedScroll.ts
- Simplifies RemapEvalWizard by removing inline useEffect
- Reusable hook for any dual-panel synchronized scrolling
- Improves code organization and testability

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

* fix(evals): make useSynchronizedScroll hook generic for type safety

- Add generic type parameters for left and right element types
- Allows hook to work with specific HTML element types (HTMLDivElement, etc.)
- Fixes TypeScript error when passing RefObject<HTMLDivElement>

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

* chore: push

* docs: adjust wording

* fix(ui): update disabled state styling for input, select, and textarea components

- Adjusted styles to include a muted background for disabled states in Input, Select, and Textarea components.
- Ensures better visual feedback for users interacting with disabled form elements.

* docs: wording

* chore: fix eslint

* Revert "chore: has otel sdk configured trpc route"

This reverts commit 23e65e7115e0d67915d826eb86e3d7333dc115c3.

* chore: mock if otel data or not

* chore: fix links

* fix: do not support new evaluators in prompt experiments yet

* chore: lint

---------

Co-authored-by: Hassieb Pakzad <68423100+hassiebp@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-30 12:57:44 +00:00
74a85d6a57 fix(widgets): preserve significant zeros in BigNumber component (#11600)
* fix(ui): preserve significant zeros in BigNumber component

- Extract repeated regex pattern into helper function stripTrailingDecimalZeros
- Only strip trailing zeros after decimal point, preserving integer zeros
- Fixes issue where 20000 displayed as 2K instead of 20K

* fix formatting

---------

Co-authored-by: Nimar <l.nimar.b@gmail.com>
2026-01-30 12:02:07 +00:00
f307faa53a fix(filters): escape pipe character in filter URL encoding (#11758)
* fix: escape pipe character in filter URL encoding

Fixes #11757

* merge tests

* remove comment

* also decode in filter state

---------

Co-authored-by: Nimar <l.nimar.b@gmail.com>
2026-01-30 11:47:08 +00:00
marliessophieandGitHub b9162bf507 fix(api): add CorrectionScore type and update GetScoresResponseData to include correction scores (#11807) 2026-01-30 11:14:46 +00:00
marliessophieandGitHub eb24b5a607 chore: Revert "feat(ui): single observation evals UI" (#11809)
Revert "feat(ui): single observation evals UI (#11788)"

This reverts commit 428d22ac00.
2026-01-30 10:57:27 +00:00
Valery MeleshkinandGitHub e94eeb2694 chore: add a secondary index to speed up media retention workload estimator (#11796) 2026-01-30 10:03:30 +00:00
428d22ac00 feat(ui): single observation evals UI (#11788)
* feat(evals): add single observation evals

* push

* push

* push

* push

* push

* push

* push

* push

* push

* push

* push

* push

* push

* push

* push

* push

* push

* push

* push

* push

* push

* push

* push

* push

* push

* push

* push

* push

* psuh

* push

* chore: add callout

* chore: callout variants

* feat: add remapping callouts

* fixup: add remapping wizard

* fixup: allow new eval types in eval set up

* fixup: format

* fixup: final mapping logic

* chore: has otel sdk configured trpc route

* fix(ui): standardize callout dismiss button to always use X icon

- Remove conditional "Dismiss" text button
- Always show X icon for dismiss action
- Simplify button styling to consistent h-6 w-6 size
- Action buttons remain positioned to the left of dismiss button

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

* refactor(evals): use EvalTargetObject constants and type helpers

- Add comprehensive type helper functions in typeHelpers.ts
- Replace all string literal comparisons with EvalTargetObject constants
- Add helpers: isTraceTarget, isEventTarget, isDatasetTarget, isExperimentTarget, isTraceOrEventTarget
- Update all eval components to use constants instead of hardcoded strings
- Improves type safety and prevents typos in target comparisons

Files updated:
- web/src/features/evals/utils/typeHelpers.ts (added helper functions)
- web/src/features/evals/utils/evaluator-form-utils.ts
- web/src/features/evals/components/eval-version-callout.tsx
- web/src/features/evals/components/legacy-eval-callout.tsx
- web/src/features/evals/components/remap-eval-wizard.tsx
- web/src/features/evals/components/inner-evaluator-form.tsx
- web/src/features/evals/components/evaluator-table.tsx

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

* refactor(filters): extract severity styling logic into helper function

- Create getSeverityStyles helper function to map severity levels to CSS classes
- Replace inline ternary chains with clean lookup-based styling
- Reduces complexity in the filter column rendering logic
- Improves readability and maintainability

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

* chore: push

* chore: push

* chore: push

* refactor(evals): extract synchronized scroll logic into custom hook

- Create useSynchronizedScroll hook in hooks/useSynchronizedScroll.ts
- Simplifies RemapEvalWizard by removing inline useEffect
- Reusable hook for any dual-panel synchronized scrolling
- Improves code organization and testability

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

* fix(evals): make useSynchronizedScroll hook generic for type safety

- Add generic type parameters for left and right element types
- Allows hook to work with specific HTML element types (HTMLDivElement, etc.)
- Fixes TypeScript error when passing RefObject<HTMLDivElement>

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

* chore: push

* docs: adjust wording

* fix(ui): update disabled state styling for input, select, and textarea components

- Adjusted styles to include a muted background for disabled states in Input, Select, and Textarea components.
- Ensures better visual feedback for users interacting with disabled form elements.

* docs: wording

* chore: fix eslint

* Revert "chore: has otel sdk configured trpc route"

This reverts commit 23e65e7115e0d67915d826eb86e3d7333dc115c3.

* chore: mock if otel data or not

* chore: fix links

---------

Co-authored-by: Hassieb Pakzad <68423100+hassiebp@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-30 09:45:39 +00:00
Valery MeleshkinandGitHub 287687bb5d fix: media retention should query top workloads under a lock (#11794) 2026-01-29 19:42:55 +01:00
marliessophieandGitHub a499cd60bd fix(session): open user id link in new tab (#11793) 2026-01-29 17:38:50 +00:00
NimarandGitHub aeb1933eaf chore(events-table): move obs switch (#11791) 2026-01-29 16:54:48 +00:00
NimarandGitHub fabebfb6db chore(events-table): move v4 beta toggle to postgres (#11787) 2026-01-29 14:09:03 +01:00
Valery MeleshkinandGitHub efc0747232 fix: reinstanting RedisLock and repeatable job. Away with BullMQ. (#11782)
* fix: reinstanting RedisLock and repeatable job. Away with BullMQ.

Revert "feat: move batchProjectCleaner to use BullMQ (#11504)"
This reverts commit 26ae2080d0.

* chore: refactor BatchDataRetentionCleaner and MediaRetentionCleaner to use Periodic Runner

* chore: reel in logging a little

* chore: adjust batch data retention behaviour + lock jitter

* chore: MediaRetentionCleaner should not run when redis is unavailable

* chore: tracing in periodic runners
2026-01-29 11:07:40 +01:00
Jannik MaierhöferandGitHub 0ad4d544d5 Chore(UI) rename models to model definitions in settings (#11786)
* chore(ui): rename models to model definitions in settings

* push
2026-01-29 09:47:01 +00:00
90ff7a9926 perf(frontend): reduce tree related re-renders and reduce initial bundle size (#10544)
* perf(ui): reduce initial bundle size with dynamic imports

- extract RootProvider.tsx and AnalyticsProvider.tsx for code splitting.
- Add dynamic imports and in layout.tsx and _app.tsx for heavy
dependencies
- Create MobileDrawer and ResizableDesktopLayout and use dynamic imports
for lazy loading in layout.tsx

* perf(ui): split command-menu into smaller components for optimized rendering, memoize commend menu context and CommandMenu

* perf(ui): improve INP performance of traces table opening closing sidebar with reusable ResizableDesktopLayout

- Extract ResizableDesktopLayout into reusable component with
configurable props
- Update opening closing of support drawer in layout.tsx to use new
ResizableDesktopLayout.
- Update toggling of filters sidebar in traces view to use
ResizableDesktopLayout. Mark it as a transition.

* perf(ui): lazy load posthog and PosthogProvider

* dont lazy load psthog

* clena p

* fix build

---------

Co-authored-by: Nimar <l.nimar.b@gmail.com>
2026-01-29 09:38:52 +00:00
Valery MeleshkinandGitHub 2bd7d3ae78 fix: catch a few more events read replica cases (#11783) 2026-01-28 19:03:33 +00:00
eecdfe25e9 feat: v4 beta toggle (#11767)
* feat: introduce global v4 beta toggle for all events based views

* fix: add hook and toggle

* move up

* fix: styling and naming

* fix: check correct feature flag

---------

Co-authored-by: Nimar <l.nimar.b@gmail.com>
2026-01-28 16:43:04 +00:00
Valery MeleshkinandGitHub a159cda5e3 feat: introducing preferredClickhouseService: EventsReadOnly (#11778) 2026-01-28 16:10:57 +00:00
NimarandGitHub e604f39a70 chore: bump turbo to 2.7.6 (#11775) 2026-01-28 15:08:29 +00:00
Valery MeleshkinandGitHub 8a32f6d90a fix: update order by clauses to include project_id (sometimes drastically improves perf) (#11766)
* fix: update order by clauses to include project_id (sometimes drastically improves perf)

* chore: more generic handling that covers more cases
2026-01-28 14:58:05 +00:00
NimarandGitHub d26a05963a chore: bump nextjs from 15.5.9 to 15.5.10 (#11772) 2026-01-28 14:44:19 +00:00
8d8d05bd14 feat: switch users table to events (#11747)
* cleanup + performance

---------

Co-authored-by: Nimar <l.nimar.b@gmail.com>
2026-01-28 15:20:27 +01:00
Nimar 4637152a68 chore: release v3.150.0 2026-01-28 14:51:49 +01:00
steffen911 f9ee134e3e Revert "chore: force sequential consistency for dual write (#11764)"
This reverts commit 6860a0beba.
2026-01-28 14:15:13 +01:00
Steffen SchmitzandGitHub 6860a0beba chore: force sequential consistency for dual write (#11764) 2026-01-28 12:57:33 +00:00
Hassieb PakzadandGitHub a604f8e61b feat(evals): add single observation evals (#11547) 2026-01-28 14:11:21 +02:00
NimarandGitHub eb7dc25428 fix(events-table): observation filter should show all events (#11761) 2026-01-28 10:44:13 +00:00
3934762d40 fix(readme): update LibreChat reference to correct repository (#11594)
fix: update LibreChat reference to correct repository

Update LibreChat references in all README files (en, cn, ja, kr) to point to the correct repository (danny-avila/LibreChat) with accurate star count (33,142) and proper sorting position.

Co-authored-by: Nimar <l.nimar.b@gmail.com>
2026-01-28 11:32:30 +01:00
marliessophieandGitHub 2dcead678a fix(evals): support adding filters in llm as a judge (#11760) 2026-01-28 10:06:42 +00:00
Valery MeleshkinandGitHub 5237d4b176 chore: relax BatchDataRetentionCleanerQueue limits to allow for multiple tables processing at the same time (#11746) 2026-01-27 16:08:09 +00:00
Nimar 100ad2ecff chore: release v3.149.0 2026-01-27 16:37:16 +01:00
617e396ba9 chore: add detailed logging for QueryBuilderError on invalid filter columns (#11744)
When a filter column doesn't match any UI/CH table mapping, the error log
now includes the invalid column name, filter type, and all available columns
for the table. This helps diagnose issues where users accidentally send
filters for one table to a different table's endpoint.

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-27 14:20:14 +00:00
marliessophieandGitHub d1754b8d52 fix(dataset-compare): adjust trace query to include timestamp and disable retries (#11743) 2026-01-27 13:37:29 +00:00
NimarandGitHub e4277eddcc fix(e2e-tests): preliminarily disable tests and change wait pattern (#11730)
* fix(e2e-tests): change wait pattern not to be fixed

* fix timeouts

* redirect after sign in

* wait for button to be enabled before click

* insane timeouts

* show debug

* remove clutter

* clean

* run test one after another

* wait for sign up

* fix double config

* disable E2E tests
2026-01-27 13:08:32 +00:00
NimarandGitHub 1b4cd50c66 feat(llm-as-a-judge): pre-filter langfuse evals out on creation (#11728) 2026-01-27 13:02:50 +00:00
NimarandGitHub e30920406f feat(playground): set thinking budget for vertex gemini models (#11741) 2026-01-27 14:03:20 +01:00
NimarandGitHub 94ed3a4b02 fix(tools): offer array names on widget x-axis (#11496) 2026-01-27 13:36:37 +01:00
Valery MeleshkinandGitHub fc41347f39 fix: media and batch delete queue config fixes (#11739) 2026-01-27 10:52:06 +00:00
Steffen SchmitzandGitHub 80ed179fa6 chore: enhance logging around TRPC errors (#11737)
* chore: enhance logging around TRPC errors

* chore: logging
2026-01-27 10:14:50 +00:00
Valery MeleshkinandGitHub 7259baff17 feat: implement preflight checks for delete operations in events, observations, and traces (#11723) 2026-01-27 09:43:39 +00:00
Hassieb PakzadandGitHub 68778ee4bc fix(support): create plain customer for users with missing name (#11734) 2026-01-27 08:13:02 +02:00
NimarandGitHub d1824a5e36 chore(events-table): add scores (#11727) 2026-01-26 22:33:12 +01:00
NimarandGitHub f30d6210dc chore(events-table): add trace id filter (#11720) 2026-01-26 14:43:42 +01:00
Valery MeleshkinandGitHub bedc8b1e59 chore: make CH send http progress slightly more often (#11719) 2026-01-26 13:33:43 +00:00
Valery MeleshkinandGitHub c3cc1d0a0d chore: report data retention behind cutoff as p90 (#11717) 2026-01-26 11:59:53 +00:00
Valery MeleshkinandGitHub d570372e4a chore: make CH send http progress slightly more often (#11716) 2026-01-26 11:36:35 +00:00
44825f5cb5 feat(auth): support multiple default orgs/projects for automated access provisioning (#11691)
* feat(auth): support multiple default orgs/projects for automated access provisioning

Extend LANGFUSE_DEFAULT_ORG_ID and LANGFUSE_DEFAULT_PROJECT_ID to accept
comma-separated lists of IDs, enabling automatic provisioning of new users
to multiple organizations and projects on signup.

- Update env.mjs Zod schemas to parse CSV strings into arrays
- Refactor createProjectMembershipsOnSignup to iterate over arrays
- Maintain backward compatibility with single-value configs
- Add documentation comment to .env.prod.example

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

* chore: update example

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-26 10:21:20 +00:00
Valery MeleshkinandGitHub c9aa60f3b1 fix: tweak CSV transformation stream performance (#11712) 2026-01-25 13:56:06 +00:00
Valery MeleshkinandGitHub 500d1dd0fa fix: move more logger.debug with JSON stringify under if debug (#11710) 2026-01-25 13:07:12 +00:00
Valery MeleshkinandGitHub 291b8f6420 chore: decrease data retention CH timeout (#11703) 2026-01-23 20:01:35 +00:00
Lotte VerheydenandGitHub 8174e0153e feat(support-chat): add Community Hours link with PostHog tracking (#11662) 2026-01-23 14:36:30 +01:00
Valery MeleshkinandGitHub 88da95404f fix: move early return so that pending_projects is always updates (#11679)
* fix: move early return so that pending_projects is always updates

* chore: add an oldest work item age metric

* chore: time past cutoff instead of just age

* chore: bump retention delete timeout

* fix: use the corret lower bound for reduce
2026-01-23 11:46:37 +00:00
NimarandGitHub dcee2e9e39 feat(dx): use tsgo for typechecking + parallel build command (#11682) 2026-01-22 20:21:12 +01:00
NimarandGitHub cbd07b3d73 fix(CI): only release tagged versions as docker images on hub (#11653) 2026-01-22 20:16:54 +01:00
NimarandGitHub c5467ef614 fix(events-table-ui): move trace observation table to header (#11680)
* fix(events-table-ui): move trace observation table to header

* add toggle

* move it
2026-01-22 17:59:10 +00:00
NimarandGitHub e8d841ab43 fix(events-table): show session id and user id if available (#11678)
* fix(events-table): show session id and user id if available

* fix lint
2026-01-22 15:36:37 +00:00
marliessophieandGitHub 4d0b5372e5 feat(folders): add breadcrumb navigation for prompt and dataset detail pages (#11676) 2026-01-22 13:38:40 +00:00
NimarandGitHub 04dce2d7d9 feat(trace): add experiment filters for event table (#11673) 2026-01-22 14:40:49 +01:00
Valery MeleshkinandGitHub 9e47e12b6c chore: remove subquery from deleteEventsByTraceIds (#11677) 2026-01-22 13:33:56 +00:00
Valery MeleshkinandGitHub f5328bc018 chore: use retention condition when estimating workload (#11674)
* chore: use retention condition when estimating workload

* chore: use hashes instead of {i}

* chore: cache hashes
2026-01-22 13:05:52 +00:00
NimarandGitHub 3cc6da2ff4 chore: bump lodash to 4.17.23 (#11672) 2026-01-22 11:09:34 +01:00
Steffen SchmitzandGitHub 662e202041 perf: refactor lodash imports for potentially more effective tree-shaking (#11671)
chore: refactor lodash imports for potentially more effective tree-shaking
2026-01-22 09:49:01 +00:00
6e6606f06e feat(IOPreview): add footer rendering for corrected output when corrections are enabled (#11543)
* feat(IOPreview): add footer rendering for corrected output when corrections are enabled

* style: padding

* chore: push

* chore: fix format

* chore: lint

* fix: remove unused JsonSection import

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

---------

Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-22 09:35:44 +00:00
NimarandGitHub 99562dbc6d feat(trace): events based observation/trace table (#11519)
* feat(trace): events based observation/trace table

* fix filter propagation

* add trace detail view

* only show to lf users

* add truncation

* full tilt

* fixplayground button

* add missing hooks

* no trace as root

* roots are arrays
2026-01-22 09:26:25 +00:00
Valery MeleshkinandGitHub 60b5706c18 fix: avoid potentialy expensive JSON.stringify when debug logging is disabled (#11661) 2026-01-21 18:40:48 +00:00
Valery MeleshkinandGitHub fb875bf26e fix(eval): yield from the loop inside createEvalJobs (#11660) 2026-01-21 16:26:47 +00:00
Nimar 695a2c01bc chore: release v3.148.0 2026-01-21 09:25:40 +01:00
Hassieb PakzadandGitHub 414c865fac fix(json-path): parse primitive strings in json root (#11654) 2026-01-20 19:40:32 +00:00
NimarandGitHub 3bf32dd981 fix(playground): allow saving new prompts after editing in playground (#11648)
* fix(playground): allow saving new prompts after editing in playground

* fix race condition

* simplify
2026-01-20 17:55:49 +00:00
NimarandGitHub 2e58620f6e fix(app): fix scrolling behavior in chrome v144 and later (#11652) 2026-01-20 17:54:52 +01:00
Valery MeleshkinandGitHub 197125b4de fix: allow using commentCount filter in batch export (#11650) 2026-01-20 16:22:51 +00:00
Valery MeleshkinGitHubellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
cbd66df315 feat: an alternative to retention queue: batch-oriented periodic jobs. (#11644)
* feat: an alternative to retention queue: batch-oriented periodic jobs.

* Update worker/src/features/batch-data-retention-cleaner/index.ts

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>

* chore: validate that we only operate on supported tables

* fixing a similar potential security issue for BATCH_DELETION_TABLES

---------

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
2026-01-20 15:30:49 +00:00
NimarandGitHub 7681971dfa chore: upgrade MCP to 1.25.3 (#11645) 2026-01-20 15:08:17 +00:00
NimarandGitHub bb4cb1cb50 fix(codemirror): allow scrolling entire page with editor on chrome 144+ (#11642) 2026-01-20 14:14:02 +00:00
marliessophieandGitHub 3695c70b92 chore(corrections): enhance CorrectedOutputField with strict JSON mode and improved display logic (#11570)
* chore(corrections): enhance CorrectedOutputField with strict JSON mode and improved display logic

* chore: lint
2026-01-20 14:08:50 +00:00
marliessophieandGitHub 9cbebbf4a0 fix(scores-table-cell): add copy to clipboard functionality and fix scroll-behaviour (#11616)
* fix(scores-table-cell): add copy to clipboard functionality and fix scroll-behaviour

* fix(scores-table-cell): prevent event propagation in copy to clipboard handler
2026-01-20 13:44:38 +00:00
NimarandGitHub 8abc89b53f chore: bump jsdiff from 7.0.0 to 8.0.3 (#11639) 2026-01-20 13:26:01 +00:00
NimarandGitHub 9667816ae0 fix(trace): dont show empty string on only tool call + thinking strings (#11641) 2026-01-20 13:21:38 +00:00
NimarandGitHub 246e844450 chore: bump release bumper deps (#11638) 2026-01-20 12:46:38 +00:00
ca4fb68b53 fix(storage): add max file limit to GCS and Azure listFiles (#11637)
Apply the same LANGFUSE_S3_LIST_MAX_KEYS limit (default: 200) to Google
Cloud Storage and Azure Blob Storage listFiles methods for consistency
with S3 implementation. This prevents potential resource exhaustion when
listing large numbers of files.

Closes #11394

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-20 12:37:01 +00:00
02a83085e9 chore(worker): consolidate ClickhouseWriter drop logs into single summary (#11636)
Replace per-record "Max attempts reached, dropping record" log messages
with a single summary log showing the total count of dropped records.
This reduces log noise while maintaining the same error metric for alerting.

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-20 12:24:35 +00:00
NimarandGitHub d9d9bc78a0 fix(trace): render thinking also on only tool calls (#11634)
* render thinking on only tool call

* add test
2026-01-20 10:54:53 +00:00
NimarandGitHub 90cf22da84 fix(prompts): draft button overlap (#11633) 2026-01-20 11:15:34 +01:00
Steffen SchmitzandGitHub 0147836b41 chore: increase event backfill dual write timeout to 10min (#11623) 2026-01-19 19:45:39 +01:00
marliessophieandGitHub ed11164d96 chore(annotation-queue-ui): simplify button labels (#11548)
* chore(annotation-queue-ui): simplify button labels

* chore: push
2026-01-19 15:56:20 +00:00
marliessophieandGitHub aa2369c4cb docs(corrections): link to docs from heading (#11571) 2026-01-19 15:37:04 +00:00
5ac1a18430 feat: add org audit log viewer (#11529)
* feat: add organization-level audit logs viewing

Organization-level changes (org CRUD, project CRUD, membership changes)
were being logged to the database but had no UI or API to view them.

This change adds:
- `auditLogs:read` scope to organization access rights (OWNER, ADMIN)
- `allByOrg` tRPC endpoint in auditLogsRouter for org-level audit logs
- OrgAuditLogsTable component for displaying org-level audit logs
- OrgAuditLogsSettingsPage component with entitlement/access checks
- "Audit Logs" tab in organization settings (visible with audit-logs entitlement)

Organization audit logs show changes where projectId is null, including:
organization create/update/delete, project create/delete/transfer, and
organization membership changes.

* refactor(audit-logs): unify project and org audit log tables

Consolidate OrgAuditLogsTable into AuditLogsTable using discriminated
union props to support both project and organization scopes. This
removes ~130 lines of duplicate code while preserving all functionality.

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

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Steffen Schmitz <steffen@langfuse.com>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-19 15:33:45 +00:00
marliessophieandGitHub 7c26edfb1e style(experiments-ui): enhance PromptModelStep to display truncated labels (#11546) 2026-01-19 15:16:48 +00:00
Steffen SchmitzandGitHub ced2fd3a40 chore: remove root core binary (#11622) 2026-01-19 17:44:12 +01:00
Hassieb PakzadandGitHub 7d328b404e chore(claude-code): CC should prefer params objects when writing funcs (#11620) 2026-01-19 18:07:13 +02:00
NimarandGitHub 3f77e7a091 fix(prompts): save editor state to sessionStorage and restore on remount (#11618) 2026-01-19 16:37:57 +01:00
NimarandGitHub 4e02088135 feat(trace): render thinking / reasoning parts in trace detail (#11615) 2026-01-19 15:26:39 +01:00
NimarandGitHub 24e165d595 chore: bump turbo from 2.7.2 to 2.7.5 (#11617) 2026-01-19 14:08:37 +00:00
Hassieb PakzadandGitHub 212e88525e fix(json-path): return all results from array slice syntax (#11568) 2026-01-19 09:31:52 +00:00
steffen911 b520cad77b chore: release v3.147.0 2026-01-15 11:05:14 +01:00
NimarandGitHub 8fd5e5bfc1 fix(trace): ensure JSON expansion state is honoured across traces (#11518)
* fix(trace): ensure JSON expansion state is honoured across traces

* add disclaimer

* add fixes

* fix expansion state handling

* fix types
2026-01-15 09:40:23 +00:00
Steffen SchmitzandGitHub 3adc89e4d7 fix(slack): force project auth on slack install endpoint (#11566) 2026-01-15 08:57:34 +01:00
24195bb369 feat: cursor-based sequential processing for event propagation (#11561)
* feat: cursor-based sequential processing for event propagation

Replace lock-based parallel partition processing with cursor-based
sequential processing for the event propagation job:

- Track last processed partition in Redis cursor
- Process partitions sequentially in chronological order
- Rely on ClickHouse table TTL (12h) for partition cleanup instead of
  explicit DROP PARTITION calls
- Enforce global concurrency of 1 in queue configuration
- Remove lock functions and multi-job scheduling

This improves debuggability by keeping data in observations_batch_staging
for 12 hours, allowing verification of the data pipeline when issues occur.

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

* chore: increase buffer before partition processing

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-15 07:38:21 +00:00
marliessophieandGitHub 69c196c42d chore(sessions): enhance TraceRow component with annotation queue item creation functionality (#11560) 2026-01-14 18:42:34 +00:00
marliessophieandGitHub 5d319e1fe3 feat(corrections): support diff viewer between actual vs. corrected output (#11556)
* feat(corrections): add corrected vs actual output diff

* chore(corrections): enhance editing state management and auto-save functionality in CorrectedOutputField
2026-01-14 18:34:45 +00:00
Hassieb PakzadandGitHub c0120f7984 fix(events-ingestion): add trace_name to direct writes (#11559)
* fix(events-ingestion): add trace_name to direct writes

* push
2026-01-14 16:55:54 +00:00
Steffen SchmitzandGitHub 535bc2d2d7 chore: reduce event dual write timestamp buffer from 3.5min to 2min (#11552)
* chore: reduce event dual write timestamp buffer from 3.5min to 2min

* Update comment for timestamp validation logic
2026-01-14 15:31:43 +00:00
NimarandGitHub a5db5aa940 chore: forbidden on excessive chatcompletion use (#11555)
* chore: forbidden on excessive chatcompletion use

* move to env.mjs
2026-01-14 14:48:23 +00:00
Jannik MaierhöferandGitHub f79a5cc52f feat(otel): support mapping of custom trace_id for litellm (#11553) 2026-01-14 15:43:17 +01:00
Valery MeleshkinandGitHub 44a8487410 chore: LANGFUSE_BATCH_PROJECT_CLEANER_SLEEP_ON_EMPTY_MS bump (#11554) 2026-01-14 13:48:05 +00:00
Valery MeleshkinandGitHub a16f4a0526 fix: fixing bullmq config for BatchProjectDelete. removing accidentally commited dataModel changes. (#11551) 2026-01-14 12:08:54 +00:00
Max DeichmannandGitHub 60183f48fc chore: add span attributes for session span (#11549) 2026-01-14 12:13:24 +01:00
NimarandGitHub 2efaace4a8 fix(filters): date picker time adjustments don't change value (#11531) 2026-01-13 19:35:11 +00:00
marliessophieandGitHub ca24bebccd chore(ui): show 1pass modal only for sign-up/sign-in input fields (#11530) 2026-01-13 18:29:07 +00:00
Valery MeleshkinandGitHub b7bb9eefe6 feat: extend BatchProjectCleanerJob to handle dataset run items (#11528) 2026-01-13 17:44:37 +00:00
marliessophieandGitHub f2125ddc43 chore(annotation-queue-ui): add onOpenChange callback for smooth scrolling behavior when the dropdown opens (#11527) 2026-01-13 17:33:29 +00:00
dc2789af31 fix(score-analytics): include source type in distribution chart legends (#11458)
Add source type (EVAL, ANNOTATION, API) to score names in distribution
chart legends to distinguish scores with identical names. This matches
the existing behavior in timeline charts and prevents confusion when
comparing e.g. 'friendliness (EVAL)' vs 'friendliness (ANNOTATION)'.

Co-authored-by: Nimar <l.nimar.b@gmail.com>
2026-01-13 16:17:59 +00:00
marliessophieandGitHub 39855914b5 chore(dataset-item-events): drop unused tables and sys_id col (#11136)
* chore(dataset-items): drop sys_id col

* chore(dataset-item-events): drop foreign key constraint from dataset_item_events

* chore(dataset-item-events): remove DatasetItemEvent model and associated migration

* chore: reorder migrations
2026-01-13 15:49:36 +00:00
marliessophieandGitHub a2b3ab9b93 feat(corrections): add global toggle for showing corrections in session and IOPreview components (#11522) 2026-01-13 14:32:42 +00:00
marliessophieandGitHub 2cdf13dc0d feat(corrections): add JSON validation toggle (#11520)
* feat(corrections): add JSON validation toggle

* chore(corrections): re-run validation if correction format changes

* chore: lint
2026-01-13 13:39:59 +00:00
Valery MeleshkinandGitHub 924d653993 fix(api): fix score API environment filtering for session scores (#11515) 2026-01-13 11:53:51 +00:00
Hassieb PakzadandGitHub e99b3a1e0e fix(otel-ingestion-ai-sdk): parse usage from both ai.usage and providerMetadata (#11498)
* fix(otel-ingestion-ai-sdk): parse usage from both ai.usage and providerMetadata

* push
2026-01-13 11:13:05 +02:00
Valery MeleshkinandGitHub 26ae2080d0 feat: move batchProjectCleaner to use BullMQ (#11504) 2026-01-12 15:46:46 +00:00
Valery MeleshkinandGitHub cd7f1b419f fix: prevent retention configuration drift when queues are backlogged (#11502) 2026-01-12 15:05:52 +00:00
marliessophieandGitHub e835a9758e fix: add search for prompt version dropdown (#11500) 2026-01-12 14:39:58 +00:00
marliessophieandGitHub 3d460b71f4 style: display message for non-annotation scores in AnnotationDrawer (#11497) 2026-01-12 14:12:43 +00:00
NimarandGitHub e33c6b42c9 fix(trace): add beta toggle instead of tab (#11495)
* fix(trace): add beta toggle instead of tab

* add hook

* fix defaults
2026-01-12 13:19:14 +00:00
marliessophieandGitHub 80e441a211 fix: update dataset name duplication logic to append "(copy)" (#11494) 2026-01-12 12:22:20 +00:00
Valery MeleshkinandGitHub 6cbb93019e fix: deleteEventsByTraceIds should respect global deletion timeout (#11491) 2026-01-12 09:58:38 +00:00
marliessophieandGitHub 7f448db0ac fix: return empty string for correction data type (#11490) 2026-01-12 09:00:42 +00:00
Valery MeleshkinandGitHub 5fae64e377 fix: make BatchProjectCleaner start conditional for events table (#11483) 2026-01-09 18:30:27 +00:00
NimarandGitHub 7aa0c4995c fix: posthog init (#11482) 2026-01-09 19:07:03 +01:00
Valery MeleshkinandGitHub a8047b629b fix: make BatchProjectCleaner start conditional for events table (#11480) 2026-01-09 17:06:10 +00:00
NimarandGitHub ae6acee9c9 fix: posthog reset (#11481) 2026-01-09 17:57:24 +01:00
NimarandGitHub 1d0567426b fix(tables): small row height spacing equalized (#11478) 2026-01-09 16:47:23 +00:00
Valery MeleshkinandGitHub d2deef409a feat: add BatchProjectCleaner as an optimization when multiple project deletions are pending (#11476)
* feat: add BatchProjectCleaner as an optimization when multiple project
deletions are pending.

* chore: add PeriodicRunner abstract base class for periodic task
execution

* chore: adjusting MutationMonitor for project deletion.

* chore: extracting RedisLock utility; lock ownership fix.
2026-01-09 15:00:12 +00:00
NimarandGitHub 240d4d7def chore: add sign up event (#11473) 2026-01-09 15:39:38 +01:00
Steffen SchmitzGitHubClaude Opus 4.5depthfirst-app[bot] <184448029+depthfirst-app[bot]@users.noreply.github.com>
99ffc45173 fix(api): make retention optional and fix metadata handling in update project (#11442)
* fix(api): make retention optional and fix metadata handling in update project

- Make retention field optional in update project API to retain existing
  setting when omitted
- Fix metadata spreading to only apply when defined, preventing null
  overwrites
- Update Fern API spec and OpenAPI documentation

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

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

* Update web/src/ee/features/admin-api/server/projects/projectById/index.ts

Co-authored-by: depthfirst-app[bot] <184448029+depthfirst-app[bot]@users.noreply.github.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: depthfirst-app[bot] <184448029+depthfirst-app[bot]@users.noreply.github.com>
2026-01-09 13:20:06 +00:00
Steffen SchmitzGitHubellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
c6207c0f62 fix: ensure correct numeric handling of usage_details values instead of concatenation (#11472)
* fix: ensure correct numeric handling of usage_details values instead of concatenation

* Update worker/src/services/IngestionService/tests/calculateTokenCost.unit.test.ts

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>

---------

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
2026-01-09 13:18:15 +00:00
Steffen SchmitzandGitHub 8499a38958 fix: enforce plan limits for org member invites (#11455)
* fix: enforce plan limits for org member invites

* chore: lint fix
2026-01-09 13:09:02 +00:00
Max Deichmann 6f221302b7 chore: release v3.146.0 2026-01-08 20:50:45 +01:00
Max DeichmannandGitHub f6ae7c5cd4 chore: remove erd generator (#11459) 2026-01-08 19:14:49 +01:00
Valery MeleshkinandGitHub 43b271ea4c chore: double-check that a project still exists before starting potentially expensive DELETE (#11456)
* chore: double-check that a project still exists before starting potentially expensive DELETE

* chore: appling the same pre-flight SELECT to data retention queries

* fix: use feature flag for event-based test

* fix: CI for worker should be able to test event tables
2026-01-08 17:21:58 +00:00
b4fe4529d9 chore(llm-connection): allow global bedrock anthropic models (#11457)
* fix: update match patterns to include global region for Claude models

* fix: add missing newline at end of default-model-prices.json

* fix: removed global prefix from regex for models without global inference support

* fix: add missing newline at end of default-model-prices.json

---------

Co-authored-by: Nimar <l.nimar.b@gmail.com>
2026-01-08 16:39:11 +00:00
Valery MeleshkinandGitHub 725a88f5ae chore: remove mutaiton waiter code path (#11454) 2026-01-08 15:56:22 +01:00
NimarandGitHub 1b2a2c01fa chore: bump preact to 10.28.2 (#11446)
chore: bump preact
2026-01-08 09:30:59 +00:00
NimarandGitHub ae0857494b fix(playground): empty values on playground jump for microsoft semant… (#11434)
* fix(playground): empty values on playground jump for microsoft semantic kernel
instrumentation

* add file

* add test

* filter

* fx
2026-01-07 21:36:55 +00:00
Valery MeleshkinandGitHub c278bc0ca1 fix: use system.processes in async delete monitoring together with query_log (#11435) 2026-01-07 21:44:18 +01:00
Valery MeleshkinandGitHub 5571ae376a fix: fixes for delete query tracking (#11431) 2026-01-07 18:47:27 +01:00
c338b0409e fix: escape CSV headers to handle commas and special characters (#11430)
Refactored the CSV field escaping logic into a reusable `escapeCsvField`
utility function that properly escapes double quotes and wraps fields.
Applied this function to both headers and body rows, fixing an issue
where headers containing commas would break CSV parsing.

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

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-07 17:39:10 +00:00
Steffen SchmitzandGitHub 95394b39aa chore: revert cookie workaround after auth misconfiguration (#11427) 2026-01-07 16:27:14 +00:00
Valery MeleshkinandGitHub 1ebc413df4 feat(api): allow hight cardinality measures in v2/metrics when its topN (#11243)
* feat(api): allow hight cardinality measures in v2/metrics when its topN

* chore: getting rid of preflight in favor of using
max_bytes_before_external_group_by
2026-01-07 14:47:36 +01:00
Valery MeleshkinandGitHub 12733ff7da fix: a workaround for socket hangup issues in our delete pipeline. (#11408)
* fix: a workaround for socket hangup issues in our delete pipeline.

* chore: re-implement based on waiting for queryId
2026-01-07 14:22:18 +01:00
41f064cb0f fix(api-docs): sync Fern API types with TypeScript definitions (#11421)
* fix(api-docs): sync Fern API types with TypeScript definitions

- Update fern/apis/server/definition/commons.yml to match TypeScript types
- Add source file references to each Fern type definition
- Fix nullable vs optional type mappings:
  - .nullable() → nullable<T>
  - .nullish() → optional<nullable<T>>
  - .optional() → optional<T>
  - Always present fields → T (not optional)
- Update backend-dev-guidelines skill with Fern API sync guidelines
- Add API Documentation section to REVIEW.md

Closes #11232

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

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

* chore: patch

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-07 12:05:15 +00:00
0e23a857c6 feat: add CLICKHOUSE_ASYNC_INSERT_BUSY_TIMEOUT_MIN_MS setting (#11420)
Add configurable async_insert_busy_timeout_min_ms for ClickHouse client.
The setting is optional and when provided must be >= 50ms.

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

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-07 10:47:13 +00:00
Nimar c77890c52a chore: release v3.145.0 2026-01-07 10:45:45 +01:00
Valery MeleshkinandGitHub fa072d0074 feat(api): allow selective expansion of metadata on observations-v2 endpoints (#11416) 2026-01-07 09:41:53 +01:00
0777afcd05 fix(auth): assign default org/project memberships for SSO users with existing accounts (#11413)
When users log in via SSO (e.g., Keycloak) with allowDangerousEmailAccountLinking
enabled, existing users were not being assigned to the default org/project because
createProjectMembershipsOnSignup was only called from createUser, not linkAccount.

This fix:
- Changes all prisma.create() calls to prisma.upsert() with update: {} to make
  the function idempotent and preserve existing roles
- Calls createProjectMembershipsOnSignup from linkAccount so SSO users with
  pre-existing accounts get default memberships assigned

Fixes #10907

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

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-06 16:56:41 +00:00
NimarandGitHub dba9fe80d1 fix(comments): dont expose inline data path on public api yet (#11415) 2026-01-06 17:59:37 +01:00
NimarandGitHub a2e086d7e6 fix(comments): make migration distinct (#11414) 2026-01-06 17:05:33 +01:00
NimarandGitHub 57537ad59e feat(trace): allow trace comments inline on fractions of IO data (#11171) 2026-01-06 17:02:21 +01:00
Valery MeleshkinandGitHub 07e863f44a fix: revert "fix: reverting get rid of extra IN clauses along trace and score deletion paths " (#11401)
Seems to make no real the difference to performance. Reverting my canry-revert to at least get #11101 fixed.

This reverts commit 9fe60a2971.
2026-01-06 13:12:31 +01:00
NimarandGitHub c50af94e91 fix(playground): enable parsing for double stringified msg array (#11400) 2026-01-06 12:05:56 +01:00
Steffen SchmitzandGitHub 22dade8808 chore: remove noisy debug log (#11398) 2026-01-06 10:38:09 +00:00
Nimar 41e6015580 chore: release v3.144.0 2026-01-06 11:04:54 +01:00
NimarandGitHub c01bd573bb fix(seeder): add default value for scores (#11397) 2026-01-06 11:03:52 +01:00
f45bc5ffe8 fix(security): encrypt blob storage secretAccessKey in public API (#11395)
Previously, the public API endpoint for blob storage integrations stored
secretAccessKey in plaintext, while the tRPC endpoint correctly encrypted it.

Changes:
- Encrypt secretAccessKey before storing in public API endpoint
- Add background migration to encrypt existing unencrypted secrets
- Add test verifying encryption works correctly

The background migration detects unencrypted values by attempting to decrypt
them - if decryption fails with "Invalid or corrupted cipher format", the
value is unencrypted and needs encryption. This is reliable because cloud
provider secrets (AWS/Azure/GCP) never contain colons, which are required
in the encrypted format (iv:encrypted:authTag).

Closes INT-372

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

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-06 09:40:38 +00:00
Steffen SchmitzandGitHub 3224365277 chore: upgrade golang-migrate to 4.19.1 (#11390) 2026-01-05 17:16:24 +00:00
NimarandGitHub 6ec4eb4cf5 feat(filters): add clear all button (#11387)
* Refactor: Add clear all filters button and tooltip

Co-authored-by: nimar <nimar@langfuse.com>

* use global tooltip provider
2026-01-05 17:46:36 +01:00
dacfb3b86e feat(traces): add refresh button for manual and periodic refresh (#11276)
* feat(traces): add refresh button for manual and periodic refresh

* use react-query pattern + add to observations table

* recalc date range on tick

* sanity check values

---------

Co-authored-by: Nimar <l.nimar.b@gmail.com>
2026-01-05 16:16:36 +01:00
Max DeichmannandGitHub 8f1b7e3db7 chore: add alpine image documentation (#11376) 2026-01-04 13:30:41 +01:00
NimarandGitHub 3570bbb22f chore: clean up error naming after eslint upgrade (#11371) 2026-01-02 23:06:09 +00:00
Max DeichmannandGitHub 5593d83f5b chore: adjust logger levels (#11370) 2026-01-02 16:52:49 +01:00
Max DeichmannandGitHub c64a9ba535 chore: add clickhouse logger (#11369) 2026-01-02 13:28:55 +01:00
Hassieb PakzadandGitHub 994b96e687 chore: revert qs and @modelcontextprotocol/sdk upgrade (#11364) 2026-01-02 00:19:15 +01:00
Max DeichmannandGitHub 23f722bf7b chore: add score v2 docs (#11356) 2026-01-01 18:47:33 +01:00
Max DeichmannandGitHub d59b6a3b62 perf: add fields api for scores v2 api (#11352) 2026-01-01 17:44:02 +01:00
Max DeichmannandGitHub 1857ae9d1e chore: upgrade playwright (#11354)
* chore: upgrade playwright

* chore: upgrade playwright
2026-01-01 15:39:50 +00:00
NimarandGitHub 9d4d99010c chore: bump lodash to 4.17.21 (#11353) 2026-01-01 14:04:44 +00:00
Max DeichmannandGitHub 1d3db1a6de chore: revert qs upgrade (#11351) 2026-01-01 13:15:18 +00:00
Max DeichmannandGitHub 1c629b9c0b chore: enable logs level trace for deletions (#11349) 2026-01-01 10:06:24 +00:00
9c799dfb33 fix(worker): improve test isolation for StorageService dependent tests (#11339)
fix(worker): improve test isolation for StorageService dependent tests by prexing files with a random value and then deleting all files with that value once the test finishes.

Co-authored-by: Nimar <l.nimar.b@gmail.com>
2025-12-31 14:01:22 +00:00
NimarandGitHub 009a932945 chore: bump qs to 6.14.1 (#11344)
* chore: upgrade turbo to 2.7.2

* Update pnpm-lock.yaml

* chore: bump qs to 6.14.1
2025-12-31 13:48:17 +00:00
NimarandGitHub 0b2cc19d92 chore: upgrade turbo to 2.7.2 (#11343)
* chore: upgrade turbo to 2.7.2

* Update pnpm-lock.yaml
2025-12-31 12:55:12 +00:00
NimarandGitHub 64bd529c98 chore: upgrade eslint to v9 (#11327) 2025-12-31 13:34:48 +01:00
Max DeichmannandGitHub 9fc7c990aa chore: reduce remainder from betterstack (#11330)
* chore: reduce remainder from betterstack

* chore: reduce remainder from betterstack
2025-12-29 20:14:33 +00:00
Max DeichmannandGitHub afbeb507db chore: adjust csp for azure-ad (#11331)
* chore: reduce remainder from betterstack

* chore: reduce remainder from betterstack
2025-12-29 19:53:48 +00:00
NimarandGitHub 86af0efae7 fix(prompts): don't throw clientside on empty labels (#11328)
* fix(prompts): don't throw clientside on empty lables

* catch earlier
2025-12-29 13:21:56 +00:00
NimarandGitHub be6ce949d6 chore: upgrade jest to v30 (#11273)
* chore: upgrade jest to v30

* fix paths

* make tests parallel again

* update

* clean
2025-12-29 09:27:52 +00:00
marliessophieandGitHub 09986a745a feat(corrections): add corrections to trace and observation preview (#11313)
* feat(corrections): add corrections to trace and observation preview

* chore: lint

* chore: rename variable

* chore: implement getMostRecentCorrection utility and update previews

* chore: push

* fix: upsert correction

* chore: push

* chore: push

* chore: push

* chore: push

* chore: add projectId, traceId, and environment props to various components for enhanced trace context

* chore: push

* chore: revert

* refactor: simplify state management in CorrectedOutputField and improve JSON error handling

* chore: remove outputCorrection from row count calculations in IOPreviewJSON and adjust scores router schema

* chore: access

* refactor: remove isSaving state from correction hooks and update cache handling
2025-12-29 09:10:29 +00:00
f1d6c26276 security: PostHog SSRF validation (#11311)
* feat: Add SSRF protection for PostHog hostname

Co-authored-by: max <max@langfuse.com>

* Refactor PostHog integration tests and add hostname validation

Co-authored-by: max <max@langfuse.com>

* Fix: Remove port from PostHog hostname in tests

Co-authored-by: max <max@langfuse.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
2025-12-24 11:14:51 +00:00
Max Deichmann 1515a49d53 chore: release v3.143.0 2025-12-23 23:52:10 +01:00
Max DeichmannandGitHub 0cec22ba56 security: upgrade langchain core (#11303)
* security: upgrade langchain

* security: upgrade langchain
2025-12-23 22:34:08 +00:00
NimarandGitHub dc23fa1d68 fix(annotation-queue): correctly parse IO in embeded viewers (#11298)
* fix(annotation-queue): correctly parse IO in embeded viewers

* fix double parsing
2025-12-23 21:51:23 +00:00
Max DeichmannandGitHub 7d7ec1ed6c security: upgrade langchain (#11302) 2025-12-23 21:08:30 +00:00
Valery MeleshkinandGitHub d18d2a0de9 fix: link to v2 endpoints from v1 (#11297) 2025-12-23 17:04:29 +00:00
marliessophieandGitHub 51fd0edf80 fix(trace-preview): optimistically populate annotation scores in trace tree (#11296)
- Updated Trace, TracePreview, and other components to use serverScores instead of scores for clarity.
- Introduced mergedScores in TraceDataContext for better score management.
- Adjusted related components to ensure consistent data handling across the application.
2025-12-23 16:17:27 +00:00
marliessophieandGitHub a9621b2673 chore: add long string value column to scores table (#11233)
* chore(prisma): rename ScoreDataType to ScoreConfigDataType and update related schema and types

* chore: update score config types

* chore: adjust score types for use cases

* fixup: adjust score types for use cases

* chore: push

* chore: push

* chore: push

* chore: push

* chore: push

* chore(corrections): add `long_string_value` to `scores` table

* fix(migrations): change `long_string_value` column type from Nullable(String) to String in scores table

* chore: add correction type definition and schema

* chore: add correction type to public API

* chore: adjust score types for use cases

* chore: add scores tests for corrections on scores v2 API

* chore: update ingestion and aggreation types

* tests: scores v1 and v2 API

* chore: update API types

* feat: enhance score type handling with data type filtering

* feat: read aggregate score types only by default

* feat: exclude CORRECTION scores in v1 and include in scores v2

* fixup: read aggregate score types only by default

* chore: push

* chore: push

* chore: types

* chore: types

* chore: types

* chore: reorder migrations

* chore: types

* chore: types

* fix: prevent association of CORRECTION scores with sessions and dataset runs

* fixup: test

* fix: test

* chore: schema

* chore: build

* chore: test

* chore: never return long_string_value but string_value for corrections

* test: add

* chore: converter

* chore: test

* fix: API return types

* chore: override correction scores to reference output

* chore: rename

* chore: rm test
2025-12-23 14:32:54 +00:00
99ff3073b6 feat(advanced-json-viewer): fully virtualized JSON Beta view with search and media attachments (#11253)
* feat: add adaptive virtualization and fix scroll handling for JSON Beta view

- Add virtualization threshold (2500 rows) to IOPreviewJSON
- Split rendering: virtualized (accordion) vs continuous (non-virtualized)
- Fix scroll capture by removing height constraints in non-virtualized mode
- Add onVirtualizationChange callback to parent components
- Create rowCount utility for threshold detection

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

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

* feat(ui): add multi-section JSON viewer with adaptive rendering

Implement MultiSectionJsonViewer component that displays multiple JSON
objects in a single viewer with collapsible sections, sticky headers,
and adaptive virtualization.

Features:
- Multiple JSON roots in one viewer with distinct sections
- Sticky section headers that remain visible during scroll
- Search across all sections with auto-expand on matches
- Per-section line numbering and custom backgrounds
- Adaptive rendering: simple (< 500 nodes) or virtualized (> 500 nodes)
- Supports wrap/nowrap/truncate string modes with proper width handling
- Context API for custom section header/footer components

Implementation:
- MultiSectionJsonViewer: Main component with data/presentation separation
- SimpleMultiSectionViewer: Non-virtualized renderer for small datasets
- VirtualizedMultiSectionViewer: Virtualized renderer for large datasets
- useMultiSectionTreeState: Hook for building and managing section trees
- multiSectionTree utils: Tree construction with section nodes
- SectionContext: React context for section state access

Width handling fixes:
- Scrollable column uses fit-content + minWidth (tree.maxContentWidth)
- Section wrappers use fit-content in nowrap mode for full expansion
- Background color applied at section wrapper level to cover full area
- Proper overflow handling: hidden for truncate/wrap, undefined for nowrap

Integration:
- IOPreviewJSON updated to use MultiSectionJsonViewer for input/output/metadata
- Command-based search UI matching LogViewToolbar styling
- Theme support with per-section background colors

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

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

* fix(ui): fix virtualization and stable line numbers in multi-section JSON viewer

Refactored VirtualizedMultiSectionViewer to match VirtualizedJsonViewer architecture:
- Removed nested scroll container that broke virtualization
- Fixed absolute positioning for all virtual items (headers, footers, spacers, rows)
- Added stable totalContentWidth calculation instead of reactive measurement
- Increased overscan from 50 to 500 for smoother scrolling

Made section line numbers stable and immutable:
- Section line numbers now assigned once during tree building
- Removed recomputeSectionLineNumbers function (no longer needed)
- Line numbers remain constant regardless of expansion state

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

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

* remove overscan

* fix(trace-view): eliminate flicker in JSON Beta viewer when selecting observations

When selecting an observation, the JSON Beta viewer would first render
unparsed JSON strings, then flicker and re-render with parsed data.
This caused a jarring visual transition and unnecessary tree rebuilds.

Root cause: Progressive rendering pattern used fallback (parsedInput ?? input),
causing component to render with raw data while Web Worker was parsing.

Changes:
- Add isWaitingForParsing flag to useParsedObservation hook
- Wait for parsing to complete before rendering IOPreviewJSON
- Show "Parsing data..." loading state (100-300ms typical)
- Remove fallback pattern - use only parsed data
- Remove unused props (input, output, metadata, isLoading, media)
- Fix React hooks rules violation (early return after all hooks)

Result: Single clean render with parsed data, no flicker.

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

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

* fix(json-viewer): ensure rows fill container width in multi-section viewer

When container width exceeded calculated content width, rows would only
use content width, creating a white gap on the right side.

Solution: Calculate effectiveRowWidth as max(totalContentWidth, containerWidth)
to ensure rows always fill at least the container width.

Also added minWidth: 100% to content container for consistency.

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

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

* fix(json-viewer): eliminate white margin with long content in nowrap mode

When stringWrapMode is "nowrap" and content exceeds container width,
individual rows would grow beyond their set width due to fit-content
children, but the parent container stayed at 100%, creating a white
margin on the right.

Solution: Match VirtualizedJsonViewer's approach:
- Parent width: nowrap ? "fit-content" : "100%"
- Parent minWidth: "100%"

In nowrap mode, parent grows to accommodate wide content, enabling
proper horizontal scrolling. In wrap/truncate modes, parent stays
constrained to 100% width.

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

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

* fix(json-viewer): measure actual monospace font width for accurate content sizing

Replaces hardcoded 6.2px character width estimate with actual DOM measurement
of the browser's monospace font at 0.7rem. This eliminates white margin issues
on the right side when viewing long content in virtualized JSON view.

Changes:
- Add useMonospaceCharWidth hook to measure actual rendered character width
- Store measurement in sessionStorage to avoid re-measuring per session
- Integrate measured width into tree building (useTreeState, useMultiSectionTreeState)
- Update VirtualizedMultiSectionViewer to use minWidth + max-content pattern

The measurement adapts to different OS/browser monospace fonts (Menlo, Consolas,
Monaco, etc.) providing accurate width estimation regardless of platform.

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

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

* chore: format code with prettier

* fix(json-viewer): enable search in virtualized mode

The debounce effect had an inverted condition that prevented
debouncedSearchQuery from being updated when needsVirtualization=true.
This caused search to appear broken in virtualized mode (datasets >2500 rows).

Root cause: Line 93 had `if (needsVirtualization) return;` which exited
early when virtualization was needed, preventing the search query from
being debounced and passed to MultiSectionJsonViewer.

Fix: Remove the early return condition. Search now works in both
virtualized and non-virtualized modes with 300ms debounce.

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

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

* feat(json-viewer): improve height estimation accuracy for wrap mode

Use measured character width to calculate dynamic characters-per-line
instead of hardcoded "80 chars per line". This significantly improves
the virtualizer's initial height estimates, reducing re-measurements
during fast scrolling.

Changes:
- Add charWidth parameter to useJsonViewerLayout
- Calculate available width accounting for indent, key, colon, quotes
- Dynamically compute charsPerLine based on measured font width
- Fall back to 80-char estimate if charWidth unavailable
- Apply to both VirtualizedJsonViewer and VirtualizedMultiSectionViewer

Benefits:
- More accurate initial height estimates for wrapped strings
- Fewer layout shifts during virtualized scrolling
- Smoother performance with large datasets in wrap mode

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

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

* fix(json-viewer): correct wrap mode height estimation for CSS layout

Fix height estimation to match actual CSS white-space: pre-wrap behavior.
All wrapped lines (including continuations) start at the same horizontal
position after the opening quote, not from the left margin.

This improves virtualization accuracy for deeply nested wrapped strings.

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

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

* feat(json-viewer): add match count badges to multi-section viewer

Enable per-row match count badges in both virtualized and non-virtualized
multi-section JSON viewers, showing indicators like "3/5" when a row has
multiple search matches.

Changes:
- MultiSectionJsonViewer: Calculate matchCounts using getMatchCountsPerNode()
- VirtualizedMultiSectionViewer: Accept and pass matchCounts to JsonRowScrollable
- SimpleMultiSectionViewer: Accept and pass matchCounts to JsonRowScrollable

This brings multi-section viewer search UX to parity with single-section viewer.

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

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

* fix(json-viewer): move match count badges to sticky column to prevent text wrapping

Move match count badges from scrollable column to sticky fixed column
(overlaid on line numbers/expand buttons) to prevent them from consuming
horizontal space and causing premature text wrapping in wrap mode.

Changes:
- JsonRowFixed: Add matchCount/currentMatchIndexInRow props, render badge absolutely positioned
- JsonRowScrollable: Remove badge rendering and unused props
- All viewers: Pass matchCount to JsonRowFixed instead of JsonRowScrollable

Benefits:
- Badge no longer reduces available width for wrapped text
- Badge always visible in sticky column (even when scrolling)
- Consistent position regardless of value length
- No layout shifts when badges appear/disappear

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

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

* feat(trace-view): add section navigation hint bar to JSON viewer

Add a thin navigation bar below the search toolbar that allows quick
jumping to Input, Output, and Metadata sections.

Features:
- Shows "Jump to: Input, Output, Metadata" with clickable section links
- Only displays links for visible sections
- Smooth scroll to section headers on click
- Compact 24px height bar with muted background
- Links styled with hover underline effect

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

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

* fix(trace-view): remove tinted backgrounds in JSON viewer sections

Change section backgrounds from colored tints (light blue, light green,
light purple) to transparent/white in light mode for a cleaner look.

Dark mode section backgrounds remain unchanged (dark slate, dark blue-gray,
dark purple for visual separation).

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

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

* fix(trace-view): use clean background for section navigation bar

Change "Jump to:" navigation bar background from bg-muted/30 to bg-background
for a cleaner white appearance that matches the UI.

Section backgrounds remain with their colored tints (blue, green, purple)
for visual separation.

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

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

* fix(json-viewer): improve section scroll-to behavior for virtualized mode

Add data-section-key attributes to section elements and use querySelector
instead of parsing text content. This ensures scroll-to works correctly in
both virtualized and non-virtualized modes, and scrolls to the actual section
position rather than just making the sticky header visible.

Changes:
- VirtualizedMultiSectionViewer: Add data-section-key to section header divs
- SimpleMultiSectionViewer: Add data-section-key to section wrapper divs
- IOPreviewJSON: Use querySelector with data attribute instead of text matching

Benefits:
- Works reliably in virtualized mode (separate virtual rows)
- Scrolls to actual section position, not just sticky header
- Simpler, more maintainable code
- No text parsing needed

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

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

* chore(json-viewer): remove debug console.log statements

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

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

* feat(json-viewer): add scrollToSection method via ref for multi-section viewers

- Add findSectionHeaderIndex utility to find section headers by key
- Expose scrollToSection via imperative handle in both virtualized and simple viewers
- MultiSectionJsonViewer forwards ref with unified interface
- IOPreviewJSON uses ref-based scrolling instead of querySelector

Works correctly in both virtualized and non-virtualized modes, handling
dynamic section positions as sections expand/collapse.

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

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

* fix(json-viewer): use auto scroll behavior instead of smooth for virtualizer

TanStack Virtual doesn't fully support smooth scrolling with dynamic sizing.
Changed from behavior: 'smooth' to 'auto' to avoid scroll failures.

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

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

* fix(trace-view): remove extra spacing in section navigation bar

Removed gap-1.5 from section wrapper and added &nbsp; after comma
to tighten spacing between section names.

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

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

* test: add NASA audio file and trace creation script for media testing

- Add sounds-of-mars-one-small-step-earth.wav (NASA public domain audio)
- Add create-test-traces-with-media.ts script to generate test traces with
  different media attachment permutations (image/audio/document)
- Script creates 7 test traces for UI testing of media buttons feature

Audio file courtesy of NASA (public domain)
Source: https://www.nasa.gov/audio-and-ringtones/

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

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

* feat(json-viewer): add media attachment buttons to section headers

- Add MediaButtonGroup component that displays media buttons grouped by type
  (image, audio, video, document) with count badges for multiple files
- Show media buttons in JSON viewer section headers (Input, Output, Metadata)
- Support hover-to-preview and click-to-pin interaction patterns
- Filter and display media by section field
- Update section header to show "N keys" instead of "N rows" with thousands
  separator and smaller font size
- Add virtualization badge in navigation bar when data exceeds threshold
- Thread media prop through component hierarchy from IOPreview to section
  headers

Media buttons appear only when media attachments exist for a section.
Hovering shows preview, clicking pins it open for interaction.

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

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

* feat(json-viewer): show media previews in popover instead of file icons

Replace file icon cards with actual media previews:
- Images: 96x96px preview that opens in new tab on click
- Audio: HTML5 audio player with controls
- Video: HTML5 video player with controls
- Documents: Keep file icon card (no preview available)

Also remove debug console.log statements from hover/click interaction.

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

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

* fix(json-viewer): add delay before closing media popover on mouse leave

Add 300ms delay before closing the popover when mouse leaves the button
or popover content. This prevents premature closing when moving the mouse
from the button down to the popover.

- Clear timeout when mouse enters either button or popover content
- Apply same delay to both button and content mouseLeave handlers
- Improves UX by giving users time to move mouse between elements

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

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

* remove lgos

* move media creation to seeder

* add chatml media seeder

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Nimar <l.nimar.b@gmail.com>
2025-12-23 14:26:32 +00:00
Valery MeleshkinandGitHub c04b990c49 chore: link to v2 endpoints from v1 (#11292) 2025-12-23 13:26:40 +00:00
Valery MeleshkinandGitHub 4341215f70 chore: limit RAM usage before forcing external group by (#11290) 2025-12-23 12:50:22 +00:00
Max DeichmannandGitHub e10102eb0b feat: add org details to project API (#11288) 2025-12-23 12:05:39 +00:00
Valery MeleshkinandGitHub 3bf5a351ef chore: add a test that observation v2 supports nested metadata keys (#11277) 2025-12-22 18:04:44 +00:00
marliessophieandGitHub 223f4571c9 chore(score-configs): rename ScoreDataType -> ScoreConfigDataType (#11266)
* chore(prisma): rename ScoreDataType to ScoreConfigDataType and update related schema and types

* chore: update score config types

* chore: adjust score types for use cases

* fixup: adjust score types for use cases

* chore: push

* chore: push

* chore: push

* chore: push

* chore: push
2025-12-22 17:27:21 +00:00
Valery MeleshkinandGitHub ece6ee5144 chore: present a better articulated error on environments without V2 support. (#11274) 2025-12-22 16:27:53 +00:00
NimarandGitHub 66b850d365 chore: minor bump radix elements (#11271) 2025-12-22 15:35:37 +00:00
NimarandGitHub dddc92e683 chore: upgrade turbo to 2.7.1 (#11268) 2025-12-22 15:03:40 +00:00
NimarandGitHub 33022d7428 feat(tracing): filter observations by tool calls (#11031)
* add plan

* add test

* move adapters to shared

* allow unused _vars in worker

* fix more lint

* fix lint

* add tests

* cleanup

* fix test

* no more json column

* don't use metadata

* increase migration version

* fix test

* frontend

* test

* fix test

* fix

* import

* fix seeder

* add seeder data

* unstage

* remove migrations

* new column setup

* update

* spelling

* update

* fixup

* fix type

* update

* fix build

* don't show on public API yet
2025-12-22 09:40:20 +00:00
Hassieb PakzadandGitHub 21de8052fa fix(ingestion-ai-sdk): subtract output_reasoning_tokens from total output tokens (#11264) 2025-12-22 09:23:30 +00:00
Max Deichmann 3d71892d16 chore: release v3.142.0 2025-12-22 09:55:07 +01:00
Max DeichmannandGitHub 6bcf0fad2c security: validate webhook reditrects (#11256)
* chore: validate redirected ip[

* chore: validate redirected ip[

* chore: validate redirected ip[

* chore: validate redirected ip[
2025-12-21 20:41:31 +00:00
Hassieb PakzadandGitHub 8df81dfdb5 feat(cost-tracking): add gemini-3-flash-preview (#11255) 2025-12-21 12:25:40 +00:00
Max DeichmannandGitHub 080d73c7a5 chore: add skeleton for events table (#11250) 2025-12-20 12:10:15 +00:00
NimarandGitHub 2089a37ff5 chore: bump google cloud storage to 7.18.0 (#11249) 2025-12-20 07:37:42 +00:00
8a90191156 chore: patch bump next-auth to 4.24.13 (#11173)
* chore: patch bump next-auth to 4.24.13

* perf: events table io loading (#11178)

* perf: events table io loading

* push

* perf: events table io loading

* perf: events table io loading

* perf: events table io loading

* perf: events table io loading

* perf: events table io loading

* fix(evalService): add filter for valid_to in extractVariables (#11238)

* chore: upgrade trpc to 11.8.0 (#11239)

* chore: release v3.141.0

* perf: optimize events reads (#11241)

* fix: reverting get rid of extra IN clauses along trace and score deletion paths  (#11242)

Revert "fix: get rid of extra IN clauses along trace and score deletion paths…"

This reverts commit a7ad12da68.

---------

Co-authored-by: Max Deichmann <m.deichmann@tum.de>
Co-authored-by: marliessophie <74332854+marliessophie@users.noreply.github.com>
Co-authored-by: Valery Meleshkin <valeriy@langfuse.com>
2025-12-20 07:11:25 +00:00
Valery MeleshkinandGitHub 9fe60a2971 fix: reverting get rid of extra IN clauses along trace and score deletion paths (#11242)
Revert "fix: get rid of extra IN clauses along trace and score deletion paths…"

This reverts commit a7ad12da68.
2025-12-19 15:30:44 +00:00
Max DeichmannandGitHub 0dd2a1ce0c perf: optimize events reads (#11241) 2025-12-19 14:50:48 +00:00
827 changed files with 56909 additions and 16014 deletions
+507
View File
@@ -0,0 +1,507 @@
---
name: add-model-price
description: Add new LLM model pricing entries to Langfuse's default-model-prices.json. Use when adding model prices, updating model pricing, creating model entries, adding Claude/OpenAI/Anthropic/Google/Gemini/AWS Bedrock/Azure/Vertex AI model pricing, working with matchPattern regex, pricingTiers, or model cost configuration. Covers model price JSON structure, regex patterns for multi-provider matching, tiered pricing with conditions, cache pricing, and validation rules.
---
# Add Model Price
## Purpose
Guide for adding new LLM model pricing entries to Langfuse's default model prices configuration. This enables accurate cost tracking across different model providers and deployment platforms.
## When to Use This Skill
Automatically activates when:
- Adding a new model to the pricing database
- Updating model pricing information
- Working with `default-model-prices.json`
- Creating model matchPattern regex
- Configuring pricingTiers or tiered pricing
- Adding prices for Claude, OpenAI, Anthropic, Google, Gemini, AWS Bedrock, Azure, or Vertex AI models
---
## Quick Start Checklist
- [ ] **Gather model info**: Fetch official pricing from provider documentation URL
- [ ] **Generate UUID**: Run `uuidgen` for the model entry ID (use lowercase)
- [ ] **Create matchPattern**: Regex covering all provider formats
- [ ] **Define pricingTiers**: At minimum, one default tier with standard prices
- [ ] **Add pricing entry**: Insert into `/worker/src/constants/default-model-prices.json`
- [ ] **Add to LLM types**: Add model to `/packages/shared/src/server/llm/types.ts` (for playground/LLM-as-judge)
- [ ] **Validate JSON**: Run `jq . default-model-prices.json` to verify syntax
---
## File Location
**Target File**: `/worker/src/constants/default-model-prices.json`
This JSON file contains an array of model pricing definitions used for cost calculation.
---
## 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 UUID (use `uuidgen` command, lowercase) |
| `modelName` | string | Primary model identifier |
| `matchPattern` | string | Regex for matching model names |
| `createdAt` | string | ISO-8601 timestamp |
| `updatedAt` | string | ISO-8601 timestamp |
| `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 (Required)
Every model must have exactly one default tier:
```json
{
"id": "{model-id}_tier_default",
"name": "Standard",
"isDefault": true,
"priority": 0,
"conditions": [],
"prices": { }
}
```
**Rules for Default Tier:**
- `isDefault`: Must be `true`
- `priority`: Must be `0`
- `conditions`: Must be empty array `[]`
### Additional Tiers (Optional)
For usage-based pricing (e.g., large context 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": { }
}
```
**Condition Operators:** `gt`, `gte`, `lt`, `lte`, `eq`, `neq`
---
## Gathering Pricing Information
**IMPORTANT**: Always fetch pricing from official provider documentation. Never use heuristics or assumptions.
### Official Pricing Sources
| Provider | URL |
|----------|-----|
| 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/ |
### Required Information to Gather
1. **Base input token price** (per MTok)
2. **Output token price** (per MTok)
3. **Cache write price** (if caching supported)
4. **Cache read price** (if caching supported)
5. **Long context pricing** (if different tiers exist)
6. **Model ID formats** for all platforms (API, Bedrock, Vertex)
---
## Price Conversion
Prices in the JSON are **per token**, not per million tokens.
| Provider Pricing | JSON Value | Calculation |
|-----------------|------------|-------------|
| $5 / MTok | `5e-6` | $5 / 1,000,000 |
| $25 / MTok | `25e-6` | $25 / 1,000,000 |
| $0.50 / MTok | `0.5e-6` | $0.50 / 1,000,000 |
| $6.25 / MTok | `6.25e-6` | $6.25 / 1,000,000 |
**Formula**: `price_per_token = price_per_mtok / 1_000_000` or `price_per_mtok * 1e-6`
---
## 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` (direct API)
- `anthropic/claude-opus-4-6` (with prefix)
- `anthropic.claude-opus-4-6-v1:0` (AWS Bedrock)
- `us.anthropic.claude-opus-4-6-v1:0` (regional Bedrock)
- `claude-opus-4-6` (GCP Vertex)
### 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 | Matches GPT-4o and gpt-4o |
| `^...$` | Full string match | Prevents partial matches |
| `(provider\/)?` | Optional provider prefix | `openai/gpt-4o` |
| `(eu\\.\\|us\\.\\|apac\\.)?` | AWS regions | `us.anthropic.model` |
| `(:0)?` | Optional version suffix | Bedrock model versions |
| `@date` | Vertex AI format | `claude-3-5-sonnet@20240620` |
---
## Step-by-Step: Adding a New Model
### Step 1: Fetch Official Pricing
Use WebFetch to get pricing from official documentation:
```
WebFetch URL: https://platform.claude.com/docs/en/about-claude/pricing
Prompt: Extract pricing for [model name] including input, output, cache write, cache read prices per MTok
```
### Step 2: Generate UUID
```bash
uuidgen
# Output: 13458BC0-1C20-44C2-8753-172F54B67647
# Convert to lowercase: 13458bc0-1c20-44c2-8753-172f54b67647
```
### Step 3: Create the Entry
Example for a model with $5 input, $25 output, $6.25 cache write, $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-02-09T00:00:00.000Z",
"updatedAt": "2026-02-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
}
}
]
}
```
### Step 4: Insert Entry
Add the entry to the JSON array in `/worker/src/constants/default-model-prices.json`.
**Placement**: Insert near related models (e.g., other Claude models together).
### Step 5: Add to LLM Types (for Playground & LLM-as-Judge)
To make the model available in the Langfuse playground and for LLM-as-a-judge evaluations, add it to the appropriate model array in `/packages/shared/src/server/llm/types.ts`.
**File**: `/packages/shared/src/server/llm/types.ts`
**Model Arrays by Provider:**
- `anthropicModels` - Anthropic Claude models
- `openAIModels` - OpenAI GPT models
- `vertexAIModels` - Google Vertex AI models
- `googleAIStudioModels` - Google AI Studio models
**IMPORTANT**: Do NOT add new models as the first entry in the array. The first entry is used as the default model for test LLM API calls, and newer models may not be available to all users yet.
**Example for Anthropic:**
```typescript
export const anthropicModels = [
"claude-sonnet-4-5-20250929", // Keep existing first entry
"claude-haiku-4-5-20251001",
"claude-opus-4-6", // Add new model here (not first!)
"claude-opus-4-5-20251101",
// ... rest of models
] as const;
```
### Step 6: Validate
```bash
# Check JSON syntax
jq . /path/to/default-model-prices.json > /dev/null && echo "Valid JSON"
# Verify entry exists
jq '.[] | select(.modelName == "claude-opus-4-6")' /path/to/default-model-prices.json
```
---
## Multi-Tier Pricing Example
For models with long context pricing (e.g., different rates above 200K tokens):
```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
The system validates pricing tiers with these rules:
1. **Exactly one default tier** with `isDefault: true`
2. **Default tier** must have `priority: 0` and empty `conditions: []`
3. **Non-default tiers** must have `priority > 0` and at least one condition
4. **All priorities** must be unique within a model
5. **All tier names** must be unique within a model
6. **Each tier** must have at least one price
7. **All tiers** must have identical usage type keys
8. **Regex patterns** must be valid and safe (no catastrophic backtracking)
---
## Common Mistakes
**Using heuristics instead of official pricing:**
```json
// Wrong - assuming cache is 1.25x input
"cache_creation_input_tokens": input_price * 1.25
// Correct - use exact value from official docs
"cache_creation_input_tokens": 6.25e-6
```
**Incorrect Price Format:**
```json
// Wrong - using MTok price directly
"input": 5
// Correct - price per token
"input": 5e-6
```
**Missing Tier ID Suffix:**
```json
// Wrong
"id": "some-uuid"
// Correct for default tier
"id": "model-uuid_tier_default"
```
**Invalid Regex Escaping:**
```json
// Wrong - unescaped dots
"matchPattern": "anthropic.claude"
// Correct - escaped dots
"matchPattern": "anthropic\\.claude"
```
---
## Testing Model Matching
After adding a model, test that the regex matches expected inputs:
```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
```
---
## Reference: Existing Model Entries
Look at these existing entries as templates:
| Model Type | Example Entry | Notes |
|------------|---------------|-------|
| Anthropic Claude | `claude-opus-4-5-20251101` | Full multi-provider pattern |
| OpenAI GPT | `gpt-4o` | Simple pattern |
| Google Gemini | `gemini-2.5-pro` | Multi-tier with large context |
---
## Related Files
- **Pricing JSON**: `/worker/src/constants/default-model-prices.json`
- **LLM Types**: `/packages/shared/src/server/llm/types.ts` (model arrays for playground/LLM-as-judge)
- **Validation**: `/packages/shared/src/server/pricing-tiers/validation.ts`
- **Matcher**: `/packages/shared/src/server/pricing-tiers/matcher.ts`
- **Tests**: `/web/src/__tests__/async/model-pricing-tiers.servertest.ts`
---
**Skill Status**: COMPLETE
**Line Count**: ~340 lines
@@ -50,6 +50,7 @@ Automatically activates when working on:
- [ ] **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)
@@ -423,6 +424,32 @@ const traces = await queryClickhouse({
});
```
### 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
@@ -645,7 +645,42 @@ async doIt()
async execute()
```
### 3. Return Types
### 3. Use Params Objects for Multiple Arguments
When a function receives multiple arguments, use a single params object instead of positional arguments:
```typescript
// ❌ BAD - Positional arguments are unclear and can be swapped
async function createTrace(
projectId: string,
userId: string,
sessionId: string,
name: string,
) {}
// Call site - which string is which?
await createTrace(projectId, userId, sessionId, name);
// ✅ GOOD - Params object makes intent clear
async function createTrace(params: {
projectId: string;
userId: string;
sessionId: string;
name: string;
}) {}
// Call site - clear and prevents argument swapping bugs
await createTrace({ projectId, userId, sessionId, name });
```
**Benefits:**
- More readable at call sites
- Prevents bugs when positional arguments of the same type are accidentally swapped
- Easier to add optional parameters later
- Self-documenting code
### 4. Return Types
Always use explicit return types:
@@ -659,7 +694,7 @@ async deleteUser(id: string): Promise<void> {}
async createUser(data) {} // No types!
```
### 4. Error Handling
### 5. Error Handling
Services should throw meaningful errors:
@@ -679,7 +714,7 @@ if (!user) {
}
```
### 5. Avoid God Services
### 6. Avoid God Services
Don't create services that do everything:
+47
View File
@@ -25,6 +25,53 @@
]
}
},
"add-model-price": {
"type": "domain",
"enforcement": "suggest",
"priority": "high",
"description": "Guide for adding LLM model pricing entries to default-model-prices.json",
"promptTriggers": {
"keywords": [
"model price",
"model pricing",
"model prices",
"add model",
"new model",
"model cost",
"default-model-prices",
"matchPattern",
"pricingTiers",
"pricing tier",
"token pricing",
"Claude pricing",
"OpenAI pricing",
"Anthropic pricing",
"Gemini pricing",
"Bedrock pricing",
"Vertex pricing"
],
"intentPatterns": [
"(add|create|insert|update).*?(model|pricing|price)",
"(new|add).*?(claude|openai|anthropic|gemini|gpt).*?(model|price)",
"model.*?(price|pricing|cost)",
"pricing.*?tier"
]
},
"fileTriggers": {
"pathPatterns": [
"worker/src/constants/default-model-prices.json",
"packages/shared/src/server/llm/types.ts",
"**/model*price*.json"
],
"contentPatterns": [
"matchPattern",
"pricingTiers",
"tokenizerId",
"anthropicModels",
"openAIModels"
]
}
},
"backend-dev-guidelines": {
"type": "domain",
"enforcement": "suggest",
+1 -1
View File
@@ -2,7 +2,7 @@
FROM mcr.microsoft.com/vscode/devcontainers/typescript-node:20-bookworm
# Install golang-migrate for database migrations
RUN curl -L https://github.com/golang-migrate/migrate/releases/download/v4.19.0/migrate.linux-amd64.tar.gz | tar xvz && \
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
+1 -1
View File
@@ -25,7 +25,7 @@ NEXTAUTH_SECRET="secret"
NEXT_PUBLIC_LANGFUSE_CLOUD_REGION="DEV"
# Langfuse experimental features
LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES="true"
LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES="false"
# Salt for API key hashing
SALT="salt"
+1 -1
View File
@@ -25,7 +25,7 @@ NEXTAUTH_SECRET="secret"
NEXT_PUBLIC_LANGFUSE_CLOUD_REGION="DEV"
# Langfuse experimental features
LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES="true"
LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES="false"
# Salt for API key hashing
SALT="salt"
+5 -1
View File
@@ -46,6 +46,8 @@ DATABASE_URL="postgresql://postgres:postgres@localhost:5432/postgres"
# Clickhouse
CLICKHOUSE_MIGRATION_URL="clickhouse://localhost:9000"
CLICKHOUSE_URL="http://localhost:8123"
# CLICKHOUSE_READ_ONLY_URL="http://localhost:8123" # Optional: read replica for legacy tables
# CLICKHOUSE_EVENTS_READ_ONLY_URL="http://localhost:8123" # Optional: read replica for events table queries
CLICKHOUSE_USER="clickhouse"
CLICKHOUSE_PASSWORD="clickhouse"
CLICKHOUSE_CLUSTER_ENABLED="false"
@@ -62,7 +64,7 @@ NEXTAUTH_SECRET="secret"
NEXT_PUBLIC_LANGFUSE_CLOUD_REGION="DEV"
# Langfuse experimental features
LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES="true"
LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES="false"
# Salt for API key hashing
SALT="salt"
@@ -147,3 +149,5 @@ LANGFUSE_AWS_BEDROCK_MODEL="eu.anthropic.claude-3-haiku-20240307-v1:0"
# Events table migration
LANGFUSE_ENABLE_EVENTS_TABLE_OBSERVATIONS=true
LANGFUSE_ENABLE_EVENTS_TABLE_FLAGS=true
LANGFUSE_ENABLE_EVENTS_TABLE_V2_APIS=true
LANGFUSE_EXPERIMENT_INSERT_INTO_EVENTS_TABLE=true
+5 -3
View File
@@ -41,8 +41,10 @@ OTEL_EXPORTER_OTLP_ENDPOINT="http://localhost:4318"
OTEL_SERVICE_NAME="langfuse"
# Default role for users who sign up, optional, can be org or org+project
# Supports comma-separated IDs for multiple orgs (e.g., "org1,org2,org3")
# LANGFUSE_DEFAULT_ORG_ID=
# LANGFUSE_DEFAULT_ORG_ROLE=
# Supports comma-separated IDs for multiple projects (e.g., "proj1,proj2,proj3")
# LANGFUSE_DEFAULT_PROJECT_ID=
# LANGFUSE_DEFAULT_PROJECT_ROLE=
@@ -51,7 +53,7 @@ OTEL_SERVICE_NAME="langfuse"
# LANGFUSE_LOG_FORMAT=text
# Enable experimental features, optional
# LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES=true
# LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES=false
# Auth, optional configuration
# AUTH_DOMAINS_WITH_SSO_ENFORCEMENT=domain1.com,domain2.com
@@ -142,7 +144,7 @@ OTEL_SERVICE_NAME="langfuse"
# Transactional email, optional
# Defines the email address to use as the from address.
# EMAIL_FROM_ADDRESS=
# Defines the connection url for smtp server.
# Defines the connection url for smtp server.
# SMTP_CONNECTION_URL=
# S3 Batch Exports
@@ -215,7 +217,7 @@ LANGFUSE_ENABLE_BLOB_STORAGE_FILE_LOG=true
# Ingestion configuration
# LANGFUSE_INGESTION_QUEUE_DELAY_MS=
# LANGFUSE_INGESTION_CLICKHOUSE_WRITE_BATCH_SIZE=
# LANGFUSE_INGESTION_CLICKHOUSE_WRITE_INTERVAL_MS=
# LANGFUSE_INGESTION_CLICKHOUSE_WRITE_INTERVAL_MS=
# LANGFUSE_INGESTION_CLICKHOUSE_MAX_ATTEMPTS=
### START Enterprise Edition Configuration
-4
View File
@@ -1,4 +0,0 @@
node_modules
**/node_modules
build
coverage
+2 -1
View File
@@ -80,8 +80,9 @@ jobs:
if (context.ref === "refs/heads/main") {
return `["staging"]`
}
// TODO revert to: `["prod-eu", "prod-us", "prod-hipaa"]`
if (context.ref === "refs/heads/production") {
return `["prod-eu", "prod-us", "prod-hipaa"]`
return `["prod-eu", "prod-us"]`
}
}
return "[]"
+20 -8
View File
@@ -172,7 +172,7 @@ jobs:
- uses: actions/checkout@v4
- name: Install golang-migrate for Clickhouse migrations
run: |
curl -L https://github.com/golang-migrate/migrate/releases/download/v4.19.0/migrate.linux-amd64.tar.gz | tar xvz
curl -L https://github.com/golang-migrate/migrate/releases/download/v4.19.1/migrate.linux-amd64.tar.gz | tar xvz
sudo mv migrate /usr/bin/migrate
which migrate
- uses: pnpm/action-setup@v3
@@ -269,7 +269,7 @@ jobs:
- uses: actions/checkout@v4
- name: Install golang-migrate for Clickhouse migrations
run: |
curl -L https://github.com/golang-migrate/migrate/releases/download/v4.19.0/migrate.linux-amd64.tar.gz | tar xvz
curl -L https://github.com/golang-migrate/migrate/releases/download/v4.19.1/migrate.linux-amd64.tar.gz | tar xvz
sudo mv migrate /usr/bin/migrate
which migrate
- uses: pnpm/action-setup@v3
@@ -396,7 +396,7 @@ jobs:
pnpm install
- name: Install golang-migrate for Clickhouse migrations
run: |
curl -L https://github.com/golang-migrate/migrate/releases/download/v4.19.0/migrate.linux-amd64.tar.gz | tar xvz
curl -L https://github.com/golang-migrate/migrate/releases/download/v4.19.1/migrate.linux-amd64.tar.gz | tar xvz
sudo mv migrate /usr/bin/migrate
which migrate
- name: Load default env
@@ -422,6 +422,18 @@ jobs:
pnpm run db:migrate
pnpm run db:seed
pnpm run --filter=shared ch:up
- name: Download and extract ClickHouse client for dev-tables setup
if: matrix.deploy-mode == ''
run: |
CH_VERSION="24.9.3.128"
ARCH="amd64"
wget "https://packages.clickhouse.com/deb/pool/main/c/clickhouse/clickhouse-common-static_${CH_VERSION}_${ARCH}.deb" -O clickhouse-client.deb
dpkg-deb -x clickhouse-client.deb ch_client_dir
sudo cp ch_client_dir/usr/bin/clickhouse /usr/local/bin/
- name: Setup Dev Tables
if: matrix.deploy-mode == ''
run: |
pnpm --filter=shared ch:dev-tables
- name: Build
run: pnpm --filter=worker... run build
- name: run tests
@@ -456,7 +468,7 @@ jobs:
pnpm install
- name: Install golang-migrate for Clickhouse migrations
run: |
curl -L https://github.com/golang-migrate/migrate/releases/download/v4.19.0/migrate.linux-amd64.tar.gz | tar xvz
curl -L https://github.com/golang-migrate/migrate/releases/download/v4.19.1/migrate.linux-amd64.tar.gz | tar xvz
sudo mv migrate /usr/bin/migrate
which migrate
- name: Load default env
@@ -548,7 +560,7 @@ jobs:
cp .env.dev.example web/.env
- name: Install golang-migrate for Clickhouse migrations
run: |
curl -L https://github.com/golang-migrate/migrate/releases/download/v4.19.0/migrate.linux-amd64.tar.gz | tar xvz
curl -L https://github.com/golang-migrate/migrate/releases/download/v4.19.1/migrate.linux-amd64.tar.gz | tar xvz
sudo mv migrate /usr/bin/migrate
which migrate
- name: Run + migrate
@@ -604,7 +616,7 @@ jobs:
pnpm install
- name: Install golang-migrate for Clickhouse migrations
run: |
curl -L https://github.com/golang-migrate/migrate/releases/download/v4.19.0/migrate.linux-amd64.tar.gz | tar xvz
curl -L https://github.com/golang-migrate/migrate/releases/download/v4.19.1/migrate.linux-amd64.tar.gz | tar xvz
sudo mv migrate /usr/bin/migrate
which migrate
- name: Load default env
@@ -653,7 +665,7 @@ jobs:
tests-web-sync,
tests-worker,
test-worker-llm-connections,
e2e-tests,
# e2e-tests, # Temporarily disabled - flaky timeouts
test-docker-build,
e2e-server-tests,
tests-web-async,
@@ -693,7 +705,7 @@ jobs:
push-docker-image:
needs: all-ci-passed
# if something inside all-ci-passed was skipped, but everything that ran passed, we still want to deploy
if: always() && needs.all-ci-passed.outputs.success == 'true' && github.event_name == 'push' && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/'))
if: always() && needs.all-ci-passed.outputs.success == 'true' && github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')
environment: "protected branches"
runs-on: ubuntu-latest
permissions:
+27
View File
@@ -0,0 +1,27 @@
name: Snyk Container - Web
on:
push:
branches: ["production", "main"]
jobs:
snyk:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- 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@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: Upload result to GitHub Code Scanning
uses: github/codeql-action/upload-sarif@v4
with:
sarif_file: snyk.sarif
+27
View File
@@ -0,0 +1,27 @@
name: Snyk Container - Worker
on:
push:
branches: ["production", "main"]
jobs:
snyk:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- 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@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: Upload result to GitHub Code Scanning
uses: github/codeql-action/upload-sarif@v4
with:
sarif_file: snyk.sarif
-28
View File
@@ -1,28 +0,0 @@
name: Snyk Container
on:
push:
branches: ["main"]
jobs:
snyk:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Scan web image with Snyk
uses: snyk/actions/docker@master
continue-on-error: true # let upload step always run
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
with:
image: langfuse/langfuse # pulled from Docker Hub
args: --severity-threshold=high # (any extra CLI flags, but NO --file)
- name: Scan worker image with Snyk
uses: snyk/actions/docker@master
continue-on-error: true # let upload step always run
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
with:
image: langfuse/langfuse-worker # pulled from Docker Hub
args: --severity-threshold=high # (any extra CLI flags, but NO --file)
+3
View File
@@ -17,6 +17,7 @@ migrate
# next.js
/.next/
/.next-check/
/out/
next-env.d.ts
@@ -35,6 +36,7 @@ npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
.eslintcache
# local env files
# do not commit any .env files to git, except for the .env.example file. https://create.t3.gg/en/usage/env-variables#using-environment-variables
@@ -64,6 +66,7 @@ node_modules
**/node_modules
**/dist
**/.next/*
**/.next-check/*
**/.turbo/*
.yarn
.turbo
+7 -2
View File
@@ -28,6 +28,11 @@
"editor.defaultFormatter": "Prisma.prisma"
},
"eslint.lintTask.enable": true,
"eslint.workingDirectories": ["./web", "./worker"],
"eslint.useFlatConfig": false
"eslint.workingDirectories": [
{ "pattern": "./web" },
{ "pattern": "./worker" },
{ "pattern": "./packages/shared" },
{ "pattern": "./ee" }
],
"eslint.useFlatConfig": true
}
+22 -5
View File
@@ -72,9 +72,11 @@ pnpm run infra:dev:up # Start Docker services (PostgreSQL, ClickHouse, Redi
pnpm run infra:dev:down # Stop Docker services
```
### Building
### Building & Type Checking
```sh
pnpm --filter=PACKAGE_NAME run build # Runs the build command, will show real typescript errors etc.
pnpm tc # Fast typecheck across all packages (alias for pnpm typecheck)
pnpm build:check # Full Next.js build to alternate dir (can run parallel with dev server)
```
### Testing in Web Package
@@ -82,11 +84,11 @@ The web package uses JEST for unit tests.
Depending on the file location (sync, async)
`web` related tests must go into the `web/src/__tests__/` folder.
```sh
pnpm test-sync --testPathPattern="$FILE_LOCATION_PATTERN" --testNamePattern="$TEST_NAME_PATTERN"
pnpm test-sync --testPathPatterns="$FILE_LOCATION_PATTERN" --testNamePattern="$TEST_NAME_PATTERN"
# For tests in the async folder:
pnpm test -- --testPathPattern="$FILE_LOCATION_PATTERN" --testNamePattern="$TEST_NAME_PATTERN"
pnpm test -- --testPathPatterns="$FILE_LOCATION_PATTERN" --testNamePattern="$TEST_NAME_PATTERN"
# For client tests:
pnpm test-client --testPathPattern="buildStepData" --testNamePattern="buildStepData"
pnpm test-client --testPathPatterns="buildStepData" --testNamePattern="buildStepData"
```
### Testing in the Worker Package
@@ -113,7 +115,7 @@ pnpm run nuke # Remove all node_modules, build files, wipe database
- **Styling**: Tailwind CSS with CSS variables for theming
- **Components**: shadcn/ui (Radix UI primitives)
- **State Management**: TanStack Query (React Query) + tRPC
- **Charts**: Tremor, Recharts
- **Charts**: Recharts
### Worker Application (`/worker/`)
- **Framework**: Express.js
@@ -192,6 +194,21 @@ To get a project, use the `get_project` capability with the full project name as
## TypeScript Best Practices
- In TypeScript, if possible, don't use the `any` type
- **Use a single params object for functions with multiple arguments** - This makes code more readable at call sites and prevents bugs when arguments of the same type are accidentally swapped:
```typescript
// ❌ Bad - positional arguments are unclear and can be swapped without type errors
function sendMessage(userId: string, sessionId: string, projectId: string) {
// ...
}
sendMessage(someString, someOtherString, anotherString); // Which is which?
// ✅ Good - params object makes intent clear and prevents argument swapping
function sendMessage(params: { userId: string; sessionId: string; projectId: string }) {
// ...
}
sendMessage({ userId: someString, sessionId: someOtherString, projectId: anotherString });
```
## General Coding Guidelines
- For easier code reviews, prefer not to move functions etc around within a file unless necessary or instructed to do so
+6 -7
View File
@@ -94,8 +94,6 @@ The diagram below may not show all relationships if the foreign key is not defin
Full database schema: [packages/shared/prisma/schema.prisma](packages/shared/prisma/schema.prisma)
<img src="./packages/shared/prisma/database.svg">
## Repository Structure
We built a monorepo using [pnpm](https://pnpm.io/motivation) and [turbo](https://turbo.build/repo/docs) to manage the dependencies and build process. The monorepo contains the following packages:
@@ -122,8 +120,8 @@ Requirements
**Steps**
1. Install development dependencies:
- [golang-migrate](https://github.com/golang-migrate/migrate/tree/master/cmd/migrate#migrate-cli) as CLI
- [clickhouse binary](https://clickhouse.com/docs/install) on macOS with brew: `brew install --cask clickhouse`
- [golang-migrate](https://github.com/golang-migrate/migrate/tree/master/cmd/migrate#migrate-cli) as CLI
- [clickhouse binary](https://clickhouse.com/docs/install) on macOS with brew: `brew install --cask clickhouse`
2. Fork the repository and clone it locally
@@ -182,6 +180,8 @@ pnpm run db:seed:examples
pnpm install
pnpm run dev
pnpm --filter=web run dev # execute command only in one package
pnpm tc # fast typecheck all packages
pnpm build:check # Full Next.js build to alternate dir (can run parallel with dev server)
```
In the root `package.json`, you can find scripts which are executed with turbo e.g. `turbo run dev`. These scripts are executed with the help of Turbo. Turbo executes the commands in all packages taking care of the correct order of execution. Task definitions can be found in the `turbo.config.js` file.
@@ -254,9 +254,9 @@ To run a specific test, for example the test: `"should handle special characters
```sh
cd web # or with --filter=web
pnpm test-sync --testPathPattern="prompts\.v2\.servertest" --testNamePattern="should handle special characters in prompt names"
pnpm test-sync --testPathPatterns="prompts\.v2\.servertest" --testNamePattern="should handle special characters in prompt names"
# for async folder tests:
pnpm test -- --testPathPattern="observations-api" --testNamePattern="should fetch all observations"
pnpm test -- --testPathPatterns="observations-api" --testNamePattern="should fetch all observations"
```
To run all tests:
@@ -424,4 +424,3 @@ npx fern-api generate --api organizations # for the organizations API
Langfuse is MIT licensed, except for `ee/` folder. See [LICENSE](LICENSE) and [docs](https://langfuse.com/docs/open-source) for more details.
When contributing to the Langfuse codebase, you need to agree to the [Contributor License Agreement](https://cla-assistant.io/langfuse/langfuse). You only need to do this once and the CLA bot will remind you if you haven't signed it yet.
+1 -1
View File
@@ -295,6 +295,7 @@ _[Langfuse 中的公共示例追踪](https://cloud.langfuse.com/project/cloramnk
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/131470832?s=40&v=4" width="20" height="20" alt=""> &nbsp; [lobehub](https://github.com/lobehub) / [lobe-chat](https://github.com/lobehub/lobe-chat) | 49003 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/85702467?s=40&v=4" width="20" height="20" alt=""> &nbsp; [langflow-ai](https://github.com/langflow-ai) / [langflow](https://github.com/langflow-ai/langflow) | 39093 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/130722866?s=40&v=4" width="20" height="20" alt=""> &nbsp; [run-llama](https://github.com/run-llama) / [llama_index](https://github.com/run-llama/llama_index) | 37368 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/169401942?s=40&v=4" width="20" height="20" alt=""> &nbsp; [danny-avila](https://github.com/danny-avila) / [LibreChat](https://github.com/danny-avila/LibreChat) | 33142 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/139558948?s=40&v=4" width="20" height="20" alt=""> &nbsp; [chatchat-space](https://github.com/chatchat-space) / [Langchain-Chatchat](https://github.com/chatchat-space/Langchain-Chatchat) | 32486 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/128289781?s=40&v=4" width="20" height="20" alt=""> &nbsp; [FlowiseAI](https://github.com/FlowiseAI) / [Flowise](https://github.com/FlowiseAI/Flowise) | 32448 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/31035808?s=40&v=4" width="20" height="20" alt=""> &nbsp; [mindsdb](https://github.com/mindsdb) / [mindsdb](https://github.com/mindsdb/mindsdb) | 26931 |
@@ -332,7 +333,6 @@ _[Langfuse 中的公共示例追踪](https://cloud.langfuse.com/project/cloramnk
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/171735272?s=40&v=4" width="20" height="20" alt=""> &nbsp; [8090-inc](https://github.com/8090-inc) / [xrx-sample-apps](https://github.com/8090-inc/xrx-sample-apps) | 138 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/104478511?s=40&v=4" width="20" height="20" alt=""> &nbsp; [babelcloud](https://github.com/babelcloud) / [LLM-RGB](https://github.com/babelcloud/LLM-RGB) | 135 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/15125613?s=40&v=4" width="20" height="20" alt=""> &nbsp; [souzatharsis](https://github.com/souzatharsis) / [tamingLLMs](https://github.com/souzatharsis/tamingLLMs) | 129 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/169401942?s=40&v=4" width="20" height="20" alt=""> &nbsp; [LibreChat-AI](https://github.com/LibreChat-AI) / [librechat.ai](https://github.com/LibreChat-AI/librechat.ai) | 128 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/51827949?s=40&v=4" width="20" height="20" alt=""> &nbsp; [deepset-ai](https://github.com/deepset-ai) / [haystack-core-integrations](https://github.com/deepset-ai/haystack-core-integrations) | 126 |
## 🔒 安全与隐私
+1 -1
View File
@@ -301,6 +301,7 @@ Langfuseを利用している主要なオープンソースPythonプロジェク
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/131470832?s=40&v=4" width="20" height="20" alt=""> &nbsp; [lobehub](https://github.com/lobehub) / [lobe-chat](https://github.com/lobehub/lobe-chat) | 49003 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/85702467?s=40&v=4" width="20" height="20" alt=""> &nbsp; [langflow-ai](https://github.com/langflow-ai) / [langflow](https://github.com/langflow-ai/langflow) | 39093 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/130722866?s=40&v=4" width="20" height="20" alt=""> &nbsp; [run-llama](https://github.com/run-llama) / [llama_index](https://github.com/run-llama/llama_index) | 37368 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/169401942?s=40&v=4" width="20" height="20" alt=""> &nbsp; [danny-avila](https://github.com/danny-avila) / [LibreChat](https://github.com/danny-avila/LibreChat) | 33142 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/139558948?s=40&v=4" width="20" height="20" alt=""> &nbsp; [chatchat-space](https://github.com/chatchat-space) / [Langchain-Chatchat](https://github.com/chatchat-space/Langchain-Chatchat) | 32486 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/128289781?s=40&v=4" width="20" height="20" alt=""> &nbsp; [FlowiseAI](https://github.com/FlowiseAI) / [Flowise](https://github.com/FlowiseAI/Flowise) | 32448 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/31035808?s=40&v=4" width="20" height="20" alt=""> &nbsp; [mindsdb](https://github.com/mindsdb) / [mindsdb](https://github.com/mindsdb/mindsdb) | 26931 |
@@ -338,7 +339,6 @@ Langfuseを利用している主要なオープンソースPythonプロジェク
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/171735272?s=40&v=4" width="20" height="20" alt=""> &nbsp; [8090-inc](https://github.com/8090-inc) / [xrx-sample-apps](https://github.com/8090-inc/xrx-sample-apps) | 138 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/104478511?s=40&v=4" width="20" height="20" alt=""> &nbsp; [babelcloud](https://github.com/babelcloud) / [LLM-RGB](https://github.com/babelcloud/LLM-RGB) | 135 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/15125613?s=40&v=4" width="20" height="20" alt=""> &nbsp; [souzatharsis](https://github.com/souzatharsis) / [tamingLLMs](https://github.com/souzatharsis/tamingLLMs) | 129 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/169401942?s=40&v=4" width="20" height="20" alt=""> &nbsp; [LibreChat-AI](https://github.com/LibreChat-AI) / [librechat.ai](https://github.com/LibreChat-AI/librechat.ai) | 128 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/51827949?s=40&v=4" width="20" height="20" alt=""> &nbsp; [deepset-ai](https://github.com/deepset-ai) / [haystack-core-integrations](https://github.com/deepset-ai/haystack-core-integrations) | 126 |
## 🔒 セキュリティとプライバシー
+1 -1
View File
@@ -283,6 +283,7 @@ _[Langfuse의 공개 예제 trace](https://cloud.langfuse.com/project/cloramnkj0
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/131470832?s=40&v=4" width="20" height="20" alt=""> &nbsp; [lobehub](https://github.com/lobehub) / [lobe-chat](https://github.com/lobehub/lobe-chat) | 49003 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/85702467?s=40&v=4" width="20" height="20" alt=""> &nbsp; [langflow-ai](https://github.com/langflow-ai) / [langflow](https://github.com/langflow-ai/langflow) | 39093 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/130722866?s=40&v=4" width="20" height="20" alt=""> &nbsp; [run-llama](https://github.com/run-llama) / [llama_index](https://github.com/run-llama/llama_index) | 37368 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/169401942?s=40&v=4" width="20" height="20" alt=""> &nbsp; [danny-avila](https://github.com/danny-avila) / [LibreChat](https://github.com/danny-avila/LibreChat) | 33142 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/139558948?s=40&v=4" width="20" height="20" alt=""> &nbsp; [chatchat-space](https://github.com/chatchat-space) / [Langchain-Chatchat](https://github.com/chatchat-space/Langchain-Chatchat) | 32486 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/128289781?s=40&v=4" width="20" height="20" alt=""> &nbsp; [FlowiseAI](https://github.com/FlowiseAI) / [Flowise](https://github.com/FlowiseAI/Flowise) | 32448 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/31035808?s=40&v=4" width="20" height="20" alt=""> &nbsp; [mindsdb](https://github.com/mindsdb) / [mindsdb](https://github.com/mindsdb/mindsdb) | 26931 |
@@ -320,7 +321,6 @@ _[Langfuse의 공개 예제 trace](https://cloud.langfuse.com/project/cloramnkj0
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/171735272?s=40&v=4" width="20" height="20" alt=""> &nbsp; [8090-inc](https://github.com/8090-inc) / [xrx-sample-apps](https://github.com/8090-inc/xrx-sample-apps) | 138 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/104478511?s=40&v=4" width="20" height="20" alt=""> &nbsp; [babelcloud](https://github.com/babelcloud) / [LLM-RGB](https://github.com/babelcloud/LLM-RGB) | 135 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/15125613?s=40&v=4" width="20" height="20" alt=""> &nbsp; [souzatharsis](https://github.com/souzatharsis) / [tamingLLMs](https://github.com/souzatharsis/tamingLLMs) | 129 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/169401942?s=40&v=4" width="20" height="20" alt=""> &nbsp; [LibreChat-AI](https://github.com/LibreChat-AI) / [librechat.ai](https://github.com/LibreChat-AI/librechat.ai) | 128 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/51827949?s=40&v=4" width="20" height="20" alt=""> &nbsp; [deepset-ai](https://github.com/deepset-ai) / [haystack-core-integrations](https://github.com/deepset-ai/haystack-core-integrations) | 126 |
## 🔒 보안 & 개인정보 보호
+12 -4
View File
@@ -71,13 +71,17 @@
<a href="./README.kr.md"><img alt="README in Korean" src="https://img.shields.io/badge/한국어-d9d9d9"></a>
</p>
<p align="center">
<a href="https://github.com/ClickHouse/ClickHouse"><strong>Proudly made with ClickHouse open source database</strong></a>
</p>
Langfuse is an **open source LLM engineering** platform. It helps teams collaboratively
**develop, monitor, evaluate,** and **debug** AI applications. Langfuse can be **self-hosted in minutes** and is **battle-tested**.
[![Langfuse Overview Video](https://github.com/user-attachments/assets/925d71db-6331-445e-8f3e-727ee95d1c9f)](https://langfuse.com/watch-demo)
## ✨ Core Features
<img width="4856" height="1944" alt="Langfuse Overview" src="https://github.com/user-attachments/assets/5dac68ef-d546-49fb-b06f-cfafc19282e3" />
@@ -144,7 +148,7 @@ See [self-hosting documentation](https://langfuse.com/self-hosting) to learn mor
| [Haystack](https://langfuse.com/docs/integrations/haystack) | Python | Automated instrumentation via Haystack content tracing system. |
| [LiteLLM](https://langfuse.com/docs/integrations/litellm) | Python, JS/TS (proxy only) | Use any LLM as a drop in replacement for GPT. Use Azure, OpenAI, Cohere, Anthropic, Ollama, VLLM, Sagemaker, HuggingFace, Replicate (100+ LLMs). |
| [Vercel AI SDK](https://langfuse.com/docs/integrations/vercel-ai-sdk) | JS/TS | TypeScript toolkit designed to help developers build AI-powered applications with React, Next.js, Vue, Svelte, Node.js. |
| [Mastra](https://langfuse.com/docs/integrations/mastra) | JS/TS | Open source framework for building AI agents and multi-agent systems. |
| [Mastra](https://langfuse.com/docs/integrations/mastra) | JS/TS | Open source framework for building AI agents and multi-agent systems. |
| [API](https://langfuse.com/docs/api) | | Directly call the public API. OpenAPI spec available. |
### Packages integrated with Langfuse:
@@ -259,6 +263,10 @@ Your contributions are welcome!
This repository is MIT licensed, except for the `ee` folders. See [LICENSE](LICENSE) and [docs](https://langfuse.com/docs/open-source) for more details.
## Dependencies
We deploy this code base in Docker containers based on the Linux Alpine Image ([source](https://github.com/nodejs/docker-node)). You may find the Dockerfiles in [web/Dockerfile](web/Dockerfile) and [worker/Dockerfile](worker/Dockerfile).
## ⭐️ Star History
<a href="https://star-history.com/#langfuse/langfuse&Date">
@@ -287,6 +295,7 @@ Top open-source Python projects that use Langfuse, ranked by stars ([Source](htt
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/6154722?s=40&v=4" width="20" height="20" alt=""> &nbsp; [microsoft](https://github.com/microsoft) / [ai-agents-for-beginners](https://github.com/microsoft/ai-agents-for-beginners) | 38012 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/139558948?s=40&v=4" width="20" height="20" alt=""> &nbsp; [chatchat-space](https://github.com/chatchat-space) / [Langchain-Chatchat](https://github.com/chatchat-space/Langchain-Chatchat) | 36071 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/31035808?s=40&v=4" width="20" height="20" alt=""> &nbsp; [mindsdb](https://github.com/mindsdb) / [mindsdb](https://github.com/mindsdb/mindsdb) | 35669 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/169401942?s=40&v=4" width="20" height="20" alt=""> &nbsp; [danny-avila](https://github.com/danny-avila) / [LibreChat](https://github.com/danny-avila/LibreChat) | 33142 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/121462774?s=40&v=4" width="20" height="20" alt=""> &nbsp; [BerriAI](https://github.com/BerriAI) / [litellm](https://github.com/BerriAI/litellm) | 28726 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/157326433?s=40&v=4" width="20" height="20" alt=""> &nbsp; [onlook-dev](https://github.com/onlook-dev) / [onlook](https://github.com/onlook-dev/onlook) | 22447 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/487568?s=40&v=4" width="20" height="20" alt=""> &nbsp; [NixOS](https://github.com/NixOS) / [nixpkgs](https://github.com/NixOS/nixpkgs) | 21748 |
@@ -342,7 +351,6 @@ Top open-source Python projects that use Langfuse, ranked by stars ([Source](htt
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/16997807?s=40&v=4" width="20" height="20" alt=""> &nbsp; [FranciscoMoretti](https://github.com/FranciscoMoretti) / [sparka](https://github.com/FranciscoMoretti/sparka) | 380 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/66303003?s=40&v=4" width="20" height="20" alt=""> &nbsp; [RobotecAI](https://github.com/RobotecAI) / [rai](https://github.com/RobotecAI/rai) | 373 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/137044109?s=40&v=4" width="20" height="20" alt=""> &nbsp; [ElectricCodeGuy](https://github.com/ElectricCodeGuy) / [SupabaseAuthWithSSR](https://github.com/ElectricCodeGuy/SupabaseAuthWithSSR) | 370 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/169401942?s=40&v=4" width="20" height="20" alt=""> &nbsp; [LibreChat-AI](https://github.com/LibreChat-AI) / [librechat.ai](https://github.com/LibreChat-AI/librechat.ai) | 339 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/15125613?s=40&v=4" width="20" height="20" alt=""> &nbsp; [souzatharsis](https://github.com/souzatharsis) / [tamingLLMs](https://github.com/souzatharsis/tamingLLMs) | 323 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/8931462?s=40&v=4" width="20" height="20" alt=""> &nbsp; [aws-samples](https://github.com/aws-samples) / [aws-ai-ml-workshop-kr](https://github.com/aws-samples/aws-ai-ml-workshop-kr) | 295 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/25676773?s=40&v=4" width="20" height="20" alt=""> &nbsp; [weizxfree](https://github.com/weizxfree) / [KnowFlow](https://github.com/weizxfree/KnowFlow) | 285 |
+8
View File
@@ -10,6 +10,7 @@
- Migrations in `packages/shared/clickhouse/migrations/clustered` should match their counterparts in `packages/shared/clickhouse/migrations/unclustered` aside from the restrictions listed above.
- When adding new indexes on ClickHouse, ensure that there is a corresponding `MATERIALIZE INDEX` statement in the same migration. The materialization can use `SETTINGS mutations_sync = 2` if they operate on smaller tables, but may timeout otherwise.
- All ClickHouse queries on project-scoped tables (traces, observations, scores, events, sessions, etc.) must include `WHERE project_id = {projectId: String}` filter to ensure proper tenant isolation and that queries only access data from the intended project.
- For operations on the `events` table, you must never use the `FINAL` keyword as it kills performance. `events` is built so that `FINAL` is never required.
### Postgres
@@ -45,3 +46,10 @@
## Seeder
- make sure that for new features with data model changes, the database seeder is adjusted.
## API Documentation
- Whenever a file in `web/src/features/public-api/types` changes, the `fern/apis` definition probably needs to be adjusted, too.
- `nullish` types should map to `optional<nullable<T>>` in fern.
- `nullable` types should map to `nullable<T>` in fern.
- `optional` types should map to `optional<T>` in fern.
BIN
View File
Binary file not shown.
+1 -1
View File
@@ -24,7 +24,7 @@ services:
SALT: ${SALT:-mysalt} # CHANGEME
ENCRYPTION_KEY: ${ENCRYPTION_KEY:-0000000000000000000000000000000000000000000000000000000000000000} # CHANGEME: generate via `openssl rand -hex 32`
TELEMETRY_ENABLED: ${TELEMETRY_ENABLED:-true}
LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES: ${LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES:-true}
LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES: ${LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES:-false}
CLICKHOUSE_MIGRATION_URL: ${CLICKHOUSE_MIGRATION_URL:-clickhouse://clickhouse:9000}
CLICKHOUSE_URL: ${CLICKHOUSE_URL:-http://clickhouse:8123}
CLICKHOUSE_USER: ${CLICKHOUSE_USER:-clickhouse}
-8
View File
@@ -1,8 +0,0 @@
/** @type {import("eslint").Linter.Config} */
module.exports = {
extends: ["@repo/eslint-config/library.js"],
parser: "@typescript-eslint/parser",
parserOptions: {
project: true,
},
};
+3
View File
@@ -0,0 +1,3 @@
import baseConfig from "@repo/eslint-config";
export default [...baseConfig];
+11 -8
View File
@@ -19,27 +19,30 @@
},
"scripts": {
"build": "tsc",
"build:check": "tsc",
"typecheck": "dotenv -e ../../.env -- tsgo --noEmit --skipLibCheck --incremental --tsBuildInfoFile .tsbuildinfo",
"dev": "tsc --watch",
"lint": "eslint . --ext .js,.jsx,.ts,.tsx --max-warnings 0",
"lint:fix": "eslint . --ext .js,.jsx,.ts,.tsx --fix"
"lint": "eslint . --cache --cache-location dist/.eslintcache --max-warnings 0",
"lint:fix": "eslint . --cache --cache-location dist/.eslintcache --fix"
},
"dependencies": {
"@langfuse/shared": "workspace:*",
"@opentelemetry/api": ">=1.0.0 <1.10.0",
"https-proxy-agent": "^7.0.6",
"next": "15.5.9",
"next-auth": "^4.24.12",
"next": "15.5.10",
"next-auth": "^4.24.13",
"zod": "^3.25.62"
},
"devDependencies": {
"@repo/eslint-config": "workspace:*",
"@repo/typescript-config": "workspace:*",
"@types/node": "^24.3.0",
"@typescript-eslint/parser": "^8.48.1",
"eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.0",
"@typescript-eslint/parser": "^8.50.1",
"@typescript/native-preview": "7.0.0-dev.20260122.3",
"eslint": "^9.39.2",
"eslint-config-prettier": "^10.1.8",
"eslint-config-standard": "^17.1.0",
"eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-prettier": "^5.5.4",
"prettier": "^3.6.2",
"ts-node": "^10.9.2",
"tsc-watch": "^6.2.0",
+5 -2
View File
@@ -18,7 +18,9 @@ types:
sessionId: optional<string>
observationId: optional<string>
datasetRunId: optional<string>
name: string
name:
type: string
docs: The name of the score. Always overrides "output" for correction scores.
value:
type: CreateScoreValue
docs: The value of the score. Must be passed as string for categorical scores, and numeric for boolean and numeric scores. Boolean score values must equal either 1 or 0 (true or false)
@@ -81,10 +83,11 @@ types:
- NUMERIC
- CATEGORICAL
- BOOLEAN
- CORRECTION
CreateScoreValue:
discriminated: false
union:
- double
- string
docs: The value of the score. Must be passed as string for categorical scores, and numeric for boolean and numeric scores
docs: The value of the score. Must be passed as string for categorical scores, and numeric for boolean and numeric scores. For corrections pass value as string and set data type to CORRECTION
+164 -89
View File
@@ -1,5 +1,6 @@
types:
# Objects
# Source: web/src/features/public-api/types/traces.ts - APITrace, APIExtendedTrace, GetTraceV1Response
Trace:
properties:
id:
@@ -9,7 +10,7 @@ types:
type: datetime
docs: The timestamp when the trace was created
name:
type: optional<string>
type: nullable<string>
docs: The name of the trace
input:
type: optional<unknown>
@@ -18,28 +19,28 @@ types:
type: optional<unknown>
docs: The output data of the trace. Can be any JSON.
sessionId:
type: optional<string>
type: nullable<string>
docs: The session identifier associated with the trace
release:
type: optional<string>
type: nullable<string>
docs: The release version of the application when the trace was created
version:
type: optional<string>
type: nullable<string>
docs: The version of the trace
userId:
type: optional<string>
type: nullable<string>
docs: The user identifier associated with the trace
metadata:
type: optional<unknown>
docs: The metadata associated with the trace. Can be any JSON.
tags:
type: optional<list<string>>
docs: The tags associated with the trace. Can be an array of strings or null.
type: list<string>
docs: The tags associated with the trace.
public:
type: optional<boolean>
type: boolean
docs: Public traces are accessible via url without login
environment:
type: optional<string>
type: string
docs: The environment from which this trace originated. Can be any lowercase alphanumeric string with hyphens and underscores that does not start with 'langfuse'.
TraceWithDetails: # GET /traces
extends: Trace
@@ -48,16 +49,16 @@ types:
type: string
docs: Path of trace in Langfuse UI
latency:
type: double
type: optional<nullable<double>>
docs: Latency of trace in seconds
totalCost:
type: double
type: optional<nullable<double>>
docs: Cost of trace in USD
observations:
type: list<string>
type: optional<nullable<list<string>>>
docs: List of observation ids
scores:
type: list<string>
type: optional<nullable<list<string>>>
docs: List of score ids
TraceWithFullDetails: # GET traces/[traceID]
extends: Trace
@@ -66,10 +67,10 @@ types:
type: string
docs: Path of trace in Langfuse UI
latency:
type: double
type: optional<nullable<double>>
docs: Latency of trace in seconds
totalCost:
type: double
type: optional<nullable<double>>
docs: Cost of trace in USD
observations:
type: list<ObservationsView>
@@ -77,119 +78,121 @@ types:
scores:
type: list<ScoreV1>
docs: List of scores
# Source: web/src/features/public-api/types/sessions.ts - APISession
Session:
properties:
id: string
createdAt: datetime
projectId: string
environment:
type: optional<string>
type: string
docs: The environment from which this session originated.
SessionWithTraces:
extends: Session
properties:
traces: list<Trace>
# Source: web/src/features/public-api/types/observations.ts - APIObservation
Observation:
properties:
id:
type: string
docs: The unique identifier of the observation
traceId:
type: optional<string>
type: nullable<string>
docs: The trace ID associated with the observation
type:
type: string
docs: The type of the observation
name:
type: optional<string>
type: nullable<string>
docs: The name of the observation
startTime:
type: datetime
docs: The start time of the observation
endTime:
type: optional<datetime>
type: nullable<datetime>
docs: The end time of the observation.
completionStartTime:
type: optional<datetime>
type: nullable<datetime>
docs: The completion start time of the observation
model:
type: optional<string>
type: nullable<string>
docs: The model used for the observation
modelParameters:
type: optional<map<string, MapValue>>
type: unknown
docs: The parameters of the model used for the observation
input:
type: optional<unknown>
type: unknown
docs: The input data of the observation
version:
type: optional<string>
type: nullable<string>
docs: The version of the observation
metadata:
type: optional<unknown>
type: unknown
docs: Additional metadata of the observation
output:
type: optional<unknown>
type: unknown
docs: The output data of the observation
usage:
type: optional<Usage>
type: Usage
docs: (Deprecated. Use usageDetails and costDetails instead.) The usage data of the observation
level:
type: ObservationLevel
docs: The level of the observation
statusMessage:
type: optional<string>
type: nullable<string>
docs: The status message of the observation
parentObservationId:
type: optional<string>
type: nullable<string>
docs: The parent observation ID
promptId:
type: optional<string>
type: nullable<string>
docs: The prompt ID associated with the observation
usageDetails:
type: optional<map<string, integer>>
type: map<string, integer>
docs: The usage details of the observation. Key is the name of the usage metric, value is the number of units consumed. The total key is the sum of all (non-total) usage metrics or the total value ingested.
costDetails:
type: optional<map<string, double>>
type: map<string, double>
docs: The cost details of the observation. Key is the name of the cost metric, value is the cost in USD. The total key is the sum of all (non-total) cost metrics or the total value ingested.
environment:
type: optional<string>
type: string
docs: The environment from which this observation originated. Can be any lowercase alphanumeric string with hyphens and underscores that does not start with 'langfuse'.
ObservationsView:
extends: Observation
properties:
promptName:
type: optional<string>
type: nullable<string>
docs: The name of the prompt associated with the observation
promptVersion:
type: optional<integer>
type: nullable<integer>
docs: The version of the prompt associated with the observation
modelId:
type: optional<string>
type: nullable<string>
docs: The unique identifier of the model
inputPrice:
type: optional<double>
type: nullable<double>
docs: The price of the input in USD
outputPrice:
type: optional<double>
type: nullable<double>
docs: The price of the output in USD.
totalPrice:
type: optional<double>
type: nullable<double>
docs: The total price in USD.
calculatedInputCost:
type: optional<double>
type: nullable<double>
docs: (Deprecated. Use usageDetails and costDetails instead.) The calculated cost of the input in USD
calculatedOutputCost:
type: optional<double>
type: nullable<double>
docs: (Deprecated. Use usageDetails and costDetails instead.) The calculated cost of the output in USD
calculatedTotalCost:
type: optional<double>
type: nullable<double>
docs: (Deprecated. Use usageDetails and costDetails instead.) The calculated total cost in USD
latency:
type: optional<double>
type: nullable<double>
docs: The latency in seconds.
timeToFirstToken:
type: optional<double>
type: nullable<double>
docs: The time to the first token in seconds
Usage:
@@ -197,14 +200,16 @@ types:
properties:
input:
docs: Number of input units (e.g. tokens)
type: optional<integer>
type: integer
output:
docs: Number of output units (e.g. tokens)
type: optional<integer>
type: integer
total:
docs: Defaults to input+output if not set
type: optional<integer>
unit: optional<ModelUsageUnit>
type: integer
unit:
docs: Unit of measurement
type: nullable<string>
inputCost:
docs: USD input cost
type: optional<double>
@@ -214,6 +219,7 @@ types:
totalCost:
docs: USD total cost, defaults to input+output
type: optional<double>
# Source: web/src/features/public-api/types/score-configs.ts - APIScoreConfig
ScoreConfig:
docs: Configuration for a score
properties:
@@ -222,45 +228,56 @@ types:
createdAt: datetime
updatedAt: datetime
projectId: string
dataType: ScoreDataType
dataType: ScoreConfigDataType
isArchived:
type: boolean
docs: Whether the score config is archived. Defaults to false
minValue:
type: optional<double>
type: optional<nullable<double>>
docs: Sets minimum value for numerical scores. If not set, the minimum value defaults to -∞
maxValue:
type: optional<double>
type: optional<nullable<double>>
docs: Sets maximum value for numerical scores. If not set, the maximum value defaults to +∞
categories:
type: optional<list<ConfigCategory>>
docs: Configures custom categories for categorical scores
description: optional<string>
description:
type: optional<nullable<string>>
docs: Description of the score config
ConfigCategory:
properties:
value: double
label: string
# Source: packages/shared/src/features/scores/interfaces/api/v1/schemas.ts - APIScoreSchemaV1
BaseScoreV1:
properties:
id: string
traceId: string
name: string
source: ScoreSource
observationId: optional<string>
observationId:
type: optional<nullable<string>>
docs: The observation ID associated with the score
timestamp: datetime
createdAt: datetime
updatedAt: datetime
authorUserId: optional<string>
comment: optional<string>
metadata: optional<unknown>
authorUserId:
type: nullable<string>
docs: The user ID of the author
comment:
type: nullable<string>
docs: Comment on the score
metadata:
type: unknown
docs: Metadata associated with the score
configId:
type: optional<string>
type: nullable<string>
docs: Reference a score config on a score. When set, config and score name must be equal and value must comply to optionally defined numerical range
queueId:
type: optional<string>
type: nullable<string>
docs: The annotation queue referenced by the score. Indicates if score was initially created while processing annotation queue.
environment:
type: optional<string>
type: string
docs: The environment from which this score originated. Can be any lowercase alphanumeric string with hyphens and underscores that does not start with 'langfuse'.
NumericScoreV1:
extends: BaseScoreV1
@@ -299,29 +316,44 @@ types:
type: BooleanScoreV1
docs: "Score with BOOLEAN data type"
# Source: packages/shared/src/features/scores/interfaces/api/v2/schemas.ts - APIScoreSchemaV2
BaseScore:
properties:
id: string
traceId: optional<string>
sessionId: optional<string>
observationId: optional<string>
datasetRunId: optional<string>
traceId:
type: optional<nullable<string>>
docs: The trace ID associated with the score
sessionId:
type: optional<nullable<string>>
docs: The session ID associated with the score
observationId:
type: optional<nullable<string>>
docs: The observation ID associated with the score
datasetRunId:
type: optional<nullable<string>>
docs: The dataset run ID associated with the score
name: string
source: ScoreSource
timestamp: datetime
createdAt: datetime
updatedAt: datetime
authorUserId: optional<string>
comment: optional<string>
metadata: optional<unknown>
authorUserId:
type: nullable<string>
docs: The user ID of the author
comment:
type: nullable<string>
docs: Comment on the score
metadata:
type: unknown
docs: Metadata associated with the score
configId:
type: optional<string>
type: nullable<string>
docs: Reference a score config on a score. When set, config and score name must be equal and value must comply to optionally defined numerical range
queueId:
type: optional<string>
type: nullable<string>
docs: The annotation queue referenced by the score. Indicates if score was initially created while processing annotation queue.
environment:
type: optional<string>
type: string
docs: The environment from which this score originated. Can be any lowercase alphanumeric string with hyphens and underscores that does not start with 'langfuse'.
NumericScore:
extends: BaseScore
@@ -347,6 +379,15 @@ types:
stringValue:
type: string
docs: The string representation of the score value. If no config is linked, can be any string. Otherwise, must map to a config category
CorrectionScore:
extends: BaseScore
properties:
value:
type: double
docs: The numeric value of the score. Always 0 for correction scores.
stringValue:
type: string
docs: The string representation of the correction content
Score:
discriminant: "dataType"
union:
@@ -359,6 +400,9 @@ types:
BOOLEAN:
type: BooleanScore
docs: "Score with BOOLEAN data type"
CORRECTION:
type: CorrectionScore
docs: "Score with CORRECTION data type"
CreateScoreValue:
discriminated: false
@@ -367,6 +411,7 @@ types:
- string
docs: The value of the score. Must be passed as string for categorical scores, and numeric for boolean and numeric scores
# Source: web/src/features/public-api/types/comments.ts - APIComment
Comment:
properties:
id: string
@@ -376,36 +421,55 @@ types:
objectType: CommentObjectType
objectId: string
content: string
authorUserId: optional<string>
authorUserId:
type: optional<nullable<string>>
docs: The user ID of the comment author
# Source: web/src/features/public-api/types/datasets.ts - APIDataset
Dataset:
properties:
id: string
name: string
description: optional<string>
metadata: optional<unknown>
description:
type: nullable<string>
docs: Description of the dataset
metadata:
type: unknown
docs: Metadata associated with the dataset
inputSchema:
type: optional<unknown>
type: nullable<unknown>
docs: JSON Schema for validating dataset item inputs
expectedOutputSchema:
type: optional<unknown>
type: nullable<unknown>
docs: JSON Schema for validating dataset item expected outputs
projectId: string
createdAt: datetime
updatedAt: datetime
# Source: web/src/features/public-api/types/datasets.ts - APIDatasetItem
DatasetItem:
properties:
id: string
status: DatasetStatus
input: optional<unknown>
expectedOutput: optional<unknown>
metadata: optional<unknown>
sourceTraceId: optional<string>
sourceObservationId: optional<string>
input:
type: unknown
docs: Input data for the dataset item
expectedOutput:
type: unknown
docs: Expected output for the dataset item
metadata:
type: unknown
docs: Metadata associated with the dataset item
sourceTraceId:
type: nullable<string>
docs: The trace ID that sourced this dataset item
sourceObservationId:
type: nullable<string>
docs: The observation ID that sourced this dataset item
datasetId: string
datasetName: string
createdAt: datetime
updatedAt: datetime
# Source: web/src/features/public-api/types/datasets.ts - APIDatasetRunItem
DatasetRunItem:
properties:
id: string
@@ -413,9 +477,12 @@ types:
datasetRunName: string
datasetItemId: string
traceId: string
observationId: optional<string>
observationId:
type: nullable<string>
docs: The observation ID associated with this run item
createdAt: datetime
updatedAt: datetime
# Source: web/src/features/public-api/types/datasets.ts - APIDatasetRun
DatasetRun:
properties:
id:
@@ -425,10 +492,10 @@ types:
type: string
docs: Name of the dataset run
description:
type: optional<string>
type: nullable<string>
docs: Description of the run
metadata:
type: optional<unknown>
type: unknown
docs: Metadata of the dataset run
datasetId:
type: string
@@ -446,6 +513,7 @@ types:
extends: DatasetRun
properties:
datasetRunItems: list<DatasetRunItem>
# Source: web/src/features/public-api/types/models.ts - APIModelDefinition
Model:
docs: |
Model definition used for transforming usage into USD cost and/or tokenization.
@@ -467,25 +535,25 @@ types:
type: string
startDate:
docs: Apply only to generations which are newer than this ISO date.
type: optional<datetime>
type: nullable<datetime>
unit:
docs: Unit used by this model.
type: optional<ModelUsageUnit>
type: optional<nullable<ModelUsageUnit>>
inputPrice:
docs: Deprecated. See 'prices' instead. Price (USD) per input unit
type: optional<double>
type: nullable<double>
outputPrice:
docs: Deprecated. See 'prices' instead. Price (USD) per output unit
type: optional<double>
type: nullable<double>
totalPrice:
docs: Deprecated. See 'prices' instead. Price (USD) per total unit. Cannot be set if input or output price is set.
type: optional<double>
type: nullable<double>
tokenizerId:
docs: Optional. Tokenizer to be applied to observations which match to this model. See docs for more details.
type: optional<string>
type: nullable<string>
tokenizerConfig:
docs: Optional. Configuration for the selected tokenizer. Needs to be JSON. See docs for more details.
type: optional<unknown>
type: unknown
isLangfuseManaged:
type: boolean
createdAt:
@@ -730,11 +798,18 @@ types:
- ANNOTATION
- API
- EVAL
ScoreConfigDataType:
enum:
- NUMERIC
- BOOLEAN
- CATEGORICAL
ScoreDataType:
enum:
- NUMERIC
- BOOLEAN
- CATEGORICAL
- CORRECTION
errors:
Error:
@@ -21,7 +21,9 @@ service:
type: string
response: commons.DatasetItem
list:
docs: Get dataset items
docs: |
Get dataset items. Optionally specify a version to get the items as they existed at that point in time.
Note: If version parameter is provided, datasetName must also be provided.
method: GET
path: /dataset-items
request:
@@ -30,6 +32,12 @@ service:
datasetName: optional<string>
sourceTraceId: optional<string>
sourceObservationId: optional<string>
version:
type: optional<datetime>
docs: |
ISO 8601 timestamp (RFC 3339, Section 5.6) in UTC (e.g., "2026-01-21T14:35:42Z").
If provided, returns state of dataset at this timestamp.
If not provided, returns the latest version. Requires datasetName to be specified.
page:
type: optional<integer>
docs: page number, starts at 1
@@ -44,6 +44,13 @@ types:
traceId:
type: optional<string>
docs: traceId should always be provided. For compatibility with older SDK versions it can also be inferred from the provided observationId.
datasetVersion:
type: optional<datetime>
docs: |
ISO 8601 timestamp (RFC 3339, Section 5.6) in UTC (e.g., "2026-01-21T14:35:42Z").
Specifies the dataset version to use for this experiment run.
If provided, the experiment will use dataset items as they existed at or before this timestamp.
If not provided, uses the latest version of dataset items.
PaginatedDatasetRunItems:
properties:
data: list<commons.DatasetRunItem>
+3 -1
View File
@@ -278,7 +278,9 @@ types:
sessionId: optional<string>
observationId: optional<string>
datasetRunId: optional<string>
name: string
name:
type: string
docs: The name of the score. Always overrides "output" for correction scores.
environment: optional<string>
queueId:
type: optional<string>
+3 -1
View File
@@ -9,7 +9,9 @@ service:
metrics:
docs: |
Get metrics from the Langfuse project using a query object.
Consider using the [v2 metrics endpoint](/api-reference#tag/metricsv2/GET/api/public/v2/metrics) for better performance.
For more details, see the [Metrics API documentation](https://langfuse.com/docs/metrics/features/metrics-api).
method: GET
path: /metrics
@@ -20,7 +20,7 @@ service:
- `basic` - name, level, statusMessage, version, environment, bookmarked, public, userId, sessionId
- `time` - completionStartTime, createdAt, updatedAt
- `io` - input, output
- `metadata` - metadata
- `metadata` - metadata (truncated to 200 chars by default, use `expandMetadata` to get full values)
- `model` - providedModelName, internalModelId, modelParameters
- `usage` - usageDetails, costDetails, totalCost
- `prompt` - promptId, promptName, promptVersion
@@ -43,6 +43,13 @@ service:
Available groups: core, basic, time, io, metadata, model, usage, prompt, metrics.
If not specified, `core` and `basic` field groups are returned.
Example: "basic,usage,model"
expandMetadata:
type: optional<string>
docs: |
Comma-separated list of metadata keys to return non-truncated.
By default, metadata values over 200 characters are truncated.
Use this parameter to retrieve full values for specific keys.
Example: "key1,key2"
limit:
type: optional<integer>
docs: Number of items to return per page. Maximum 1000, default 50.
+4 -1
View File
@@ -16,7 +16,10 @@ service:
docs: The unique langfuse identifier of an observation, can be an event, span or generation
response: commons.ObservationsView
getMany:
docs: Get a list of observations
docs: |
Get a list of observations.
Consider using the [v2 observations endpoint](/api-reference#tag/observationsv2/GET/api/public/v2/observations) for cursor-based pagination and field selection.
method: GET
path: /observations
request:
+18 -2
View File
@@ -44,8 +44,12 @@ service:
type: optional<map<string, unknown>>
docs: Optional metadata for the project
retention:
type: integer
docs: Number of days to retain data. Must be 0 or at least 3 days. Requires data-retention entitlement for non-zero values. Optional.
type: optional<integer>
docs: |
Number of days to retain data.
Must be 0 or at least 3 days.
Requires data-retention entitlement for non-zero values.
Optional. Will retain existing retention setting if omitted.
response: Project
delete:
@@ -102,10 +106,22 @@ types:
properties:
data: list<Project>
Organization:
properties:
id:
type: string
docs: The unique identifier of the organization
name:
type: string
docs: The name of the organization
Project:
properties:
id: string
name: string
organization:
type: Organization
docs: The organization this project belongs to
metadata:
type: map<string, unknown>
docs: Metadata for the project
@@ -55,7 +55,7 @@ types:
CreateScoreConfigRequest:
properties:
name: string
dataType: commons.ScoreDataType
dataType: commons.ScoreConfigDataType
categories:
type: optional<list<commons.ConfigCategory>>
docs: Configure custom categories for categorical scores. Pass a list of objects with `label` and `value` properties. Categories are autogenerated for boolean configs and cannot be passed
+20
View File
@@ -59,6 +59,9 @@ service:
traceId:
type: optional<string>
docs: Retrieve only scores with a specific traceId.
observationId:
type: optional<string>
docs: Comma-separated list of observation IDs to filter scores by.
queueId:
type: optional<string>
docs: Retrieve only scores with a specific annotation queueId.
@@ -69,6 +72,17 @@ service:
type: optional<string>
allow-multiple: true
docs: Only scores linked to traces that include all of these tags will be returned.
fields:
type: optional<string>
docs: "Comma-separated list of field groups to include in the response. Available field groups: 'score' (core score fields), 'trace' (trace properties: userId, tags, environment). If not specified, both 'score' and 'trace' are returned by default. Example: 'score' to exclude trace data, 'score,trace' to include both. Note: When filtering by trace properties (using userId or traceTags parameters), the 'trace' field group must be included, otherwise a 400 error will be returned."
filter:
type: optional<string>
docs: >
A JSON stringified array of filter objects. Each object requires type, column, operator, and value.
Supports filtering by score metadata using the stringObject type.
Example: [{"type":"stringObject","column":"metadata","key":"user_id","operator":"=","value":"abc123"}].
Supported types: stringObject (metadata key-value filtering), string, number, datetime, stringOptions, arrayOptions.
Supported operators for stringObject: =, contains, does not contain, starts with, ends with.
response: GetScoresResponse
get-by-id:
docs: Get a score (supports both trace and session scores)
@@ -108,12 +122,18 @@ types:
properties:
trace: optional<GetScoresResponseTraceData>
GetScoresResponseDataCorrection:
extends: commons.CorrectionScore
properties:
trace: optional<GetScoresResponseTraceData>
GetScoresResponseData:
discriminant: dataType
union:
NUMERIC: GetScoresResponseDataNumeric
CATEGORICAL: GetScoresResponseDataCategorical
BOOLEAN: GetScoresResponseDataBoolean
CORRECTION: GetScoresResponseDataCorrection
GetScoresResponse:
properties:
+8 -4
View File
@@ -1,6 +1,6 @@
{
"name": "langfuse",
"version": "3.141.0",
"version": "3.153.0",
"author": "engineering@langfuse.com",
"license": "MIT",
"private": true,
@@ -21,6 +21,9 @@
"dx-f": "pnpm i && pnpm run infra:dev:prune && pnpm run infra:dev:up --pull always && pnpm --filter=shared run db:reset:test && pnpm --filter=shared run db:reset -f && SKIP_CONFIRM=1 pnpm --filter=shared run ch:reset && pnpm --filter=shared run db:seed:examples && pnpm run dev",
"dx:skip-infra": "pnpm i && pnpm --filter=shared run db:reset:test && pnpm --filter=shared run db:reset && pnpm --filter=shared run ch:reset && pnpm --filter=shared run db:seed:examples && pnpm run dev",
"build": "turbo run build",
"build:check": "turbo run build:check",
"typecheck": "turbo run typecheck",
"tc": "turbo run typecheck",
"start": "turbo run start",
"dev": "turbo run dev",
"dev:worker": "turbo run dev --filter=worker",
@@ -40,7 +43,7 @@
"husky": "^9.1.7",
"prettier": "^3.6.2",
"release-it": "^19.0.4",
"turbo": "^2.6.3"
"turbo": "^2.7.6"
},
"release-it": {
"git": {
@@ -93,10 +96,11 @@
"tar-fs": "^2.1.2",
"rollup@^4.0.0": "^4.22.4",
"@types/node-fetch": "^2.6.13",
"glob": "^10.5.0"
"glob": "^10.5.0",
"qs": "6.14.1"
},
"patchedDependencies": {
"next-auth@4.24.12": "patches/next-auth@4.24.12.patch"
"next-auth@4.24.13": "patches/next-auth@4.24.13.patch"
}
}
}
+87
View File
@@ -0,0 +1,87 @@
import js from "@eslint/js";
import globals from "globals";
import tseslint from "typescript-eslint";
import turboConfig from "eslint-config-turbo/flat";
import eslintPluginPrettierRecommended from "eslint-plugin-prettier/recommended";
import "eslint-plugin-only-warn";
export default tseslint.config(
// Global ignores
{
name: "langfuse/ignores",
ignores: [
"**/node_modules/",
"**/dist/",
"**/build/",
"**/coverage/",
"**/.next/",
"**/.*",
"eslint.config.mjs",
],
},
// Base JS rules (same as eslint v8 library.js)
js.configs.recommended,
// Turbo monorepo rules
...turboConfig,
// Prettier (last for rule precedence)
eslintPluginPrettierRecommended,
// Global settings
{
name: "langfuse/base/globals",
languageOptions: {
ecmaVersion: 2022,
sourceType: "module",
globals: {
...globals.node,
...globals.es2021,
React: "readonly",
JSX: "readonly",
},
},
rules: {
"no-redeclare": "off",
"import/order": "off",
},
},
// TypeScript-specific - parser only + custom rules
// Note: Old library.js had no TS rules, only eslint:recommended
// Adding parser + plugin to support custom rules, but not extending recommended
{
name: "langfuse/base/typescript",
files: ["**/*.ts", "**/*.tsx", "**/*.mts", "**/*.cts"],
plugins: {
"@typescript-eslint": tseslint.plugin,
},
languageOptions: {
parser: tseslint.parser,
},
rules: {
"no-undef": "off", // TypeScript handles this
"no-dupe-class-members": "off", // TypeScript handles this (and supports overloads)
"no-unused-vars": "off", // Use @typescript-eslint/no-unused-vars instead
"no-restricted-globals": [
"error",
{
name: "redis",
message: "Import redis explicitly from '@langfuse/shared/src/server'",
},
],
// Custom rule from eslint v8 shared/.eslintrc.js
"@typescript-eslint/no-unused-vars": [
"warn",
{
argsIgnorePattern: "^_",
varsIgnorePattern: "^_",
caughtErrorsIgnorePattern: "^_",
destructuredArrayIgnorePattern: "^_",
ignoreRestSiblings: true,
},
],
},
},
);
+2
View File
@@ -0,0 +1,2 @@
// @ts-check
export { default } from "./base.js";
-62
View File
@@ -1,62 +0,0 @@
const { resolve } = require("node:path");
const project = resolve(process.cwd(), "tsconfig.json");
// Handle eslint-config-turbo's default export
const turboConfig = require("eslint-config-turbo");
const turboConfigToUse = turboConfig.default || turboConfig;
/** @type {import("eslint").Linter.Config} */
module.exports = {
// extends: ["eslint:recommended", "prettier"require().default],
extends: ["eslint:recommended", "prettier"],
plugins: ["only-warn", "turbo"],
globals: {
React: true,
JSX: true,
},
env: {
node: true,
es6: true,
},
settings: {
"import/resolver": {
typescript: {
project,
},
},
},
ignorePatterns: [
// Ignore dotfiles
".*.js",
"node_modules/",
"dist/",
],
rules: {
"no-redeclare": "off",
"import/order": "off",
...(turboConfigToUse.rules || {}),
},
overrides: [
{
files: ["*.js?(x)", "*.ts?(x)"],
},
{
files: ["*.ts", "*.mts", "*.cts", "*.tsx"],
// no-undef doesn't make sense in TS, see:
// https://typescript-eslint.io/troubleshooting/faqs/eslint/#i-get-errors-from-the-no-undef-rule-about-global-variables-not-being-defined-even-though-there-are-no-typescript-errors
rules: {
"no-undef": "off",
"no-restricted-globals": [
"error",
{
name: "redis",
message:
"Import redis explicitly from '@langfuse/shared/src/server' instead of using global.",
},
],
},
},
...(turboConfigToUse.overrides || []),
],
};
+82 -69
View File
@@ -1,79 +1,92 @@
const { resolve } = require("node:path");
import tseslint from "typescript-eslint";
import { FlatCompat } from "@eslint/eslintrc";
import { fileURLToPath } from "node:url";
import path from "node:path";
import eslintPluginPrettierRecommended from "eslint-plugin-prettier/recommended";
import turboConfig from "eslint-config-turbo/flat";
import "eslint-plugin-only-warn";
const project = resolve(process.cwd(), "tsconfig.json");
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const turboConfig = require("eslint-config-turbo");
const turboConfigToUse = turboConfig.default || turboConfig;
const compat = new FlatCompat({
baseDirectory: __dirname,
});
/*
* This is a custom ESLint configuration for use with
* Next.js apps.
*
* This config extends the Vercel Engineering Style Guide.
* For more information, see https://github.com/vercel/style-guide
*
*/
module.exports = {
parser: "@typescript-eslint/parser", // Set the parser to @typescript-eslint/parser
extends: [
"plugin:@typescript-eslint/recommended",
"plugin:@typescript-eslint/strict-type-checked",
],
rules: {
"@typescript-eslint/no-non-null-assertion": "off",
"@typescript-eslint/no-confusing-void-expression": "off",
export default tseslint.config(
// Global ignores - include config files
{
name: "langfuse/ignores",
ignores: [
"**/node_modules/",
"**/dist/",
"**/.next/",
"**/coverage/",
"eslint.config.mjs",
],
},
parser: "@typescript-eslint/parser",
parserOptions: {
project,
},
globals: {
React: true,
JSX: true,
},
plugins: ["@typescript-eslint"],
extends: ["next/core-web-vitals"],
env: {
es6: true,
jest: true,
},
settings: {
"import/resolver": {
typescript: {
project,
},
// Next.js rules via FlatCompat (applies to all files)
...compat.extends("next/core-web-vitals"),
// Turbo rules
...turboConfig,
// Disable noisy turbo env var rule - project has many env vars not in turbo.json
{
name: "langfuse/next/turbo-overrides",
rules: {
"turbo/no-undeclared-env-vars": "off",
},
},
ignorePatterns: ["node_modules/", "dist/"],
// add rules configurations here
rules: {
...(turboConfigToUse.rules || {}),
"@typescript-eslint/consistent-type-imports": [
"warn",
{
prefer: "type-imports",
fixStyle: "inline-type-imports",
// Prettier (last)
eslintPluginPrettierRecommended,
// TypeScript config for TS files
// Note: The old config had a bug (duplicate extends) that prevented TS rules from applying
// Only adding parser + plugin + custom rules to match old behavior
{
name: "langfuse/next/typescript",
files: ["**/*.ts", "**/*.tsx"],
plugins: {
"@typescript-eslint": tseslint.plugin,
},
languageOptions: {
parser: tseslint.parser,
globals: {
React: "readonly",
JSX: "readonly",
},
],
"@typescript-eslint/no-unused-vars": [
// see: https://typescript-eslint.io/rules/no-unused-vars/#why-does-this-rule-report-variables-used-only-for-types
// since v8, vars only used for types are unused at runtime and therefore throw a warning.
// we fix those with workarounds (mostly just export the variables, as they are used as part of an API anyways)
// see: https://github.com/typescript-eslint/typescript-eslint/issues/10266
"warn",
{
argsIgnorePattern: "^_",
varsIgnorePattern: "^_",
caughtErrorsIgnorePattern: "^_",
},
settings: {
"import/resolver": {
typescript: {
project: "./tsconfig.json",
},
},
],
"react/jsx-key": [
"error",
{
warnOnDuplicates: true,
},
],
},
rules: {
"no-unused-vars": "off", // Use @typescript-eslint/no-unused-vars instead
// Custom rules from old config
"@typescript-eslint/consistent-type-imports": [
"warn",
{
prefer: "type-imports",
fixStyle: "inline-type-imports",
},
],
"@typescript-eslint/no-unused-vars": [
"warn",
{
argsIgnorePattern: "^_",
varsIgnorePattern: "^_",
caughtErrorsIgnorePattern: "^_",
destructuredArrayIgnorePattern: "^_",
ignoreRestSiblings: true,
},
],
"react/jsx-key": ["error", { warnOnDuplicates: true }],
},
},
};
);
+22 -9
View File
@@ -3,18 +3,31 @@
"version": "0.0.0",
"license": "MIT",
"private": true,
"type": "module",
"exports": {
".": "./index.js",
"./base": "./base.js",
"./next": "./next.js"
},
"files": [
"library.js",
"index.js",
"base.js",
"next.js"
],
"devDependencies": {
"@typescript-eslint/eslint-plugin": "^8.48.1",
"@typescript-eslint/parser": "^8.48.1",
"@vercel/style-guide": "^6.0.0",
"eslint-config-next": "15.5.9",
"eslint-config-prettier": "^9.1.0",
"eslint-config-turbo": "^2.6.3",
"dependencies": {
"@eslint/compat": "^2.0.0",
"@eslint/eslintrc": "^3.3.3",
"@eslint/js": "^9.39.2",
"eslint-config-next": "15.5.10",
"eslint-config-prettier": "^10.1.8",
"eslint-config-turbo": "^2.7.6",
"eslint-plugin-only-warn": "^1.1.0",
"typescript": "^5.7.2"
"eslint-plugin-prettier": "^5.5.4",
"globals": "^16.0.0",
"typescript-eslint": "^8.50.1"
},
"peerDependencies": {
"eslint": "^9.39.0",
"typescript": ">=5.0.0"
}
}
-8
View File
@@ -1,8 +0,0 @@
/** @type {import("eslint").Linter.Config} */
module.exports = {
extends: ["@repo/eslint-config/library.js"],
parser: "@typescript-eslint/parser",
parserOptions: {
project: true,
},
};
@@ -0,0 +1 @@
ALTER TABLE scores ON CLUSTER default DROP COLUMN IF EXISTS long_string_value;
@@ -0,0 +1 @@
ALTER TABLE scores ON CLUSTER default ADD COLUMN IF NOT EXISTS long_string_value String CODEC(ZSTD(3));
@@ -0,0 +1 @@
ALTER TABLE scores DROP COLUMN IF EXISTS long_string_value;
@@ -0,0 +1 @@
ALTER TABLE scores ADD COLUMN IF NOT EXISTS long_string_value String CODEC(ZSTD(3));
@@ -75,6 +75,8 @@ clickhouse client \
-- Create observations_batch_staging table for batch processing
-- This table uses 3-minute partitions to efficiently process observations in batches
-- and merge them with traces data into the events table.
-- Partitions are automatically expired after 12 hours via TTL (ttl_only_drop_parts=1
-- ensures only complete partitions are dropped, not individual rows).
-- See LFE-7122 for implementation details.
CREATE TABLE IF NOT EXISTS observations_batch_staging
(
@@ -123,7 +125,9 @@ ORDER BY (
toDate(s3_first_seen_timestamp),
trace_id,
id
);
)
TTL s3_first_seen_timestamp + INTERVAL 12 HOUR
SETTINGS ttl_only_drop_parts = 1;
-- Create new events table for development setups.
-- We expect this to be fully immutable and eventually replace observations.
+3
View File
@@ -0,0 +1,3 @@
import baseConfig from "@repo/eslint-config";
export default [...baseConfig];
+24 -15
View File
@@ -25,16 +25,26 @@
"import": "./dist/src/server/auth/apiKeys.js",
"require": "./dist/src/server/auth/apiKeys.js"
},
"./src/utils/chatml": {
"import": "./dist/src/utils/chatml/index.js",
"require": "./dist/src/utils/chatml/index.js"
},
"./encryption": {
"import": "./dist/src/encryption/index.js",
"require": "./dist/src/encryption/index.js"
},
"./src/server/ee/ingestionMasking": {
"import": "./dist/src/server/ee/ingestionMasking/index.js",
"require": "./dist/src/server/ee/ingestionMasking/index.js"
}
},
"scripts": {
"build": "tsc",
"build:check": "tsc",
"typecheck": "dotenv -e ../../.env -- tsc --noEmit --incremental --tsBuildInfoFile .tsbuildinfo",
"dev": "tsc --watch",
"lint": "eslint . --ext .js,.jsx,.ts,.tsx --max-warnings 0",
"lint:fix": "eslint . --ext .js,.jsx,.ts,.tsx --fix",
"lint": "eslint . --cache --cache-location dist/.eslintcache --max-warnings 0",
"lint:fix": "eslint . --cache --cache-location dist/.eslintcache --fix",
"db:migrate": "DISABLE_ERD=false dotenv -e ../../.env -- npx prisma migrate dev",
"db:push": "DISABLE_ERD=false dotenv -e ../../.env -- npx prisma db push",
"db:reset": "dotenv -e ../../.env npx -- prisma migrate reset",
@@ -63,12 +73,12 @@
"@aws-sdk/s3-request-presigner": "^3.679.0",
"@azure/storage-blob": "^12.26.0",
"@clickhouse/client": "^1.13.0",
"@google-cloud/storage": "^7.17.0",
"@google-cloud/storage": "^7.18.0",
"@langchain/anthropic": "^0.3.32",
"@langchain/aws": "^0.1.15",
"@langchain/core": "^0.3.58",
"@langchain/google-genai": "^0.2.12",
"@langchain/google-vertexai": "^0.2.12",
"@langchain/core": "^0.3.80",
"@langchain/google-genai": "^0.2.18",
"@langchain/google-vertexai": "^0.2.18",
"@langchain/openai": "^0.5.13",
"@opentelemetry/api": ">=1.0.0 <1.10.0",
"@prisma/client": "^6.17.1",
@@ -90,11 +100,11 @@
"ipaddr.js": "^2.2.0",
"jsonpath-plus": "10.3.0",
"kysely": "^0.27.4",
"langchain": "^0.3.30",
"langchain": "^0.3.37",
"langfuse-langchain": "3.38.6",
"lodash": "^4.17.21",
"lodash": "^4.17.23",
"lossless-json": "^4.1.1",
"next-auth": "^4.24.12",
"next-auth": "^4.24.13",
"nodemailer": "^7.0.11",
"prisma-extension-kysely": "^3.0.0",
"safe-regex2": "^5.0.0",
@@ -106,21 +116,20 @@
"devDependencies": {
"@repo/eslint-config": "workspace:*",
"@repo/typescript-config": "workspace:*",
"@types/lodash": "^4.17.10",
"@types/lodash": "^4.17.23",
"@types/node": "^24.3.0",
"@types/nodemailer": "^7.0.4",
"@types/pg": "^8.11.10",
"@types/react": "19.2.3",
"@types/uuid": "^9.0.8",
"@typescript-eslint/parser": "^8.48.1",
"eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.0",
"@typescript-eslint/parser": "^8.50.1",
"eslint": "^9.39.2",
"eslint-config-prettier": "^10.1.8",
"eslint-config-standard": "^17.1.0",
"eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-prettier": "^5.5.4",
"kysely-codegen": "^0.16.8",
"prettier": "^3.6.2",
"prisma": "^6.17.1",
"prisma-erd-generator": "^2.1.0",
"prisma-kysely": "^1.8.0",
"ts-node": "^10.9.2",
"tsc-watch": "^6.2.0",
+23 -20
View File
@@ -47,12 +47,13 @@ export const LegacyPrismaScoreSource = {
} as const;
export type LegacyPrismaScoreSource =
(typeof LegacyPrismaScoreSource)[keyof typeof LegacyPrismaScoreSource];
export const ScoreDataType = {
export const ScoreConfigDataType = {
CATEGORICAL: "CATEGORICAL",
NUMERIC: "NUMERIC",
BOOLEAN: "BOOLEAN",
} as const;
export type ScoreDataType = (typeof ScoreDataType)[keyof typeof ScoreDataType];
export type ScoreConfigDataType =
(typeof ScoreConfigDataType)[keyof typeof ScoreConfigDataType];
export const AnnotationQueueStatus = {
PENDING: "PENDING",
COMPLETED: "COMPLETED",
@@ -135,6 +136,13 @@ export const BlobStorageExportMode = {
} as const;
export type BlobStorageExportMode =
(typeof BlobStorageExportMode)[keyof typeof BlobStorageExportMode];
export const AnalyticsIntegrationExportSource = {
TRACES_OBSERVATIONS: "TRACES_OBSERVATIONS",
TRACES_OBSERVATIONS_EVENTS: "TRACES_OBSERVATIONS_EVENTS",
EVENTS: "EVENTS",
} as const;
export type AnalyticsIntegrationExportSource =
(typeof AnalyticsIntegrationExportSource)[keyof typeof AnalyticsIntegrationExportSource];
export const DashboardWidgetViews = {
TRACES: "TRACES",
OBSERVATIONS: "OBSERVATIONS",
@@ -145,6 +153,7 @@ export type DashboardWidgetViews =
(typeof DashboardWidgetViews)[keyof typeof DashboardWidgetViews];
export const DashboardWidgetChartType = {
LINE_TIME_SERIES: "LINE_TIME_SERIES",
AREA_TIME_SERIES: "AREA_TIME_SERIES",
BAR_TIME_SERIES: "BAR_TIME_SERIES",
HORIZONTAL_BAR: "HORIZONTAL_BAR",
VERTICAL_BAR: "VERTICAL_BAR",
@@ -358,6 +367,7 @@ export type BlobStorageIntegration = {
file_type: Generated<BlobStorageIntegrationFileType>;
export_mode: Generated<BlobStorageExportMode>;
export_start_date: Timestamp | null;
export_source: Generated<AnalyticsIntegrationExportSource>;
created_at: Generated<Timestamp>;
updated_at: Generated<Timestamp>;
};
@@ -379,6 +389,10 @@ export type Comment = {
updated_at: Generated<Timestamp>;
content: string;
author_user_id: string | null;
data_field: string | null;
path: Generated<string[]>;
range_start: Generated<number[]>;
range_end: Generated<number[]>;
};
export type CommentReaction = {
id: string;
@@ -447,25 +461,10 @@ export type DatasetItem = {
dataset_id: string;
created_at: Generated<Timestamp>;
updated_at: Generated<Timestamp>;
sys_id: Generated<string | null>;
valid_from: Generated<Timestamp>;
valid_to: Timestamp | null;
is_deleted: Generated<boolean>;
};
export type DatasetItemEvent = {
id: string;
item_id: string;
project_id: string;
dataset_id: string;
status: DatasetStatus | null;
input: unknown | null;
expected_output: unknown | null;
metadata: unknown | null;
source_trace_id: string | null;
source_observation_id: string | null;
created_at: Timestamp | null;
deleted_at: Timestamp | null;
};
export type DatasetRunItems = {
id: string;
project_id: string;
@@ -543,6 +542,7 @@ export type JobExecution = {
job_input_trace_timestamp: Timestamp | null;
job_input_observation_id: string | null;
job_input_dataset_item_id: string | null;
job_input_dataset_item_valid_from: Timestamp | null;
job_output_score_id: string | null;
execution_trace_id: string | null;
};
@@ -596,7 +596,7 @@ export type LegacyPrismaScore = {
queue_id: string | null;
created_at: Generated<Timestamp>;
updated_at: Generated<Timestamp>;
data_type: Generated<ScoreDataType>;
data_type: Generated<ScoreConfigDataType>;
};
export type LegacyPrismaTrace = {
id: string;
@@ -683,6 +683,7 @@ export type MixpanelIntegration = {
last_sync_at: Timestamp | null;
enabled: boolean;
created_at: Generated<Timestamp>;
export_source: Generated<AnalyticsIntegrationExportSource>;
};
export type Model = {
id: string;
@@ -756,6 +757,7 @@ export type PosthogIntegration = {
last_sync_at: Timestamp | null;
enabled: boolean;
created_at: Generated<Timestamp>;
export_source: Generated<AnalyticsIntegrationExportSource>;
};
export type Price = {
id: string;
@@ -785,6 +787,7 @@ export type Project = {
deleted_at: Timestamp | null;
name: string;
retention_days: number | null;
has_traces: Generated<boolean>;
metadata: unknown | null;
};
export type ProjectMembership = {
@@ -834,7 +837,7 @@ export type ScoreConfig = {
updated_at: Generated<Timestamp>;
project_id: string;
name: string;
data_type: ScoreDataType;
data_type: ScoreConfigDataType;
is_archived: Generated<boolean>;
min_value: number | null;
max_value: number | null;
@@ -924,6 +927,7 @@ export type User = {
password: string | null;
image: string | null;
admin: Generated<boolean>;
v4_beta_enabled: Generated<boolean>;
created_at: Generated<Timestamp>;
updated_at: Generated<Timestamp>;
feature_flags: Generated<string[]>;
@@ -954,7 +958,6 @@ export type DB = {
cron_jobs: CronJobs;
dashboard_widgets: DashboardWidget;
dashboards: Dashboard;
dataset_item_events: DatasetItemEvent;
dataset_items: DatasetItem;
dataset_run_items: DatasetRunItems;
dataset_runs: DatasetRuns;
@@ -0,0 +1,8 @@
/*
Warnings:
- Changed the type of `data_type` on the `score_configs` table. No cast exists, the column would be dropped and recreated, which cannot be done if there is data, since the column is required.
*/
-- Rename enum
ALTER TYPE "ScoreDataType" RENAME TO "ScoreConfigDataType";
@@ -0,0 +1,2 @@
INSERT INTO background_migrations (id, name, script, args)
VALUES ('01a0c890-2094-8e2f-c773-cf4d14638fa4', '20260106_encrypt_blob_storage_secrets', 'encryptBlobStorageSecrets', '{}');
@@ -0,0 +1,14 @@
-- Add inline comment positioning columns to comments table
-- These columns enable character-based inline comments on trace/observation IO
-- data_field: which IO field the comment is on ('input' | 'output' | 'metadata')
ALTER TABLE "comments" ADD COLUMN "data_field" TEXT;
-- path: JSON Path expressions array, e.g., ["$.messages[1].text"]
ALTER TABLE "comments" ADD COLUMN "path" TEXT[] DEFAULT '{}';
-- range_start: start offsets per path (inclusive), UTF-16 code units
ALTER TABLE "comments" ADD COLUMN "range_start" INTEGER[] DEFAULT '{}';
-- range_end: end offsets per path (exclusive), UTF-16 code units
ALTER TABLE "comments" ADD COLUMN "range_end" INTEGER[] DEFAULT '{}';
@@ -0,0 +1,9 @@
/*
Warnings:
- You are about to drop the column `sys_id` on the `dataset_items` table. All the data in the column will be lost.
*/
-- AlterTable
ALTER TABLE "dataset_items" DROP COLUMN "sys_id";
@@ -0,0 +1,3 @@
-- DropForeignKey
ALTER TABLE "dataset_item_events" DROP CONSTRAINT "dataset_item_events_dataset_id_project_id_fkey";
@@ -0,0 +1,8 @@
/*
Warnings:
- You are about to drop the `dataset_item_events` table. If the table is not empty, all the data it contains will be lost.
*/
-- DropTable
DROP TABLE "dataset_item_events";
@@ -0,0 +1,11 @@
-- CreateEnum
CREATE TYPE "AnalyticsIntegrationExportSource" AS ENUM ('TRACES_OBSERVATIONS', 'TRACES_OBSERVATIONS_EVENTS', 'EVENTS');
-- AlterTable
ALTER TABLE "blob_storage_integrations" ADD COLUMN "export_source" "AnalyticsIntegrationExportSource" NOT NULL DEFAULT 'TRACES_OBSERVATIONS';
-- AlterTable
ALTER TABLE "mixpanel_integrations" ADD COLUMN "export_source" "AnalyticsIntegrationExportSource" NOT NULL DEFAULT 'TRACES_OBSERVATIONS';
-- AlterTable
ALTER TABLE "posthog_integrations" ADD COLUMN "export_source" "AnalyticsIntegrationExportSource" NOT NULL DEFAULT 'TRACES_OBSERVATIONS';
@@ -0,0 +1,2 @@
-- CreateIndex
CREATE INDEX CONCURRENTLY "media_project_id_created_at_idx" ON "media"("project_id", "created_at");
@@ -0,0 +1 @@
ALTER TABLE "users" ADD COLUMN "v4_beta_enabled" BOOLEAN NOT NULL DEFAULT false;
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "job_executions" ADD COLUMN "job_input_dataset_item_valid_from" TIMESTAMP(3);
@@ -0,0 +1,5 @@
-- DropIndex
DROP INDEX "public"."pending_deletions_project_id_object_is_deleted_idx";
-- CreateIndex
CREATE INDEX "pending_deletions_project_id_object_is_deleted_object_id_id_idx" ON "pending_deletions"("project_id", "object", "is_deleted", "object_id", "id");
@@ -0,0 +1 @@
ALTER TABLE "projects" ADD COLUMN "has_traces" BOOLEAN NOT NULL DEFAULT false;
@@ -0,0 +1 @@
ALTER TYPE "DashboardWidgetChartType" ADD VALUE 'AREA_TIME_SERIES';
+57 -60
View File
@@ -13,14 +13,6 @@ datasource db {
shadowDatabaseUrl = env("SHADOW_DATABASE_URL")
}
generator erd {
provider = "prisma-erd-generator"
ignoreTables = ["_prisma_migrations", "Session", "Account", "Example"]
disabled = true
ignoreEnums = true
output = "database.svg"
}
generator kysely {
provider = "prisma-kysely"
@@ -72,6 +64,7 @@ model User {
password String?
image String?
admin Boolean @default(false)
v4BetaEnabled Boolean @default(false) @map("v4_beta_enabled")
accounts Account[]
sessions Session[]
organizationMemberships OrganizationMembership[]
@@ -135,6 +128,7 @@ model Project {
deletedAt DateTime? @map("deleted_at")
name String
retentionDays Int? @map("retention_days")
hasTraces Boolean @default(false) @map("has_traces")
metadata Json?
projectMembers ProjectMembership[]
organization Organization @relation(fields: [orgId], references: [id], onUpdate: Cascade, onDelete: Cascade)
@@ -457,7 +451,7 @@ model LegacyPrismaScore {
queueId String? @map("queue_id")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
dataType ScoreDataType @default(NUMERIC) @map("data_type")
dataType ScoreConfigDataType @default(NUMERIC) @map("data_type")
scoreConfig ScoreConfig? @relation(fields: [configId], references: [id], onDelete: SetNull)
@@unique([id, projectId]) // used for upserts via prisma
@@ -488,7 +482,7 @@ model ScoreConfig {
projectId String @map("project_id")
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
name String
dataType ScoreDataType @map("data_type")
dataType ScoreConfigDataType @map("data_type")
isArchived Boolean @default(false) @map("is_archived")
minValue Float? @map("min_value")
maxValue Float? @map("max_value")
@@ -506,7 +500,7 @@ model ScoreConfig {
@@map("score_configs")
}
enum ScoreDataType {
enum ScoreConfigDataType {
CATEGORICAL
NUMERIC
BOOLEAN
@@ -592,21 +586,20 @@ model CronJobs {
}
model Dataset {
id String @default(cuid())
projectId String @map("project_id")
id String @default(cuid())
projectId String @map("project_id")
name String
description String?
metadata Json?
remoteExperimentUrl String? @map("remote_experiment_url")
remoteExperimentPayload Json? @map("remote_experiment_payload")
inputSchema Json? @map("input_schema") @db.Json
expectedOutputSchema Json? @map("expected_output_schema") @db.Json
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
remoteExperimentUrl String? @map("remote_experiment_url")
remoteExperimentPayload Json? @map("remote_experiment_payload")
inputSchema Json? @map("input_schema") @db.Json
expectedOutputSchema Json? @map("expected_output_schema") @db.Json
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
datasetItems DatasetItem[]
datasetRuns DatasetRuns[]
datasetItemEvents DatasetItemEvent[]
@@id([id, projectId])
@@unique([projectId, name])
@@ -629,7 +622,6 @@ model DatasetItem {
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
// dataset version cols
sysId String? @default(dbgenerated()) @map("sys_id")
validFrom DateTime @default(now()) @map("valid_from")
validTo DateTime? @map("valid_to")
isDeleted Boolean @default(false) @map("is_deleted")
@@ -645,25 +637,6 @@ model DatasetItem {
@@map("dataset_items")
}
model DatasetItemEvent {
id String @id @default(cuid())
itemId String @map("item_id")
projectId String @map("project_id")
datasetId String @map("dataset_id")
dataset Dataset @relation(fields: [datasetId, projectId], references: [id, projectId], onDelete: Cascade)
status DatasetStatus?
input Json?
expectedOutput Json? @map("expected_output")
metadata Json?
sourceTraceId String? @map("source_trace_id")
sourceObservationId String? @map("source_observation_id")
createdAt DateTime? @map("created_at")
deletedAt DateTime? @map("deleted_at")
@@index([projectId, datasetId, itemId, createdAt])
@@map("dataset_item_events")
}
enum DatasetStatus {
ACTIVE
ARCHIVED
@@ -722,6 +695,15 @@ model Comment {
authorUserId String? @map("author_user_id") // no fk constraint, user can be deleted
reactions CommentReaction[]
// Inline comment positioning (all must be set together or all null/empty)
// dataField: which IO field the comment is on ('input' | 'output' | 'metadata')
// path: Array of JSON Path expressions, e.g., ["$.messages[1].text"]
// rangeStart/rangeEnd: parallel arrays for start/end offsets per path (exclusive end, UTF-16 code units)
dataField String? @map("data_field")
path String[] @default([]) @map("path")
rangeStart Int[] @default([]) @map("range_start")
rangeEnd Int[] @default([]) @map("range_end")
@@index([projectId, objectType, objectId])
@@map("comments")
}
@@ -1032,6 +1014,8 @@ model JobExecution {
jobInputDatasetItemId String? @map("job_input_dataset_item_id") // no fk constraint - job execution sensible standalone
jobInputDatasetItemValidFrom DateTime? @map("job_input_dataset_item_valid_from")
jobOutputScoreId String? @map("job_output_score_id")
executionTraceId String? @map("execution_trace_id")
@@ -1079,25 +1063,27 @@ model SsoConfig {
}
model PosthogIntegration {
projectId String @id @map("project_id")
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
encryptedPosthogApiKey String @map("encrypted_posthog_api_key")
posthogHostName String @map("posthog_host_name")
lastSyncAt DateTime? @map("last_sync_at")
projectId String @id @map("project_id")
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
encryptedPosthogApiKey String @map("encrypted_posthog_api_key")
posthogHostName String @map("posthog_host_name")
lastSyncAt DateTime? @map("last_sync_at")
enabled Boolean
createdAt DateTime @default(now()) @map("created_at")
createdAt DateTime @default(now()) @map("created_at")
exportSource AnalyticsIntegrationExportSource @default(TRACES_OBSERVATIONS) @map("export_source")
@@map("posthog_integrations")
}
model MixpanelIntegration {
projectId String @id @map("project_id")
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
encryptedMixpanelProjectToken String @map("encrypted_mixpanel_project_token")
mixpanelRegion String @map("mixpanel_region")
lastSyncAt DateTime? @map("last_sync_at")
projectId String @id @map("project_id")
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
encryptedMixpanelProjectToken String @map("encrypted_mixpanel_project_token")
mixpanelRegion String @map("mixpanel_region")
lastSyncAt DateTime? @map("last_sync_at")
enabled Boolean
createdAt DateTime @default(now()) @map("created_at")
createdAt DateTime @default(now()) @map("created_at")
exportSource AnalyticsIntegrationExportSource @default(TRACES_OBSERVATIONS) @map("export_source")
@@map("mixpanel_integrations")
}
@@ -1117,13 +1103,14 @@ model BlobStorageIntegration {
forcePathStyle Boolean @map("force_path_style")
// Integration Config
nextSyncAt DateTime? @map("next_sync_at")
lastSyncAt DateTime? @map("last_sync_at")
nextSyncAt DateTime? @map("next_sync_at")
lastSyncAt DateTime? @map("last_sync_at")
enabled Boolean
exportFrequency String @map("export_frequency")
fileType BlobStorageIntegrationFileType @default(CSV) @map("file_type")
exportMode BlobStorageExportMode @default(FULL_HISTORY) @map("export_mode")
exportStartDate DateTime? @map("export_start_date")
exportFrequency String @map("export_frequency")
fileType BlobStorageIntegrationFileType @default(CSV) @map("file_type")
exportMode BlobStorageExportMode @default(FULL_HISTORY) @map("export_mode")
exportStartDate DateTime? @map("export_start_date")
exportSource AnalyticsIntegrationExportSource @default(TRACES_OBSERVATIONS) @map("export_source")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
@@ -1155,6 +1142,14 @@ enum BlobStorageExportMode {
@@map("BlobStorageExportMode")
}
enum AnalyticsIntegrationExportSource {
TRACES_OBSERVATIONS
TRACES_OBSERVATIONS_EVENTS
EVENTS
@@map("AnalyticsIntegrationExportSource")
}
model BatchExport {
id String @id @default(cuid())
createdAt DateTime @default(now()) @map("created_at")
@@ -1227,6 +1222,7 @@ model Media {
@@unique([projectId, id])
@@unique([projectId, sha256Hash])
@@index([projectId, createdAt])
@@map("media")
}
@@ -1350,6 +1346,7 @@ enum DashboardWidgetViews {
enum DashboardWidgetChartType {
LINE_TIME_SERIES
AREA_TIME_SERIES
BAR_TIME_SERIES
HORIZONTAL_BAR
VERTICAL_BAR
@@ -1555,7 +1552,7 @@ model PendingDeletion {
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
@@index([projectId, object, isDeleted])
@@index([projectId, object, isDeleted, objectId, id])
@@index([objectId, object])
@@map("pending_deletions")
}
@@ -0,0 +1,285 @@
/**
* Media Seeding Utilities
*
* Seeds test traces with media attachments (images, PDFs, audio) for testing
* the JSON Beta view's media rendering capabilities.
*
* Integrated into the seeder flow - runs automatically during `dx` or `db:seed:examples`.
*/
import crypto from "crypto";
import fs from "fs";
import path from "path";
import { Readable } from "stream";
import { prisma } from "../../src/db";
import { env } from "../../src/env";
import { logger, StorageServiceFactory } from "../../src/server";
// Test file paths (relative to monorepo root)
const TEST_FILES_DIR = path.join(
__dirname,
"../../../../web/src/__tests__/static",
);
interface MediaFile {
name: string;
contentType: string;
filePath: string;
}
const MEDIA_FILES: Record<string, MediaFile> = {
image: {
name: "langfuse-logo.png",
contentType: "image/png",
filePath: path.join(TEST_FILES_DIR, "langfuse-logo.png"),
},
pdf: {
name: "bitcoin.pdf",
contentType: "application/pdf",
filePath: path.join(TEST_FILES_DIR, "bitcoin.pdf"),
},
audio: {
name: "sounds-of-mars.wav",
contentType: "audio/wav",
filePath: path.join(
TEST_FILES_DIR,
"sounds-of-mars-one-small-step-earth.wav",
),
},
};
// Deterministic trace IDs for media test traces
export const MEDIA_TEST_TRACE_IDS = {
imageOnly: "seed-media-image-only",
allTypes: "seed-media-all-types",
allTypesChatML: "seed-media-all-types-chatml",
} as const;
/**
* Derive media ID from SHA256 hash (first 22 chars, URL-safe)
*/
function getMediaIdFromHash(sha256Hash: string): string {
const urlSafeHash = sha256Hash.replaceAll("+", "-").replaceAll("/", "_");
return urlSafeHash.slice(0, 22);
}
/**
* Get bucket path for a media file
*/
function getBucketPath(
projectId: string,
mediaId: string,
contentType: string,
): string {
const extensionMap: Record<string, string> = {
"image/png": "png",
"image/jpeg": "jpg",
"application/pdf": "pdf",
"audio/wav": "wav",
"audio/mpeg": "mp3",
};
const extension = extensionMap[contentType] || "bin";
const prefix = env.LANGFUSE_S3_MEDIA_UPLOAD_PREFIX || "";
return `${prefix}${projectId}/${mediaId}.${extension}`;
}
/**
* Upload a media file to storage and create database records
*/
async function uploadAndCreateMediaRecord(
projectId: string,
traceId: string,
field: "input" | "output" | "metadata",
mediaFile: MediaFile,
): Promise<void> {
// Check if bucket is configured
if (!env.LANGFUSE_S3_MEDIA_UPLOAD_BUCKET) {
logger.warn(
"[seed-media] LANGFUSE_S3_MEDIA_UPLOAD_BUCKET not configured, skipping media seeding",
);
return;
}
// Check if file exists
if (!fs.existsSync(mediaFile.filePath)) {
logger.warn(
`[seed-media] Test file not found: ${mediaFile.filePath}, skipping`,
);
return;
}
const fileBytes = fs.readFileSync(mediaFile.filePath);
const sha256Hash = crypto
.createHash("sha256")
.update(fileBytes)
.digest("base64");
const mediaId = getMediaIdFromHash(sha256Hash);
const bucketPath = getBucketPath(projectId, mediaId, mediaFile.contentType);
// Check if media already exists
const existingMedia = await prisma.media.findUnique({
where: {
projectId_sha256Hash: {
projectId,
sha256Hash,
},
},
});
if (existingMedia && existingMedia.uploadHttpStatus === 200) {
logger.debug(
`[seed-media] Media already exists for ${mediaFile.name}, creating TraceMedia link only`,
);
// Just create the TraceMedia link
await prisma.$queryRaw`
INSERT INTO "trace_media" ("id", "project_id", "trace_id", "media_id", "field")
VALUES (${crypto.randomUUID()}, ${projectId}, ${traceId}, ${existingMedia.id}, ${field})
ON CONFLICT DO NOTHING;
`;
return;
}
// Upload to storage
try {
const storageClient = StorageServiceFactory.getInstance({
bucketName: env.LANGFUSE_S3_MEDIA_UPLOAD_BUCKET,
accessKeyId: env.LANGFUSE_S3_MEDIA_UPLOAD_ACCESS_KEY_ID,
secretAccessKey: env.LANGFUSE_S3_MEDIA_UPLOAD_SECRET_ACCESS_KEY,
endpoint: env.LANGFUSE_S3_MEDIA_UPLOAD_ENDPOINT,
region: env.LANGFUSE_S3_MEDIA_UPLOAD_REGION,
forcePathStyle: env.LANGFUSE_S3_MEDIA_UPLOAD_FORCE_PATH_STYLE === "true",
awsSse: env.LANGFUSE_S3_MEDIA_UPLOAD_SSE,
awsSseKmsKeyId: env.LANGFUSE_S3_MEDIA_UPLOAD_SSE_KMS_KEY_ID,
});
await storageClient.uploadFile({
fileName: bucketPath,
fileType: mediaFile.contentType,
data: Readable.from(fileBytes),
});
logger.debug(`[seed-media] Uploaded ${mediaFile.name} to ${bucketPath}`);
} catch (error) {
logger.error(`[seed-media] Failed to upload ${mediaFile.name}:`, error);
return;
}
// Create Media record
await prisma.$queryRaw`
INSERT INTO "media" (
"id",
"project_id",
"sha_256_hash",
"bucket_path",
"bucket_name",
"content_type",
"content_length",
"uploaded_at",
"upload_http_status"
)
VALUES (
${mediaId},
${projectId},
${sha256Hash},
${bucketPath},
${env.LANGFUSE_S3_MEDIA_UPLOAD_BUCKET},
${mediaFile.contentType},
${BigInt(fileBytes.length)},
${new Date()},
${200}
)
ON CONFLICT ("project_id", "sha_256_hash")
DO UPDATE SET
"bucket_name" = ${env.LANGFUSE_S3_MEDIA_UPLOAD_BUCKET},
"bucket_path" = ${bucketPath},
"content_type" = ${mediaFile.contentType},
"content_length" = ${BigInt(fileBytes.length)},
"uploaded_at" = ${new Date()},
"upload_http_status" = ${200}
`;
// Create TraceMedia link
await prisma.$queryRaw`
INSERT INTO "trace_media" ("id", "project_id", "trace_id", "media_id", "field")
VALUES (${crypto.randomUUID()}, ${projectId}, ${traceId}, ${mediaId}, ${field})
ON CONFLICT DO NOTHING;
`;
logger.info(
`[seed-media] Created media record for ${mediaFile.name} -> ${field}`,
);
}
/**
* Seed media test traces for a project
*
* Creates two test traces with media attachments:
* 1. Image only (in input)
* 2. All types (image in input, PDF in output, audio in metadata)
*/
export async function seedMediaTraces(projectId: string): Promise<void> {
logger.info(`[seed-media] Seeding media traces for project ${projectId}`);
// Check if bucket is configured
if (!env.LANGFUSE_S3_MEDIA_UPLOAD_BUCKET) {
logger.warn(
"[seed-media] LANGFUSE_S3_MEDIA_UPLOAD_BUCKET not configured, skipping media seeding",
);
return;
}
// Trace 1: Image only (input)
const trace1Id = MEDIA_TEST_TRACE_IDS.imageOnly;
await uploadAndCreateMediaRecord(
projectId,
trace1Id,
"input",
MEDIA_FILES.image,
);
// Trace 2: All media types
const trace2Id = MEDIA_TEST_TRACE_IDS.allTypes;
await uploadAndCreateMediaRecord(
projectId,
trace2Id,
"input",
MEDIA_FILES.image,
);
await uploadAndCreateMediaRecord(
projectId,
trace2Id,
"output",
MEDIA_FILES.pdf,
);
await uploadAndCreateMediaRecord(
projectId,
trace2Id,
"metadata",
MEDIA_FILES.audio,
);
// Trace 3: All media types with ChatML format (pretty-rendered)
const trace3Id = MEDIA_TEST_TRACE_IDS.allTypesChatML;
await uploadAndCreateMediaRecord(
projectId,
trace3Id,
"input",
MEDIA_FILES.image,
);
await uploadAndCreateMediaRecord(
projectId,
trace3Id,
"output",
MEDIA_FILES.pdf,
);
await uploadAndCreateMediaRecord(
projectId,
trace3Id,
"metadata",
MEDIA_FILES.audio,
);
logger.info("[seed-media] Media seeding completed");
}
+131 -9
View File
@@ -9,7 +9,8 @@ import {
PrismaClient,
type Project,
ScoreConfigCategoryDomain,
ScoreDataType,
ScoreDataTypeEnum,
type ScoreDataTypeType,
} from "../../src/index";
import { getDisplaySecretKey, hashSecretKey, logger } from "../../src/server";
import { redis } from "../../src/server/redis/redis";
@@ -30,6 +31,7 @@ import {
generateEvalTraceId,
} from "./utils/seed-helpers";
import { seedDatasetVersions } from "./seed-dataset-versions";
import { seedMediaTraces } from "./seed-media";
const options = {
environment: { type: "string" },
@@ -349,6 +351,9 @@ async function main() {
await createDashboardsAndWidgets([project1, project2]);
await seedDatasetVersions(prisma, [project1.id, project2.id]);
// Seed media test traces (uploads to MinIO + creates Media/TraceMedia records)
await seedMediaTraces(project1.id);
await prisma.llmSchema.createMany({
data: [
{
@@ -536,6 +541,8 @@ export async function createDatasets(
datasetItemIds.push(itemId);
// Create dataset items in versioned format with all required fields
// Set validFrom to 30 days ago so experiment runs can reference this version
const validFrom = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000);
itemsToCreate.push({
id: itemId,
projectId,
@@ -546,7 +553,7 @@ export async function createDatasets(
expectedOutput: item.output,
metadata: Math.random() > 0.5 ? { key: "value" } : undefined,
status: "ACTIVE" as const,
validFrom: new Date(),
validFrom,
isDeleted: false,
});
}
@@ -586,6 +593,121 @@ export async function createDatasets(
update: {},
});
}
// Create multiple versions for test-dataset-versioning
if (datasetName === "test-dataset-versioning") {
const itemId = `test-version-item-${projectId.slice(-8)}`;
const baseTime = new Date("2025-01-20T10:00:00Z");
// Version 1: Initial version
const v1Time = new Date(baseTime.getTime());
await prisma.datasetItem.create({
data: {
id: itemId,
projectId,
datasetId: dataset.id,
sourceTraceId: null,
sourceObservationId: null,
input: { color: "red" },
expectedOutput: "#FF0000",
metadata: { version: "1", description: "Initial version" },
status: "ACTIVE",
validFrom: v1Time,
isDeleted: false,
},
});
// Version 2: Updated output (2 hours later)
const v2Time = new Date(baseTime.getTime() + 2 * 60 * 60 * 1000);
await prisma.$executeRaw`
UPDATE dataset_items
SET valid_to = ${v2Time}
WHERE project_id = ${projectId}
AND dataset_id = ${dataset.id}
AND id = ${itemId}
AND valid_from = ${v1Time}
`;
await prisma.datasetItem.create({
data: {
id: itemId,
projectId,
datasetId: dataset.id,
sourceTraceId: null,
sourceObservationId: null,
input: { color: "red" },
expectedOutput: "#FF0001", // Slightly different
metadata: {
version: "2",
description: "Fixed hex value precision",
},
status: "ACTIVE",
validFrom: v2Time,
isDeleted: false,
},
});
// Version 3: Updated input and output (1 day later)
const v3Time = new Date(baseTime.getTime() + 24 * 60 * 60 * 1000);
await prisma.$executeRaw`
UPDATE dataset_items
SET valid_to = ${v3Time}
WHERE project_id = ${projectId}
AND dataset_id = ${dataset.id}
AND id = ${itemId}
AND valid_from = ${v2Time}
`;
await prisma.datasetItem.create({
data: {
id: itemId,
projectId,
datasetId: dataset.id,
sourceTraceId: null,
sourceObservationId: null,
input: { color: "crimson" }, // Changed input
expectedOutput: "#DC143C",
metadata: {
version: "3",
description: "Changed to crimson color",
},
status: "ACTIVE",
validFrom: v3Time,
isDeleted: false,
},
});
// Version 4: Current version (2 days after initial)
const v4Time = new Date(baseTime.getTime() + 2 * 24 * 60 * 60 * 1000);
await prisma.$executeRaw`
UPDATE dataset_items
SET valid_to = ${v4Time}
WHERE project_id = ${projectId}
AND dataset_id = ${dataset.id}
AND id = ${itemId}
AND valid_from = ${v3Time}
`;
await prisma.datasetItem.create({
data: {
id: itemId,
projectId,
datasetId: dataset.id,
sourceTraceId: null,
sourceObservationId: null,
input: { color: "blue" }, // Changed to blue
expectedOutput: "#0000FF",
metadata: { version: "4", description: "Changed to blue color" },
status: "ACTIVE",
validFrom: v4Time,
isDeleted: false,
},
});
logger.info(
`Created 4 versions for test-dataset-versioning in project ${projectId}`,
);
}
}
}
}
@@ -753,7 +875,7 @@ async function generateConfigsForProject(projects: Project[]) {
{
name: string;
id: string;
dataType: ScoreDataType;
dataType: ScoreDataTypeType;
categories: ScoreConfigCategoryDomain[] | null;
}[]
> = new Map();
@@ -803,7 +925,7 @@ async function generateConfigs(project: Project) {
const configNameAndId: {
name: string;
id: string;
dataType: ScoreDataType;
dataType: ScoreDataTypeType;
categories: ScoreConfigCategoryDomain[] | null;
}[] = [];
@@ -811,7 +933,7 @@ async function generateConfigs(project: Project) {
{
id: `config-${v4()}`,
name: "manual-score",
dataType: ScoreDataType.NUMERIC,
dataType: ScoreDataTypeEnum.NUMERIC,
projectId: project.id,
isArchived: false,
},
@@ -819,7 +941,7 @@ async function generateConfigs(project: Project) {
id: `config-${v4()}`,
projectId: project.id,
name: "Accuracy",
dataType: ScoreDataType.CATEGORICAL,
dataType: ScoreDataTypeEnum.CATEGORICAL,
categories: [
{ label: "Incorrect", value: 0 },
{ label: "Partially Correct", value: 1 },
@@ -831,7 +953,7 @@ async function generateConfigs(project: Project) {
id: `config-${v4()}`,
projectId: project.id,
name: "Toxicity",
dataType: ScoreDataType.BOOLEAN,
dataType: ScoreDataTypeEnum.BOOLEAN,
categories: [
{ label: "True", value: 1 },
{ label: "False", value: 0 },
@@ -880,7 +1002,7 @@ async function generateQueuesForProject(
{
name: string;
id: string;
dataType: ScoreDataType;
dataType: ScoreDataTypeType;
categories: ScoreConfigCategoryDomain[] | null;
}[]
>,
@@ -904,7 +1026,7 @@ async function generateQueues(
configIdsAndNames: {
name: string;
id: string;
dataType: ScoreDataType;
dataType: ScoreDataTypeType;
categories: ScoreConfigCategoryDomain[] | null;
}[],
) {
@@ -40,7 +40,7 @@ await orchestrator.createSyntheticData(projectIds, config);
### 1. Dataset Experiment Data
- **Purpose**: Realistic experiment traces based on actual datasets
- **Environment**: `langfuse-prompt-experiments`
- **Environment**: `langfuse-prompt-experiment`
- **Structure**: Each dataset item links to a trace with a single generation observation
- **ID Pattern**: `trace-dataset-{datasetName}-{itemIndex}-{projectId}-{runNumber}`
@@ -195,7 +195,11 @@ export class ClickHouseQueryBuilder {
start_time AS event_ts,
0 AS is_deleted,
'' AS usage_pricing_tier_id,
'' AS usage_pricing_tier_name
'' AS usage_pricing_tier_name,
map() AS tool_definitions,
[] AS tool_calls,
[] AS tool_call_names
FROM numbers(${totalObservations});
`;
}
@@ -250,7 +254,8 @@ export class ClickHouseQueryBuilder {
timestamp AS updated_at,
timestamp AS event_ts,
0 AS is_deleted,
NULL AS execution_trace_id
NULL AS execution_trace_id,
'' AS long_string_value
FROM numbers(${totalScores});
`;
}
@@ -41,7 +41,7 @@ import {
* Generates realistic test data for traces, observations, and scores.
*
* Use generateXxxTraces() for creating different data types:
* - generateDatasetTrace(): For dataset experiment runs (langfuse-prompt-experiments env)
* - generateDatasetTrace(): For dataset experiment runs (langfuse-prompt-experiment env)
* - generateEvaluationTraces(): For evaluation data (langfuse-evaluation env)
* - generateSyntheticTraces(): For large-scale synthetic data (default env)
*/
@@ -151,8 +151,8 @@ export class DataGenerator {
name: `dataset-run-item-${uuidv4()}`,
input: traceInput,
output: traceOutput,
environment: "langfuse-prompt-experiments",
metadata: { experimentType: "langfuse-prompt-experiments" },
environment: "langfuse-prompt-experiment",
metadata: { experimentType: "langfuse-prompt-experiment" },
public: false,
bookmarked: false,
session_id: null,
@@ -212,7 +212,7 @@ export class DataGenerator {
total: Math.round(totalCost * 100000) / 100000,
},
total_cost: Math.round(totalCost * 100000) / 100000,
environment: "langfuse-prompt-experiments",
environment: "langfuse-prompt-experiment",
});
}
@@ -237,7 +237,7 @@ export class DataGenerator {
string_value: undefined,
data_type: "NUMERIC",
source: "API",
environment: "langfuse-prompt-experiments",
environment: "langfuse-prompt-experiment",
});
}
@@ -265,7 +265,7 @@ export class DataGenerator {
string_value: undefined,
data_type: "NUMERIC",
source: "API",
environment: "langfuse-prompt-experiments",
environment: "langfuse-prompt-experiment",
});
}
@@ -759,6 +759,8 @@ export class DataGenerator {
);
// 6. GENERATION - Final summary generation
const toolObservation = observations[observations.length - 1];
const toolName = toolObservation.name || "WebSearchTool";
observations.push(
createObservation({
id: `${traceId}-generation`,
@@ -800,6 +802,14 @@ export class DataGenerator {
output: this.randomInt(35, 45) / 100000,
total: this.randomInt(50, 70) / 100000,
},
tool_definitions: {
WebSearchTool: "Search the web for information",
CalculatorTool: "Perform mathematical calculations",
WeatherForecastTool: "Get weather forecasts and data",
EmailSenderTool: "Send emails to recipients",
},
tool_calls: ["call_workflow_1"],
tool_call_names: [toolName],
}),
);
@@ -1121,6 +1131,16 @@ export class DataGenerator {
updated_at: end,
event_ts: start,
is_deleted: 0,
tool_definitions: d.tool
? {
"billing.lookup":
"Look up billing records and transaction history",
"billing.refund":
"Issue refunds for duplicate or erroneous charges",
}
: undefined,
tool_calls: d.tool ? [`call_${index}`] : undefined,
tool_call_names: d.tool ? [d.tool.name] : undefined,
};
if (!d.tool) return [baseGen];
@@ -1157,6 +1177,8 @@ export class DataGenerator {
updated_at: start - 5,
event_ts: start - 40,
is_deleted: 0,
tool_definitions: undefined,
tool_calls: undefined,
};
return [toolObs, baseGen];
@@ -1184,6 +1206,7 @@ export class DataGenerator {
config_id: null,
data_type: "NUMERIC",
string_value: null,
long_string_value: "",
queue_id: null,
created_at: baseTs,
updated_at: baseTs,
@@ -1203,6 +1226,7 @@ export class DataGenerator {
environment: "default",
name: "safe",
value: safeVal,
long_string_value: "",
source: "API",
comment: "Content safety",
metadata: {},
@@ -1230,6 +1254,7 @@ export class DataGenerator {
observation_id: null,
environment: "default",
name: "resolved",
long_string_value: "",
value: 1,
source: "API",
comment: "Conversation resolved",
@@ -182,6 +182,10 @@ const SEED_DATASET_ITEMS_GREETINGS = [
{ input: { language: "German" }, output: "Guten Tag" },
];
const SEED_DATASET_ITEMS_VERSION_TEST = [
{ input: { color: "red" }, output: "#FF0000" },
];
export const SEED_DATASETS = [
{
name: "demo-countries-dataset",
@@ -238,6 +242,15 @@ export const SEED_DATASETS = [
items: SEED_DATASET_ITEMS_GREETINGS,
shouldRunExperiment: false,
},
{
name: "test-dataset-versioning",
description: "Test dataset with multiple versions of a single item",
metadata: {
purpose: "testing dataset versioning feature",
},
items: SEED_DATASET_ITEMS_VERSION_TEST,
shouldRunExperiment: false,
},
];
// Prompts
@@ -7,8 +7,10 @@ import { DataGenerator } from "./data-generators";
import { ClickHouseQueryBuilder } from "./clickhouse-builder";
import { FrameworkTraceLoader } from "./framework-traces/framework-trace-loader";
import { EVAL_TRACE_COUNT, SEED_DATASETS } from "./postgres-seed-constants";
import { MEDIA_TEST_TRACE_IDS } from "../seed-media";
import {
clickhouseClient,
createTrace,
DatasetRunItemRecordInsertType,
logger,
ObservationRecordInsertType,
@@ -29,7 +31,7 @@ const DATASET_RUN_SCORE_NAMES = [
* Orchestrates seeding operations across ClickHouse and PostgreSQL.
*
* Use createXxxData() for specific data types:
* - createDatasetExperimentData(): Dataset runs in langfuse-prompt-experiments env
* - createDatasetExperimentData(): Dataset runs in langfuse-prompt-experiment env
* - createEvaluationData(): Evaluation data in langfuse-evaluation env
* - createSyntheticData(): Large synthetic data in default env
* - executeFullSeed(): All data types together
@@ -338,6 +340,9 @@ export class SeederOrchestrator {
// create traces from real examples for each framework source
await this.createFrameworkTraces(projectIds);
// Create traces for media attachment testing
await this.createMediaTestTraces(projectIds);
// Log completion statistics (commented out to reduce terminal noise)
await this.logStatistics();
@@ -448,4 +453,106 @@ export class SeederOrchestrator {
}
}
}
/**
* Creates test traces for media attachment testing (JSON Beta view).
* Use for: Testing media rendering in the trace detail view.
*/
async createMediaTestTraces(projectIds: string[]): Promise<void> {
logger.info(
`Creating media test traces for ${projectIds.length} projects.`,
);
const now = Date.now();
for (const projectId of projectIds) {
logger.info(`Processing media test traces for project ${projectId}`);
const traces: TraceRecordInsertType[] = [
// Trace 1: Image only (in input)
createTrace({
id: MEDIA_TEST_TRACE_IDS.imageOnly,
project_id: projectId,
name: "Media Test: Image Only",
timestamp: now,
input: JSON.stringify({
message: "This trace has an image attachment in input",
description: "Used for testing media rendering in JSON Beta view",
}),
output: JSON.stringify({ status: "success" }),
metadata: { test_type: "media", media_types: "image" },
tags: ["media-test", "image"],
environment: "default",
}),
// Trace 2: All media types
createTrace({
id: MEDIA_TEST_TRACE_IDS.allTypes,
project_id: projectId,
name: "Media Test: All Types",
timestamp: now + 1000,
input: JSON.stringify({
message: "This trace has an image in input",
description: "Testing image attachment",
}),
output: JSON.stringify({
message: "This trace has a PDF in output",
description: "Testing PDF attachment",
}),
metadata: {
message: "This trace has audio in metadata",
description: "Testing audio attachment",
test_type: "media",
media_types: "image,pdf,audio",
},
tags: ["media-test", "all-types"],
environment: "default",
}),
// Trace 3: All media types with ChatML format (pretty-rendered)
createTrace({
id: MEDIA_TEST_TRACE_IDS.allTypesChatML,
project_id: projectId,
name: "Media Test: All Types (ChatML)",
timestamp: now + 2000,
input: JSON.stringify([
{
role: "system",
content:
"You are a helpful assistant that can analyze images, documents, and audio files.",
},
{
role: "user",
content:
"Please analyze the attached image and describe what you see.",
},
]),
output: JSON.stringify([
{
role: "assistant",
content:
"I can see the Langfuse logo in the image. It appears to be a modern, clean design with distinctive branding elements. The attached PDF contains additional documentation about the Bitcoin whitepaper.",
},
]),
metadata: {
message: "This trace has audio in metadata",
description: "Testing audio attachment with ChatML format",
test_type: "media",
media_types: "image,pdf,audio",
format: "chatml",
},
tags: ["media-test", "all-types", "chatml"],
environment: "default",
}),
];
try {
await this.queryBuilder.executeTracesInsert(traces);
logger.info(
`✓ Created ${traces.length} media test traces for project ${projectId}`,
);
} catch (error) {
logger.error(`✗ Media test traces insert failed:`, error);
throw error;
}
}
}
}
-1
View File
@@ -1,4 +1,3 @@
/* eslint-disable no-unused-vars */
// disable lint as this is exported and used in packages
export enum ModelUsageUnit {
Characters = "CHARACTERS",
@@ -3,7 +3,6 @@ import { FilterState } from "../types";
import { z } from "zod/v4";
export enum TriggerEventSource {
// eslint-disable-next-line no-unused-vars
Prompt = "prompt",
}
@@ -23,6 +23,7 @@ export const DatasetRunItemSchema = z.object({
datasetItemInput: jsonSchema,
datasetItemExpectedOutput: jsonSchema,
datasetItemMetadata: MetadataDomain,
datasetItemVersion: z.date().nullable(),
});
// Conditional type for dataset run item domain with optional IO
@@ -93,6 +93,10 @@ export const ObservationSchema = z.object({
// pricing tier information
usagePricingTierId: z.string().nullable(),
usagePricingTierName: z.string().nullable(),
// tool data
toolDefinitions: z.record(z.string(), z.string()).nullable(),
toolCalls: z.array(z.string()).nullable(),
toolCallNames: z.array(z.string()).nullable(),
});
export type Observation = z.infer<typeof ObservationSchema>;
@@ -105,6 +109,7 @@ export type ObservationCoreFields = Pick<
export const EventsObservationSchema = ObservationSchema.extend({
userId: z.string().nullable(),
sessionId: z.string().nullable(),
traceName: z.string().nullable(),
});
export type EventsObservation = z.infer<typeof EventsObservationSchema>;
+42 -1
View File
@@ -10,15 +10,19 @@ export const ScoreSourceEnum = {
export const ScoreSourceDomain = z.enum(ScoreSourceArray);
export type ScoreSourceType = z.infer<typeof ScoreSourceDomain>;
export const CORRECTION_NAME = "output" as const;
export const ScoreDataTypeArray = [
"NUMERIC",
"CATEGORICAL",
"BOOLEAN",
"CORRECTION",
] as const;
export const ScoreDataTypeEnum = {
NUMERIC: "NUMERIC",
CATEGORICAL: "CATEGORICAL",
BOOLEAN: "BOOLEAN",
CORRECTION: "CORRECTION",
} as const;
export const ScoreDataTypeDomain = z.enum(ScoreDataTypeArray);
export type ScoreDataTypeType = z.infer<typeof ScoreDataTypeDomain>;
@@ -38,6 +42,11 @@ export const BooleanData = z.object({
dataType: z.literal("BOOLEAN"),
});
const CorrectionData = z.object({
stringValue: z.null(),
dataType: z.literal("CORRECTION"),
});
// Only used for backwards compatibility with old score API schemas
export const ScoreSchemaExclReferencesAndDates = z.object({
// Core identifiers
@@ -70,11 +79,43 @@ const ScoreFoundationSchema = ScoreSchemaExclReferencesAndDates.and(
sessionId: z.string().nullable(),
datasetRunId: z.string().nullable(),
observationId: z.string().nullable(),
longStringValue: z.string().default(""),
}),
);
export const ScoreSchema = ScoreFoundationSchema.and(
z.discriminatedUnion("dataType", [NumericData, CategoricalData, BooleanData]),
z.discriminatedUnion("dataType", [
NumericData,
CategoricalData,
BooleanData,
CorrectionData,
]),
);
export type ScoreDomain = z.infer<typeof ScoreSchema>;
export type ScoreByDataType<T extends ScoreDataTypeType> = ScoreDomain & {
dataType: T;
};
export type ScoresByDataTypes<T extends readonly ScoreDataTypeType[]> =
T extends readonly (infer U)[]
? U extends ScoreDataTypeType
? ScoreByDataType<U>
: never
: never;
// Aggregatable score types - used in most read queries to exclude CORRECTION scores
export const AGGREGATABLE_SCORE_TYPES = [
"NUMERIC",
"BOOLEAN",
"CATEGORICAL",
] as const satisfies readonly ScoreDataTypeType[];
export type AggregatableScoreDataType =
(typeof AGGREGATABLE_SCORE_TYPES)[number];
// Type helper for functions that return only aggregatable scores
export type AggregatableScore = ScoresByDataTypes<
typeof AGGREGATABLE_SCORE_TYPES
>;
@@ -3,13 +3,15 @@ import { orderBy } from "../interfaces/orderBy";
import z from "zod/v4";
export enum TableViewPresetTableName {
Traces = "traces", // eslint-disable-line no-unused-vars
Observations = "observations", // eslint-disable-line no-unused-vars
Scores = "scores", // eslint-disable-line no-unused-vars
Sessions = "sessions", // eslint-disable-line no-unused-vars
Datasets = "datasets", // eslint-disable-line no-unused-vars
Traces = "traces",
Observations = "observations",
Scores = "scores",
Sessions = "sessions",
SessionDetail = "session-detail",
Datasets = "datasets",
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const TableViewPresetDomainSchema = z.object({
id: z.string(),
projectId: z.string().nullable(),
+33 -2
View File
@@ -51,6 +51,7 @@ const EnvSchema = z.object({
LANGFUSE_CACHE_PROMPT_TTL_SECONDS: z.coerce.number().default(300), // 5 minutes
CLICKHOUSE_URL: z.string().url(),
CLICKHOUSE_READ_ONLY_URL: z.string().url().optional(),
CLICKHOUSE_EVENTS_READ_ONLY_URL: z.string().url().optional(),
CLICKHOUSE_CLUSTER_NAME: z.string().default("default"),
CLICKHOUSE_DB: z.string().default("default"),
CLICKHOUSE_USER: z.string(),
@@ -60,6 +61,11 @@ const EnvSchema = z.object({
// Optional to allow for server-setting fallbacks
CLICKHOUSE_ASYNC_INSERT_MAX_DATA_SIZE: z.string().optional(),
CLICKHOUSE_ASYNC_INSERT_BUSY_TIMEOUT_MS: z.coerce.number().int().optional(),
CLICKHOUSE_ASYNC_INSERT_BUSY_TIMEOUT_MIN_MS: z.coerce
.number()
.int()
.min(50)
.optional(),
CLICKHOUSE_LIGHTWEIGHT_DELETE_MODE: z
.enum(["alter_update", "lightweight_update", "lightweight_update_force"])
.default("alter_update"),
@@ -192,7 +198,7 @@ const EnvSchema = z.object({
}
return map;
} catch (err) {
} catch {
return new Map<string, number>();
}
}),
@@ -277,9 +283,34 @@ const EnvSchema = z.object({
LANGFUSE_DATASET_SERVICE_READ_FROM_VERSIONED_IMPLEMENTATION: z
.enum(["true", "false"])
.default("true"),
// EE License
LANGFUSE_EE_LICENSE_KEY: z.string().optional(),
// Ingestion Masking (EE feature)
LANGFUSE_INGESTION_MASKING_CALLBACK_URL: z.string().url().optional(),
LANGFUSE_INGESTION_MASKING_CALLBACK_TIMEOUT_MS: z.coerce
.number()
.positive()
.default(500),
LANGFUSE_INGESTION_MASKING_CALLBACK_FAIL_CLOSED: z
.enum(["true", "false"])
.default("false"),
LANGFUSE_INGESTION_MASKING_MAX_RETRIES: z.coerce
.number()
.nonnegative()
.default(1),
LANGFUSE_INGESTION_MASKING_PROPAGATED_HEADERS: z
.string()
.optional()
.transform((s) =>
s ? s.split(",").map((h) => h.toLowerCase().trim()) : [],
),
});
export const env: z.infer<typeof EnvSchema> =
export type SharedEnv = z.infer<typeof EnvSchema>;
export const env: SharedEnv =
process.env.DOCKER_BUILD === "1" // eslint-disable-line turbo/no-undeclared-env-vars
? (process.env as any)
: EnvSchema.parse(removeEmptyEnvVariables(process.env));
@@ -0,0 +1,7 @@
import { BaseError } from "./BaseError";
export class NotImplementedError extends BaseError {
constructor(description = "Not Implemented") {
super("NotImplementedError", 501, description, true);
}
}
+1
View File
@@ -8,3 +8,4 @@ export { ApiError } from "./ApiError";
export { InternalServerError } from "./InternalServerError";
export { LangfuseConflictError } from "./ConflictError";
export { ServiceUnavailableError } from "./ServiceUnavailableError";
export { NotImplementedError } from "./NotImplementedError";
+38
View File
@@ -73,6 +73,14 @@ export const eventsTableCols: ColumnDefinition[] = [
internal: "e.session_id",
nullable: true,
},
{
name: "Trace Name",
id: "traceName",
type: "stringOptions",
internal: "e.trace_name",
options: [], // to be added at runtime
nullable: true,
},
{
name: "Level",
id: "level",
@@ -235,4 +243,34 @@ export const eventsTableCols: ColumnDefinition[] = [
type: "string",
internal: "", // handled by comment filter helpers
},
{
name: "Has Parent Observation",
id: "hasParentObservation",
type: "boolean",
internal: "e.parent_span_id != ''",
},
{
name: "Experiment Dataset ID",
id: "experimentDatasetId",
type: "stringOptions",
internal: "e.experiment_dataset_id",
options: [], // to be added at runtime
nullable: true,
},
{
name: "Experiment ID",
id: "experimentId",
type: "stringOptions",
internal: "e.experiment_id",
options: [], // to be added at runtime
nullable: true,
},
{
name: "Experiment Name",
id: "experimentName",
type: "stringOptions",
internal: "e.experiment_name",
options: [], // to be added at runtime
nullable: true,
},
];
@@ -0,0 +1,32 @@
// Export source options for analytics integrations (PostHog, Mixpanel, Blob Storage)
// This is a client-safe file that can be imported from @langfuse/shared
import { AnalyticsIntegrationExportSource } from "@prisma/client";
export const EXPORT_SOURCE_OPTIONS: Array<{
value: AnalyticsIntegrationExportSource;
label: string;
description: string;
}> = [
{
value: "TRACES_OBSERVATIONS" as const,
label: "Traces and observations (legacy)",
description:
"Export traces, observations and scores. This is the legacy behavior prior to tracking traces and observations in separate tables. It is recommended to use the enriched observations option instead.",
},
{
value: "TRACES_OBSERVATIONS_EVENTS" as const,
label: "Traces and observations (legacy) and enriched observations",
description:
"Export traces, observations, scores and enriched observations. This exports both the legacy data source (traces, observations) and the new one (enriched observations) and essentially exports duplicate data. Therefore, it should only be used to migrate existing integrations to the new recommended enriched observations and check validity of the data for downstream consumers of the export data.",
},
{
value: "EVENTS" as const,
label: "Enriched observations (recommended)",
description:
"Export enriched observations and scores. This is the recommended data source for integrations and will be the default for new integrations.",
},
] as const;
export type ExportSourceOption = (typeof EXPORT_SOURCE_OPTIONS)[number];
export type ExportSourceValue = ExportSourceOption["value"];
@@ -1,10 +1,11 @@
import { JSONPath } from "jsonpath-plus";
import { set } from "lodash";
import set from "lodash/set";
import type {
FieldMappingConfig,
SourceField,
AddToDatasetMapping,
} from "./addToDatasetTypes";
import { parseJsonPrioritised } from "../../utils/json";
type ObservationData = {
input: unknown;
@@ -37,9 +38,14 @@ export function testJsonPath(props: { jsonPath: string; data: unknown }): {
*/
export function evaluateJsonPath(data: unknown, jsonPath: string): unknown {
try {
const parsed = typeof data === "string" ? JSON.parse(data) : data;
const results = JSONPath({ path: jsonPath, json: parsed });
return results?.[0];
const parsed = typeof data === "string" ? parseJsonPrioritised(data) : data;
const result = JSONPath({
path: jsonPath,
json: parsed as string | object,
wrap: false,
});
return result;
} catch {
return undefined;
}
@@ -4,7 +4,6 @@ import { orderBy } from "../../interfaces/orderBy";
import { BatchTableNames } from "../../interfaces/tableNames";
import { TracingSearchType } from "../../interfaces/search";
/* eslint-disable no-unused-vars */
export enum BatchActionType {
Create = "create",
Delete = "delete",
@@ -8,17 +8,17 @@ import { BatchTableNames } from "../../interfaces/tableNames";
import { TracingSearchType } from "../../interfaces/search";
export enum BatchExportStatus {
QUEUED = "QUEUED", // eslint-disable-line no-unused-vars
PROCESSING = "PROCESSING", // eslint-disable-line no-unused-vars
COMPLETED = "COMPLETED", // eslint-disable-line no-unused-vars
FAILED = "FAILED", // eslint-disable-line no-unused-vars
CANCELLED = "CANCELLED", // eslint-disable-line no-unused-vars
QUEUED = "QUEUED",
PROCESSING = "PROCESSING",
COMPLETED = "COMPLETED",
FAILED = "FAILED",
CANCELLED = "CANCELLED",
}
export enum BatchExportFileFormat {
JSON = "JSON", // eslint-disable-line no-unused-vars
CSV = "CSV", // eslint-disable-line no-unused-vars
JSONL = "JSONL", // eslint-disable-line no-unused-vars
JSON = "JSON",
CSV = "CSV",
JSONL = "JSONL",
}
// Use shared BatchTableNames enum for consistency across batch operations
+63 -7
View File
@@ -2,19 +2,73 @@ import { z } from "zod/v4";
const MAX_COMMENT_LENGTH = 5000;
const COMMENT_OBJECT_TYPES = [
export const COMMENT_OBJECT_TYPES = [
"TRACE",
"OBSERVATION",
"SESSION",
"PROMPT",
] as const;
export const CreateCommentData = z.object({
projectId: z.string(),
content: z.string().trim().min(1).max(MAX_COMMENT_LENGTH),
objectId: z.string(),
objectType: z.enum(COMMENT_OBJECT_TYPES),
});
export const COMMENT_DATA_FIELDS = ["input", "output", "metadata"] as const;
// JSON Path validation
// TODO: simplify
// Allows: $ (root), $.foo, $[0], $.foo[0].bar, $['key-with-dash'], etc.
export const jsonPathSchema = z
.string()
.regex(
/^\$(\.[a-zA-Z_][a-zA-Z0-9_-]*|\[\d+\]|\['[^']+'\]|\["[^"]+"\])*$/,
"Invalid JSON Path syntax",
);
export const CreateCommentData = z
.object({
projectId: z.string(),
content: z.string().trim().min(1).max(MAX_COMMENT_LENGTH),
objectId: z.string(),
objectType: z.enum(COMMENT_OBJECT_TYPES),
// Optional inline positioning (parallel arrays)
dataField: z.enum(COMMENT_DATA_FIELDS).nullish(),
path: z.array(jsonPathSchema).nullish(),
rangeStart: z.array(z.number().int().min(0)).nullish(),
rangeEnd: z.array(z.number().int().min(1)).nullish(),
})
.refine(
(data) => {
const hasDataField = data.dataField != null;
const hasPath = data.path != null && data.path.length > 0;
const hasRangeStart =
data.rangeStart != null && data.rangeStart.length > 0;
const hasRangeEnd = data.rangeEnd != null && data.rangeEnd.length > 0;
// All must be set together, or all must be null/empty
if (hasDataField || hasPath || hasRangeStart || hasRangeEnd) {
return (
hasDataField &&
hasPath &&
hasRangeStart &&
hasRangeEnd &&
data.path!.length === data.rangeStart!.length &&
data.path!.length === data.rangeEnd!.length
);
}
return true;
},
{
message:
"dataField, path, rangeStart, rangeEnd must all be set together with matching lengths, or all be null/empty",
},
)
.refine(
(data) => {
// Validate each range: start must be < end
if (data.rangeStart && data.rangeEnd) {
return data.rangeStart.every((start, i) => start < data.rangeEnd![i]);
}
return true;
},
{ message: "Each rangeStart must be less than corresponding rangeEnd" },
);
export const DeleteCommentData = z.object({
projectId: z.string(),
@@ -22,3 +76,5 @@ export const DeleteCommentData = z.object({
objectId: z.string(),
objectType: z.enum(COMMENT_OBJECT_TYPES),
});
export type CreateCommentInput = z.infer<typeof CreateCommentData>;
@@ -0,0 +1,353 @@
import { z } from "zod/v4";
import { DEFAULT_TRACE_ENVIRONMENT } from "../../server/ingestion/types";
import { type EventRecordBaseType } from "../../server/repositories/definitions";
import { ObservationLevel, ObservationType } from "../../domain";
import { SingleValueOption } from "../../tableDefinitions";
import { ColumnDefinition } from "../../tableDefinitions";
import { formatColumnOptions } from "../../tableDefinitions/typeHelpers";
const flexibleUsageCostSchema = z.record(z.string(), z.number().nullable());
export const observationForEvalSchema = z.object({
// Identifiers
span_id: z.string(),
trace_id: z.string(),
project_id: z.string(),
parent_span_id: z.string().nullish(),
// Core properties
type: z.string(),
name: z.string(),
environment: z.string().default(DEFAULT_TRACE_ENVIRONMENT),
version: z.string().nullish(),
level: z.string().default(ObservationLevel.DEFAULT),
status_message: z.string().nullish(),
// Trace-level properties
trace_name: z.string().nullish(),
user_id: z.string().nullish(),
session_id: z.string().nullish(),
tags: z.array(z.string()).default([]),
release: z.string().nullish(),
// Model
provided_model_name: z.string().nullish(),
model_parameters: z.unknown().nullish(),
// Prompt
prompt_id: z.string().nullish(),
prompt_name: z.string().nullish(),
// Accepts string, number, or any other type from ingestion
prompt_version: z.union([z.string().nullish(), z.number().nullish()]),
// Usage & Cost - accepts number values directly from ingestion
provided_usage_details: flexibleUsageCostSchema,
provided_cost_details: flexibleUsageCostSchema,
usage_details: flexibleUsageCostSchema,
cost_details: flexibleUsageCostSchema,
// Tool calls
tool_definitions: z.record(z.string(), z.unknown()).default({}),
tool_calls: z.array(z.unknown()).default([]),
tool_call_names: z.array(z.string()).default([]),
// Experiment
experiment_id: z.string().nullish(),
experiment_name: z.string().nullish(),
experiment_description: z.string().nullish(),
experiment_dataset_id: z.string().nullish(),
experiment_item_id: z.string().nullish(),
experiment_item_expected_output: z.string().nullish(),
experiment_item_root_span_id: z.string().nullish(),
// Data - accepts any type (string, array, object) from different OTEL SDKs
input: z.unknown().nullish(),
output: z.unknown().nullish(),
metadata: z.record(z.string(), z.unknown()).nullish(),
});
export type ObservationForEval = z.infer<typeof observationForEvalSchema>;
export function convertEventRecordToObservationForEval(
record: EventRecordBaseType,
): ObservationForEval {
return observationForEvalSchema.parse(record);
}
export type ObservationEvalFilterColumnInternal =
/** Column identifier (must match an ObservationForEval field name) */
keyof Pick<
ObservationForEval,
| "type"
| "name"
| "environment"
| "level"
| "version"
| "trace_name"
| "user_id"
| "session_id"
| "tags"
| "experiment_dataset_id"
| "metadata"
>;
export type ObservationEvalMappingColumnInternal = keyof Pick<
ObservationForEval,
"input" | "output" | "metadata" | "experiment_item_expected_output"
>;
export interface ObservationEvalVariableColumn {
/** Column identifier (must match an ObservationForEval field name) */
id: string;
/** Display name for UI */
name: string;
/** Description for UI tooltips */
description: string;
/** Optional type hint for special handling (e.g., stringObject for metadata) */
type?: "stringObject";
internal: ObservationEvalMappingColumnInternal;
}
/**
* Columns available for variable extraction in observation-based evals.
* These are the fields that can be mapped to template variables.
*
* When configuring an eval, users can map these columns to template
* variables like {{input}}, {{output}}, {{expected_output}}, etc.
*/
export const observationEvalVariableColumns: ObservationEvalVariableColumn[] = [
{
id: "input",
name: "Input",
description: "Observation input data",
internal: "input",
},
{
id: "output",
name: "Output",
description: "Observation output data",
internal: "output",
},
{
id: "metadata",
name: "Metadata",
description: "Observation metadata",
type: "stringObject",
internal: "metadata",
},
{
id: "experimentItemExpectedOutput",
name: "Expected Output",
description: "Expected output from experiment item",
internal: "experiment_item_expected_output",
},
];
export const availableObservationEvalVariableColumns = [
...observationEvalVariableColumns,
{
id: "toolCalls",
name: "Tool Calls",
description: "Tool calls",
internal: "tool_calls",
},
{
id: "toolDefinitions",
name: "Tool Definitions",
description: "Tool definitions",
internal: "tool_definitions",
},
{
id: "toolCallNames",
name: "Tool Call Names",
description: "Tool call names",
internal: "tool_call_names",
},
{
id: "providedModelName",
name: "Model",
description: "Model",
internal: "provided_model_name",
},
{
id: "modelParameters",
name: "Model Parameters",
description: "Model parameters",
internal: "model_parameters",
},
{
id: "usageDetails",
name: "Usage Details",
description: "Usage details",
internal: "usage_details",
},
{
id: "costDetails",
name: "Cost Details",
description: "Cost details",
internal: "cost_details",
},
];
type ObservationEvalColumnDef = ColumnDefinition & {
internal: ObservationEvalFilterColumnInternal;
};
/**
* Columns available for filtering in observation-based evals.
*
* These columns can be used in filter conditions to determine
* which observations should be evaluated.
*/
export const observationEvalFilterColumns: ObservationEvalColumnDef[] = [
{
name: "Type",
id: "type",
type: "stringOptions",
internal: "type",
options: Object.values(ObservationType).map((key) => ({ value: key })),
},
{
name: "Name",
id: "name",
type: "stringOptions",
internal: "name",
options: [], // to be filled at runtime
},
{
name: "Environment",
id: "environment",
type: "stringOptions",
internal: "environment",
options: [], // to be filled at runtime
},
{
name: "Level",
id: "level",
type: "stringOptions",
internal: "level",
options: Object.values(ObservationLevel).map((key) => ({ value: key })),
},
{
name: "Version",
id: "version",
type: "string",
internal: "version",
nullable: true,
},
{
name: "Trace Name",
id: "traceName",
type: "stringOptions",
internal: "trace_name",
options: [], // to be filled at runtime
nullable: true,
},
{
name: "User ID",
id: "userId",
type: "string",
internal: "user_id",
nullable: true,
},
{
name: "Session ID",
id: "sessionId",
type: "string",
internal: "session_id",
nullable: true,
},
{
name: "Tags",
id: "tags",
type: "arrayOptions",
internal: "tags",
options: [], // to be filled at runtime
},
{
name: "Metadata",
id: "metadata",
type: "stringObject",
internal: "metadata",
},
];
export const experimentEvalFilterColumns: ObservationEvalColumnDef[] = [
{
name: "Dataset",
id: "experimentDatasetId",
type: "stringOptions",
internal: "experiment_dataset_id",
options: [], // to be filled at runtime
},
];
export const eventsEvalFilterColumns: ObservationEvalColumnDef[] = [
...observationEvalFilterColumns,
...experimentEvalFilterColumns,
];
// Options type for observation eval filters
export type ObservationEvalOptions = {
environment?: Array<SingleValueOption>;
tags?: Array<SingleValueOption>;
traceName?: Array<SingleValueOption>;
name?: Array<SingleValueOption>;
};
export type ExperimentEvalOptions = {
experimentDatasetId?: Array<SingleValueOption>;
};
export function observationEvalFilterColsWithOptions(
options?: ObservationEvalOptions,
cols: ColumnDefinition[] = observationEvalFilterColumns,
): ColumnDefinition[] {
return cols.map((col) => {
if (col.id === "environment") {
return formatColumnOptions(col, options?.environment ?? []);
}
if (col.id === "tags") {
return formatColumnOptions(col, options?.tags ?? []);
}
if (col.id === "traceName") {
return formatColumnOptions(col, options?.traceName ?? []);
}
if (col.id === "name") {
return formatColumnOptions(col, options?.name ?? []);
}
return col;
});
}
export function experimentEvalFilterColsWithOptions(
options?: ExperimentEvalOptions,
cols: ColumnDefinition[] = experimentEvalFilterColumns,
): ColumnDefinition[] {
return cols.map((col) => {
if (col.id === "experimentDatasetId") {
return formatColumnOptions(col, options?.experimentDatasetId ?? []);
}
return col;
});
}
/**
* Field mapper for observation eval filters.
* Maps camelCase filter column IDs to snake_case observation fields.
* Based on events table column definitions.
*
* @param observation - The observation data object
* @param column - The camelCase column ID from filter definitions
* @returns The value from the observation object
*/
export function mapEventEvalFilterColumnIdToField(
observation: ObservationForEval,
column: string,
) {
const columnMapping = eventsEvalFilterColumns.find((c) => c.id === column);
if (!columnMapping) {
return undefined;
}
return observation[columnMapping.internal];
}
+39 -4
View File
@@ -1,5 +1,17 @@
import z from "zod/v4";
export const EvalTargetObject = {
TRACE: "trace",
DATASET: "dataset",
EVENT: "event",
EXPERIMENT: "experiment",
} as const;
export type EvalTargetObject =
(typeof EvalTargetObject)[keyof typeof EvalTargetObject];
export const EvalTargetObjectSchema = z.enum(Object.values(EvalTargetObject));
export const langfuseObjects = [
"trace",
"span",
@@ -23,25 +35,32 @@ export const variableMapping = z
.object({
templateVariable: z.string(), // variable name in the template
// name of the observation to extract the variable from
// not required for trace, as we only have one.
// not required for trace or dataset_item, as we only have one of each.
objectName: z.string().nullish(),
langfuseObject: langfuseObject,
selectedColumnId: z.string(),
jsonSelector: z.string().nullish(),
})
.refine(
(value) => value.langfuseObject === "trace" || value.objectName !== null,
(value) =>
value.langfuseObject === "trace" ||
value.langfuseObject === "dataset_item" ||
value.objectName !== null,
{
message: "objectName is required for langfuseObjects other than trace",
message:
"objectName is required for observation objects (generation, span, score)",
},
);
export const variableMappingList = z.array(variableMapping);
// WIP version for forms - langfuseObject optional to support both:
// - Trace/Dataset evals: Include langfuseObject and objectName to specify which observation
// - Event/Experiment evals: Omit them since the observation is already selected
export const wipVariableMapping = z.object({
templateVariable: z.string(),
objectName: z.string().nullish(),
langfuseObject: langfuseObject,
langfuseObject: langfuseObject.optional(),
selectedColumnId: z.string().nullish(),
jsonSelector: z.string().nullish(),
});
@@ -157,3 +176,19 @@ export const JobTimeScopeZod = z.enum(["NEW", "EXISTING"]);
export type JobTimeScope = z.infer<typeof JobTimeScopeZod>;
export const TimeScopeSchema = z.array(JobTimeScopeZod).default(["NEW"]);
// Simplified variable mapping for observation-based evals.
// Unlike trace-based evals, we don't need objectName since we're directly
// targeting a specific observation - no need to specify which observation to extract from.
export const observationVariableMapping = z.object({
templateVariable: z.string(), // variable name in the template
selectedColumnId: z.string(), // column to extract (must match observationEvalVariableColumns.id)
jsonSelector: z.string().nullish(), // optional JSON path selector
});
export const observationVariableMappingList = z.array(
observationVariableMapping,
);
export type ObservationVariableMapping = z.infer<
typeof observationVariableMapping
>;
@@ -1,6 +1,4 @@
import z from "zod/v4";
import { JSONPath } from "jsonpath-plus";
import { variableMapping } from "./types";
/**
* Parses an unknown value to a string representation
@@ -67,10 +65,11 @@ function parseJsonDefault(selectedColumn: unknown, jsonSelector: string) {
export function extractValueFromObject(
obj: Record<string, unknown>,
mapping: z.infer<typeof variableMapping>,
parseJson?: (selectedColumn: unknown, jsonSelector: string) => unknown, // eslint-disable-line no-unused-vars
selectedColumnId: string,
jsonSelector?: string,
parseJson?: (selectedColumn: unknown, jsonSelector: string) => unknown,
): { value: string; error: Error | null } {
let selectedColumn = obj[mapping.selectedColumnId];
let selectedColumn = obj[selectedColumnId];
// Simple preprocessing: attempt to parse to valid JSON object
if (typeof selectedColumn === "string") {
@@ -82,9 +81,9 @@ export function extractValueFromObject(
let jsonSelectedColumn;
let error: Error | null = null;
if (mapping.jsonSelector && selectedColumn) {
if (jsonSelector && selectedColumn) {
try {
jsonSelectedColumn = jsonParser(selectedColumn, mapping.jsonSelector);
jsonSelectedColumn = jsonParser(selectedColumn, jsonSelector);
} catch (err) {
error =
err instanceof Error
@@ -14,9 +14,8 @@ export const SingleChatMessageSchema = PromptChatMessageSchema;
export type SingleChatMessage = z.infer<typeof SingleChatMessageSchema>;
export enum PromptType {
// eslint-disable-next-line no-unused-vars
Chat = "chat",
// eslint-disable-next-line no-unused-vars
Text = "text",
}
@@ -13,7 +13,7 @@ import {
*/
export const filterAndValidateDbScoreConfigList = (
scoreConfigs: ScoreConfigDbType[],
onParseError?: (error: z.ZodError) => void, // eslint-disable-line no-unused-vars
onParseError?: (error: z.ZodError) => void,
): ScoreConfigDomain[] =>
scoreConfigs.reduce((acc, ts) => {
const result = ScoreConfigSchema.safeParse(ts);
@@ -7,9 +7,14 @@ import {
ScoreDataTypeDomain,
ScoreSourceDomain,
} from "../../../../domain/scores";
import { singleFilter } from "../../../../interfaces/filters";
import { InvalidRequestError } from "../../../../errors";
const operators = ["<", ">", "<=", ">=", "!=", "="] as const;
export const SCORE_FIELD_GROUPS = ["score", "trace"] as const;
export type ScoreFieldGroup = (typeof SCORE_FIELD_GROUPS)[number];
/**
* Endpoints
*/
@@ -41,6 +46,31 @@ export const GetScoresQuery = z.object({
message: "Each score ID must be a string",
})
.nullish(),
fields: z
.string()
.nullish()
.transform((v) => {
if (!v) return null;
return v
.split(",")
.map((f) => f.trim())
.filter((f) => SCORE_FIELD_GROUPS.includes(f as ScoreFieldGroup));
})
.pipe(z.array(z.enum(SCORE_FIELD_GROUPS)).nullable()),
filter: z
.string()
.optional()
.transform((str) => {
if (!str) return undefined;
try {
const parsed = JSON.parse(str);
return parsed;
} catch (e) {
if (e instanceof InvalidRequestError) throw e;
throw new InvalidRequestError("Invalid JSON in filter parameter");
}
})
.pipe(z.array(singleFilter).optional()),
});
// POST /scores
@@ -69,6 +99,11 @@ export const PostScoresBody = applyScoreValidation(
dataType: z.literal("BOOLEAN"),
configId: z.string().nullish(),
}),
z.object({
value: z.string(), // Corrected output text
dataType: z.literal("CORRECTION"),
configId: z.undefined().nullish(), // Cannot have config
}),
z.object({
value: z.union([z.string(), z.number()]),
dataType: z.undefined(),
@@ -11,7 +11,7 @@ import { GetScoreResponseDataV1 } from "./endpoints";
*/
export const filterAndValidateV1GetScoreList = (
scores: unknown[],
// eslint-disable-next-line no-unused-vars
onParseError?: (error: z.ZodError) => void,
): z.infer<typeof GetScoreResponseDataV1>[] =>
scores.reduce(
@@ -12,19 +12,28 @@ export const GetScoresQueryV2 = GetScoresQuery.extend({
sessionId: z.string().nullish(),
traceId: z.string().nullish(),
datasetRunId: z.string().nullish(),
observationId: z
.string()
.transform((str) => str.split(",").map((id) => id.trim()))
.refine((arr) => arr.every((id) => typeof id === "string"), {
message: "Each observation ID must be a string",
})
.nullish(),
});
export const GetScoreResponseDataV2 = z.intersection(
APIScoreSchemaV2,
z.object({
trace: z
.object({
userId: z.string().nullish(),
tags: z.array(z.string()).nullish(),
environment: z.string().nullish(),
})
.nullish(),
}),
);
export const GetScoreResponseDataV2 = z
.intersection(
APIScoreSchemaV2,
z.object({
trace: z
.object({
userId: z.string().nullish(),
tags: z.array(z.string()).nullish(),
environment: z.string().nullish(),
})
.nullish(),
}),
)
.or(APIScoreSchemaV2);
export const GetScoresResponseV2 = z.object({
data: z.array(GetScoreResponseDataV2),
@@ -6,6 +6,11 @@ import {
} from "../../../../../domain";
import z from "zod/v4";
const CorrectionData = z.object({
stringValue: z.string(),
dataType: z.literal("CORRECTION"),
});
/**
* Foundation schema for scores API v2 i.e. trace, observation AND session scores
*
@@ -25,7 +30,12 @@ const ScoreFoundationSchemaV2 = ScoreSchemaExclReferencesAndDates.extend({
});
export const APIScoreSchemaV2 = ScoreFoundationSchemaV2.and(
z.discriminatedUnion("dataType", [NumericData, CategoricalData, BooleanData]),
z.discriminatedUnion("dataType", [
NumericData,
CategoricalData,
BooleanData,
CorrectionData,
]),
);
export type APIScoreV2 = z.infer<typeof APIScoreSchemaV2>;

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