Compare commits

...
145 Commits
Author SHA1 Message Date
Max Deichmann bc323772c1 chore: release v3.9.0 2025-01-17 12:26:06 +01:00
Marc KlingenandGitHub fcef737dc0 feat(cloud): snapshot usage metering to postgres db (#5075) 2025-01-17 12:13:46 +01:00
marliessophieandGitHub 4bf77d68ed feat(model): add support for o1 model costs (#5073)
* feat(model): add support for o1 model costs

* order cost list by ID

* push

* update contributing

* push
2025-01-17 10:23:25 +00:00
Steffen SchmitzandGitHub d7ec924ed8 chore: remove outdated postgres executions (#5069) 2025-01-17 11:10:20 +01:00
Marc KlingenandGitHub cefaf56613 chore(cloud): region details (#5080) 2025-01-17 00:18:00 +01:00
Max DeichmannandGitHub 6aff9b0f4a fix: quickly recover from metering queue errors (#5077)
fix: quickly reciver from metering queue errors
2025-01-16 22:11:55 +01:00
Max DeichmannandGitHub 815de149aa fix: retry stripe apis (#5076)
push
2025-01-16 21:57:05 +01:00
Max DeichmannandGitHub bc8923485e fix: add race condition check to billing queue (#5074) 2025-01-16 20:40:59 +01:00
Max DeichmannandGitHub 1a60425692 fix: retry stripe in metering logic and meter again if error ocurred (#5072) 2025-01-16 19:42:10 +01:00
marliessophieandGitHub 1d3ac98123 style(ui): improve observation type label colors for readbility, accessibility (#5071)
* style(ui): adjust muted colors for readbility, accessibility

* style(ui): adjust comment labels given type

* margins

* push
2025-01-16 16:27:47 +00:00
Max DeichmannandGitHub de67ec1a02 chore: add cloud meter event trigger (#5068)
* push

* push

* feat: add metering event
2025-01-16 15:22:59 +00:00
marliessophieandGitHub 9510995a67 fix(ui-tables): populate filter operator by default (#5067) 2025-01-16 14:21:48 +00:00
Marc KlingenandGitHub 4623d76155 fix(cloud): remove race condition check in metering cron job (#5066) 2025-01-16 11:31:00 +01:00
Steffen SchmitzandGitHub 6a6abc6bd8 chore: skip zod parse for json parsing (#5063)
* chore: skip zod parse for json parsing

* chore: lint

* chore: add JsonNested type

* chore: add one more test for invalid json
2025-01-16 09:32:41 +00:00
Max DeichmannandGitHub ddee827e22 chore: update architecture diagram (#5055) 2025-01-15 22:07:16 +00:00
Max DeichmannandGitHub e327db8bbd chore: correct data region (#5054) 2025-01-15 22:01:58 +00:00
Max DeichmannandGitHub 9229d759e1 chore: add project_id to get clickhouse query ingestion (#5051) 2025-01-15 20:09:53 +01:00
Hassieb PakzadandGitHub d8d8ab7060 fix(jump-to-playground): hide button if message parsing will fail (#5048) 2025-01-15 16:55:58 +01:00
Steffen SchmitzandGitHub b851359923 chore: remove legacy v3-migration env variables (#5041)
* chore: remove legacy v3-migration env variables

* chore: linting

* chore: move legacy files to current env

* chore: remove outdated tests

* chore: add async ingestion test

* chore: reduce delay for tests

* chore: lint

* chore: skip scores and enable redis on CI

* chore: remove outdated test classes
2025-01-15 15:39:20 +00:00
marliessophieandGitHub b36ea74314 fix(ui-annoation): bug when re-submitting score comments on categorical scores (#5045)
fix(ui-annoation): re-submit score comments on categorical scores
2025-01-15 15:12:46 +00:00
Hassieb PakzadandGitHub b2f103549a feat(trace-tree-iu): add hint for hidden obs (#5044) 2025-01-15 14:47:39 +01:00
marliessophieandGitHub f660b80438 style(ui-tables): fix position of No results. text if table has no data (#5043)
* style(ui-tables): fix position of `No results.` text if table has no data

* fix: dom nesting
2025-01-15 13:18:23 +00:00
Steffen SchmitzandGitHub 5bbfffef50 chore: allow any value for input/output in ingestion endpoint (#5040)
* chore: allow any value for input/output in ingestion endpoint

* chore: add explanation
2025-01-15 10:36:13 +00:00
Hassieb PakzadandGitHub cb6f95dc93 feat(trace-detail-ui): filter by minObservationlvl (#5039) 2025-01-15 11:40:57 +01:00
Max Deichmann 8345ce48fc chore: release v3.8.0 2025-01-15 10:26:15 +01:00
Max DeichmannandGitHub f71d28900a perf: use streaming for observation and trace list endpoints (#5038)
push
2025-01-15 10:25:28 +01:00
Hassieb PakzadandGitHub 14eaf62e45 fix(ingestion): overwriting of observation level (#5027) 2025-01-15 09:43:41 +01:00
Steffen SchmitzandGitHub f8e61c2659 chore: fallback to error.status for eval error handling (#5037) 2025-01-15 07:40:36 +00:00
marliessophieandGitHub 3b205b45ce chore(ui-dashboard): save selected score graphs in the dashboard to localStorage (project-bound) (#5029)
* chore(ui-dashboard): save selected score graphs in the dashboard to `localStorage` (project-bound)

* add comment

* push
2025-01-14 19:14:17 +00:00
Max DeichmannandGitHub 7260c44486 fix: correctly order sessions by time (#5028)
* fix: correctly order sessions by time

* fix: correctly order sessions by time

* fix: correctly order sessions by time
2025-01-14 19:00:53 +00:00
marliessophieandGitHub 92ec2b60e2 feat(ui-tabels): Count of group selection on column visibility (#5024) 2025-01-14 16:26:01 +00:00
Steffen SchmitzandGitHub a856488f2d chore: upgrade bullmq to 5.34.10 (#5022)
* chore: upgrade bullmq to 5.34.10

* chore: adjust tests
2025-01-14 13:59:55 +00:00
Steffen SchmitzandGitHub a18f09fd8d fix: process status codes on eval execution errors (#5013)
* fix: process status codes on eval execution errors

* chore: error handling
2025-01-14 12:29:44 +00:00
Hassieb PakzadandGitHub 2a95dd1da0 fix(openapi-models): modelusageunit to optional (#5015) 2025-01-14 10:25:07 +01:00
Hassieb PakzadandGitHub 04ebd5994f fix(fern-models): unit to optional commons.model (#5014) 2025-01-14 10:22:32 +01:00
Hassieb PakzadandGitHub 268b741a3a chore(models): update fern types startDate unit (#5004) 2025-01-13 19:53:41 +01:00
Steffen SchmitzandGitHub 8b686e9b31 fix: fail open on redis errors in ratelimitservice (#5001) 2025-01-13 17:29:17 +00:00
Max Deichmann e802a5c23a chore: release v3.7.1 2025-01-13 14:39:24 +01:00
Max DeichmannandGitHub 5180e2382e fix: drop dataset run item temp tables from all CH nodes (#4999) 2025-01-13 14:38:11 +01:00
Max DeichmannandGitHub 2cae1253d3 fix: fix session table sorting (#4995)
* fix: fix session table sorting

* push
2025-01-13 12:18:37 +00:00
ClemoandGitHub be2414a82c docs: fix typo 2025-01-13 10:54:43 +01:00
ClemoandGitHub 95e63c6f78 docs: Update careers 2025-01-13 10:53:54 +01:00
Marc Klingen 32b6502d13 chore: release v3.7.0 2025-01-12 09:59:41 +01:00
Marc KlingenandGitHub 1bc33c7b92 fix: telemetry error "invalid input syntax for type interval" (#4991) 2025-01-12 09:57:46 +01:00
Marc KlingenandGitHub 4967b20478 fix(ui): support menu forwardref error, memo list of options, keys for separators (#4989)
fix(ui): support menu forwardref error
2025-01-12 03:33:37 +00:00
Marc KlingenandGitHub 1130894cdc chore: remove old launch week notifications (#4986) 2025-01-12 03:18:13 +00:00
Marc KlingenandGitHub af276e9e78 feat(cloud): add banner for status page announcement (#4988)
* wip

* fix(ui): "chatisvisble not a function"

* csp

* feat(cloud): add banner for status page announcement
2025-01-12 03:17:45 +00:00
Marc KlingenandGitHub d69dbd81a2 fix(ui): chatIsVisible rendering issues (#4987)
fix(ui): "chatisvisble not a function"
2025-01-12 02:43:25 +00:00
Marc KlingenandGitHub 7e807c0de8 feat(annotation-queue): show "add to dataset" button on trace preview (#4985) 2025-01-12 02:24:57 +00:00
Marc KlingenandGitHub cfa2ac6da9 chore(ui): improve button labels for playground and experiments on prompt view (#4984) 2025-01-12 02:11:49 +00:00
Marc KlingenandGitHub 952a550847 feat(ui): add support dropdown menu to replace /support page (#4983) 2025-01-12 02:05:46 +00:00
Marc KlingenandGitHub 6199903117 chore(ui): add descriptions to the setup onboarding flow (#4982) 2025-01-12 00:00:19 +00:00
Marc KlingenandGitHub 88cdb8999d chore(ui): do not collapse chat messages on prompt view (#4981) 2025-01-11 23:48:35 +00:00
Marc KlingenGitHubellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
9ad2ef1f91 fix(ui): docs menu item should open in new tab (#4980)
* fix(ui): docs menu item should open in new tab

* Update web/src/components/nav-main.tsx

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>
2025-01-11 21:11:37 +00:00
Marc KlingenandGitHub 7a9717cb5e chore(ui): remove introducing orgs banner (#4979) 2025-01-11 20:59:51 +00:00
Marc KlingenandGitHub ce31a806c7 chore(cloud): improve ui of cloud region selection, remove login error notice (#4978)
chore(cloud): imrpove ui of cloud region selection, remove login error notice
2025-01-11 18:03:20 +00:00
Max DeichmannandGitHub e7e6cf1b99 feat: script to replay missing events (#4966)
* feat: script to replay missing events

* feat: script to replay missing events
2025-01-10 15:40:49 +00:00
Steffen SchmitzandGitHub 9f76457f22 chore: stream clickhouse results to posthog to reduce memory consumption (#4967)
* chore: stream clickhouse results to posthog to reduce memory consumption

* chore: end span after streaming all results

* chore: tracing for clickhouse streaming
2025-01-10 13:42:03 +00:00
Marc Klingen c79c84ea96 chore: release v3.6.2 2025-01-10 14:49:19 +01:00
Hassieb PakzadandGitHub 4634669fcd chore(docker-compose): add ingestion env vars (#4969) 2025-01-10 14:47:25 +01:00
Marc KlingenandGitHub 549e0eb629 chore: daily instead of hourly telemetry (#4968) 2025-01-10 14:10:51 +01:00
Steffen SchmitzandGitHub 80adf63fe6 fix: adjust deletion order for apikey invalidation on project delete (#4964) 2025-01-10 10:24:26 +00:00
Hassieb Pakzad 6c1d7534bb chore: release v3.6.1 2025-01-10 09:15:57 +01:00
Max DeichmannandGitHub 23a4bbd2cb fix: fix trace exports (#4958)
* fix: fix trace exports

* push

* fix: fix trace exports
2025-01-09 19:36:07 +00:00
Steffen SchmitzandGitHub 07d048d46f chore: enable clickhouse cluster mode by default (#4956) 2025-01-09 16:32:55 +00:00
Hassieb PakzadandGitHub bd7226bbb8 fix(pubic-api): observations and traces FINAL (#4951) 2025-01-09 17:41:35 +01:00
Steffen SchmitzandGitHub fe70d92211 chore: set created_at in ClickHouse based on min S3 timestamp (#4955)
* chore: set created_at in ClickHouse based on min S3 timestamp

* chore: fix worker tests
2025-01-09 16:12:23 +00:00
Max DeichmannandGitHub bb3621f31d fix: sessions metrics api performance (#4954) 2025-01-09 17:15:30 +01:00
Steffen SchmitzandGitHub 63225a9fd6 chore: accept projectId list which skips clickhouse reads during ingestion (#4952) 2025-01-09 14:13:23 +00:00
Max DeichmannandGitHub 947da9cc84 fix: sessions table id filter (#4953) 2025-01-09 14:33:09 +01:00
Max DeichmannandGitHub 93a0ab6f3a fix: remove trace tracesession fk (#4946)
* fix: remove trace tracesession fk

* push

* push

* push

* fix: remove trace tracesession fk

* fix: remove trace tracesession fk

* fix: remove trace tracesession fk
2025-01-09 09:50:58 +00:00
Hassieb PakzadandGitHub 68ad079d8c fix(models-table): sorting across pages (#4947) 2025-01-09 10:30:00 +01:00
Max DeichmannandGitHub cf29c6b7e4 feat: backup redis queues to postgres (#4943)
feat: backup redis queuer
2025-01-08 23:15:03 +00:00
Max DeichmannandGitHub e4d2a5ec6c perf: improve session all time search performance (#4926)
* push

* fix: correctly export categorical scores for trace exports

* fixes

* push

* Merge branch 'max/lfe-3345-fix-session-table-all-time-search' of https://github.com/langfuse/langfuse into max/lfe-3345-fix-session-table-all-time-search

* fix

* fixes
2025-01-08 16:54:01 +00:00
Steffen SchmitzandGitHub 35f3092a1c feat: retry 429 errors for eval executions for up to 24h (#4933)
* feat: retry 429 errors for eval executions for up to 24h

* chore: skip type error retries for eval execution
2025-01-08 14:51:36 +00:00
Hassieb PakzadandGitHub 7b222318e1 chore(cost): add langchain-compatible price keys (#4932) 2025-01-08 15:49:21 +01:00
Steffen SchmitzandGitHub d9c077ea56 chore: add BUILD_ID to otel setup and exclude health from tracing (#4930)
* chore: add BUILD_ID to otel setup

* chore: remove comment
2025-01-08 13:17:00 +00:00
Hassieb PakzadandGitHub 1ec7325b85 feat(models): lastUsed column to full lookback (#4931) 2025-01-08 14:21:07 +01:00
Steffen SchmitzandGitHub 96b2127f36 chore: reduce number of parsing iterations for observations and traces (#4927)
* chore: reduce number of parsing iterations for observations and traces

* chore: lint
2025-01-08 12:36:37 +00:00
Max DeichmannandGitHub d3c2b8cf27 fix: correctly export categorical scores for trace exports (#4928)
* fix: correctly export categorical scores for trace exports

* push
2025-01-08 11:31:50 +00:00
Hassieb PakzadandGitHub beeef222e4 feat(models): sort models table by lastUsed (#4925) 2025-01-08 12:06:57 +01:00
steffen911 ee376f9cad chore: release v3.6.0 2025-01-07 14:43:35 +01:00
c8936ec68d feat: add CLICKHOUSE_DB support (#4911)
* take CLICKHOUSE_DB environment variable into account

Fixes: #4888
Signed-off-by: Aleksandar Pesic <peske.nis@gmail.com>

* chore: cleanup env usage

* chore: extend env.prod.example

---------

Signed-off-by: Aleksandar Pesic <peske.nis@gmail.com>
Co-authored-by: Aleksandar Pesic <peske.nis@gmail.com>
2025-01-07 13:18:03 +00:00
Max DeichmannandGitHub 8d8170c62c security: prevent prototype pollution in dataset compare view (#4910) 2025-01-07 12:15:33 +00:00
Max DeichmannandGitHub 72e1a43192 security: upgrade nanoid (#4909) 2025-01-07 11:47:31 +00:00
Hassieb PakzadandGitHub a842625bd1 chore(media): increase rate-limit to use ingestion budget (#4907) 2025-01-07 11:42:07 +01:00
Max DeichmannandGitHub 890a542759 fix: do not error on 403 by model provider for eval executions (#4905)
* fix: do not error on 403 by model provider

* push
2025-01-07 10:23:58 +00:00
Steffen SchmitzandGitHub 1e8af5b821 chore: move sessions, traces, comments router logs to logger (#4902)
* chore: move sessions router logs to logger

* chore: adjust traces and comments
2025-01-07 07:45:44 +00:00
Hassieb PakzadandGitHub d3f3f354db fix(models): allow string tokenizer config (#4898) 2025-01-06 16:24:49 +01:00
steffen911 18a7cb6684 chore: release v3.5.3 2025-01-06 13:43:19 +01:00
Steffen SchmitzandGitHub 2d70ca574e chore: skip existence check for score deletion (#4896)
* chore: skip existence check for score deletion

* chore: lint
2025-01-06 11:03:51 +00:00
Steffen SchmitzandGitHub 1c980c78dd fix: use singular unit in clickhouse datediff for 23.x support (#4894) 2025-01-06 10:35:53 +00:00
Marc KlingenGitHubMax Deichmanndependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Steffen SchmitzHassieb Pakzad
5170718aad fix(prompt-experiments): do not restrict to evalModelsOnly in order to use anthropic, bedrock, vertex (#4872)
* fix(prompt-experiments): do not restrict to evalModelsOnly in order to use anthropic, bedrock, vertex

* chore(cloud): disable deletion of traces via UI as it caused performance issues (#4873)

chore(cloud): disable deletion of traces via the UI as it caused performance issues

* docs: add link to data model documentation to api reference of ingestion endpoint (#4877)

* fix: use database upserts correctly (#4884)

* fix: use database upserts correctly

* fix: use database upserts correctly

* push

* fix: use database upserts correctly

* fix: use database upserts correctly

* push

* fix: use database upserts correctly

* chore(deps): bump next from 14.2.15 to 14.2.21 (#4887)

Bumps [next](https://github.com/vercel/next.js) from 14.2.15 to 14.2.21.
- [Release notes](https://github.com/vercel/next.js/releases)
- [Changelog](https://github.com/vercel/next.js/blob/canary/release.js)
- [Commits](https://github.com/vercel/next.js/compare/v14.2.15...v14.2.21)

---
updated-dependencies:
- dependency-name: next
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* chore: release v3.5.2

* chore: remove postgres from ingestion merge (#4467)

* chore: remove postgres from ingestion merge

* chore: remove pg tests

* chore: remove postgres tracing

* remove evalModelsOnly

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: Max Deichmann <m.deichmann@tum.de>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Steffen Schmitz <steffen@langfuse.com>
Co-authored-by: Hassieb Pakzad <68423100+hassiebp@users.noreply.github.com>
2025-01-06 10:29:57 +00:00
Steffen SchmitzandGitHub a01c27f4cc chore: remove postgres from ingestion merge (#4467)
* chore: remove postgres from ingestion merge

* chore: remove pg tests

* chore: remove postgres tracing
2025-01-06 09:29:26 +00:00
Max Deichmann 51354e6c6d chore: release v3.5.2 2025-01-06 10:34:55 +01:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
10dd50cff0 chore(deps): bump next from 14.2.15 to 14.2.21 (#4887)
Bumps [next](https://github.com/vercel/next.js) from 14.2.15 to 14.2.21.
- [Release notes](https://github.com/vercel/next.js/releases)
- [Changelog](https://github.com/vercel/next.js/blob/canary/release.js)
- [Commits](https://github.com/vercel/next.js/compare/v14.2.15...v14.2.21)

---
updated-dependencies:
- dependency-name: next
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-06 09:19:47 +00:00
Max DeichmannandGitHub d8783f8698 fix: use database upserts correctly (#4884)
* fix: use database upserts correctly

* fix: use database upserts correctly

* push

* fix: use database upserts correctly

* fix: use database upserts correctly

* push

* fix: use database upserts correctly
2025-01-03 21:45:56 +00:00
Marc KlingenandGitHub 16a572f51d docs: add link to data model documentation to api reference of ingestion endpoint (#4877) 2025-01-02 21:10:26 +00:00
Marc KlingenandGitHub 947641cbbf chore(cloud): disable deletion of traces via UI as it caused performance issues (#4873)
chore(cloud): disable deletion of traces via the UI as it caused performance issues
2025-01-02 19:03:40 +01:00
Marc KlingenandGitHub 249e161473 chore: remove beta label from evaluation menu (#4871) 2025-01-02 15:14:32 +00:00
Marc KlingenandGitHub 90bab4386f fix(ui): improve /trace/:id redirect when trace is accessed while still being processed (#4868) 2025-01-02 13:51:56 +00:00
Max DeichmannandGitHub 33fddb1344 fix: support large amount of traces for sessions in session.byid (#4847) 2024-12-29 21:42:50 +00:00
Hassieb PakzadandGitHub c24dcb068a fix(legacy-ingestion): usageDetails support (#4841) 2024-12-27 10:11:24 +01:00
Max DeichmannandGitHub 2986723254 fix: ensure idemptence of project deletion queue (#4831) 2024-12-24 11:49:29 +00:00
Max DeichmannandGitHub b45b2fd8f8 chore: clean up error message bullmq endpoint (#4826) 2024-12-23 21:09:57 +00:00
Marc Klingen 8936fb4ad9 chore: release v3.5.0 2024-12-23 17:39:13 +01:00
Marc KlingenandGitHub 7d7e25b39c fix: use clickhouse for tracing usage stats (#4825) 2024-12-23 17:07:50 +01:00
Marc KlingenandGitHub 138d2f1c09 feat(ui): switch to github discussions and issues for in-product feedback (#4822) 2024-12-21 14:16:27 +01:00
Steffen SchmitzandGitHub 51595eb15d chore: export core information tables to S3 daily (#4821) 2024-12-21 12:23:10 +00:00
Marc KlingenandGitHub 6ee68cf508 chore: posthog integration out of beta (#4820)
chore: Posthog integration out of Beta
2024-12-21 12:03:30 +00:00
Marc KlingenandGitHub 308513e644 chore(ui): remove dateRangeFilter on prompt metrics table as no longer necessary on CH (#4818)
* chore(ui): remove dateRangeFilter on prompt metrics table as no longer necessary on CH

* fix lint
2024-12-21 11:49:03 +00:00
Marc KlingenandGitHub 155543bc93 feat(ui): enable the creation of a new LLM connection from the LLM selection dropdown (#4819)
feat(ui): allow to create new llm connection from llm selection dropdown
2024-12-21 11:46:34 +00:00
Marc KlingenandGitHub 8f961925d5 feat(ui): add export status table to project settings (#4816) 2024-12-21 04:38:03 +01:00
Marc KlingenandGitHub 7ddc844d7d fix(ui): clarify what applying producation label to a new prompt does (#4812)
* fix(ui): clarify what applying producation label to a new prompt does

* fix lint and copy
2024-12-21 01:54:52 +00:00
Marc KlingenandGitHub 5381c5f19b feat(cloud): improve open/close state of support chat (#4815)
chore(cloud): hide chat by default, show when opened manually or on incoming/unread messages, persist in session storage
2024-12-21 01:40:39 +00:00
Marc Klingen dcd6060665 chore: release v3.4.0 2024-12-20 22:41:08 +01:00
Chris TranandGitHub 0bfba7d690 feat(auth): add AUTH_CUSTOM_CLIENT_AUTH_METHOD to support PKCE code flow (#4810) 2024-12-20 22:34:02 +01:00
Hassieb Pakzad 25b363e1bc chore: fix eslint error 2024-12-20 18:54:34 +01:00
Hassieb PakzadandGitHub 0de37b0437 feat(models): add UI to add all usage types (#4802) 2024-12-20 18:49:44 +01:00
Max DeichmannandGitHub ab5febb9c7 fix: include time filter into user metrics query (#4808)
* fixes: fix user table sorting and filters

* fixes: fix user table sorting and filters

* fixes: fix user table sorting and filters
2024-12-20 15:59:11 +00:00
marliessophieandGitHub 0e5cade9e7 chore(ui): toggle json editor for text prompts (#4807) 2024-12-20 15:22:28 +00:00
Max DeichmannandGitHub 1438e052df fix: fix json parsing of trace / observation i/o (#4805)
* remove only

* push

* remove only
2024-12-20 12:54:47 +00:00
Max DeichmannandGitHub 9917aff8cf fix: add latencies to trace exports (#4804)
* fix: add latencies to trace exports

* remove only
2024-12-20 12:23:52 +00:00
marliessophieandGitHub c915c2b678 chore(ui): move dataset details into popover on runs table (#4801)
* chore(ui): move dataset details into popover on runs table

* push
2024-12-20 11:41:38 +00:00
marliessophieandGitHub 50379bd4b5 feat(datasets): support charts for scores and resource metrics (#4754)
* fixup: dataset run analytics; compare

* refactor: extract common timeseries component

* push

* fix: proper limit and offset settings

* chore: extend route to accept run ids

* eslint

* chore: order chart data ASC

* styles

* push

* push

* style: adjust multi-select component

* push

* push

* push
2024-12-20 10:14:37 +00:00
Marc KlingenandGitHub 8fd93ecff9 fix(cloud): remove unnecessary posthog events (#4798) 2024-12-20 01:59:39 +01:00
Max DeichmannandGitHub 046c6c6125 fix: fix tests (#4797) 2024-12-20 00:04:31 +01:00
Max DeichmannandGitHub ba750a2d55 fix: fix time to first token in trace detail view (#4796) 2024-12-19 23:41:40 +01:00
Marc Klingen 485120806e chore: release v3.3.0 2024-12-19 22:02:50 +01:00
87acc4f7f3 feat: redirect ingestion traffic for specific projects to secondary queue (#4792)
* feat: redirect specific projects to secondary ingestion queue

* feat: redirect specific projects to secondary ingestion queue

* push

* Update worker/src/queues/ingestionQueue.ts

Co-authored-by: Steffen Schmitz <steffen@langfuse.com>

* push

* push

---------

Co-authored-by: Steffen Schmitz <steffen@langfuse.com>
2024-12-19 16:06:20 +00:00
Steffen SchmitzandGitHub 21b1faa21b fix: adjust clickhouse type mapping for session duration filter (#4791) 2024-12-19 13:04:39 +00:00
Max DeichmannandGitHub 76cfaf1069 chore: measure lookup hits in ingestion pipeline (#4787) 2024-12-19 10:20:10 +01:00
dbe808d520 feat(auth): add AUTH_IGNORE_ACCOUNT_FIELDS to sanitize IDP fields before creating an account (#4728)
* feat: Field sanitization before creating an Account

* add comments

---------

Co-authored-by: Marc Klingen <git@marcklingen.com>
2024-12-18 23:26:56 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
6c977429c2 chore(deps-dev): bump tailwindcss from 3.4.13 to 3.4.17 (#4784)
Bumps [tailwindcss](https://github.com/tailwindlabs/tailwindcss) from 3.4.13 to 3.4.17.
- [Release notes](https://github.com/tailwindlabs/tailwindcss/releases)
- [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/v3.4.17/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/tailwindcss/compare/v3.4.13...v3.4.17)

---
updated-dependencies:
- dependency-name: tailwindcss
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-18 23:24:43 +00:00
Marc KlingenandGitHub c42c6a0f91 chore: do not add latest, major, and major.minor tags to docker images of -rc versions (#4781) 2024-12-18 21:37:46 +01:00
Max DeichmannandGitHub f14b69f68d chore: add attributes to ingestion queue processing (#4779)
git push
2024-12-18 19:54:32 +00:00
marliessophieandGitHub dd892459a4 chore(ui): order score names alphabetically in dropdowns (#4776)
* chore(ui): order score names alphabetically in dropdowns

* push
2024-12-18 16:40:04 +00:00
Marc KlingenandGitHub 3488b5c443 docs: Update README.md 2024-12-18 03:51:09 +01:00
Max DeichmannandGitHub 6e0f4483b4 chore: increase rate limits (#4751) 2024-12-17 15:55:18 +00:00
Marc KlingenandGitHub b5d9fb3e63 chore(cloud): remove posthog metrics cron job (#4753) 2024-12-17 15:52:42 +00:00
Marc KlingenandGitHub e087da6b6e docs: update README.md 2024-12-17 12:53:21 +01:00
Marc KlingenandGitHub ea8734d709 docs: update readme (#4749) 2024-12-17 12:49:14 +01:00
Hassieb PakzadandGitHub 9cf4e57da1 fix(dashboard-modelUsage): none model name handling (#4748) 2024-12-17 10:52:36 +01:00
Marc KlingenandGitHub 292501b6dd feat(ui): add organizations menu item link for all global pages (#4741) 2024-12-16 20:46:57 +00:00
Hassieb PakzadandGitHub 2a1424e4d0 fix(dashboard-modelUsage): NaN handling on totals (#4738) 2024-12-16 18:28:19 +01:00
248 changed files with 9172 additions and 14173 deletions
-86
View File
@@ -1,86 +0,0 @@
# When adding additional environment variables, the schema in "/src/env.mjs"
# should be updated accordingly.
# Prisma
# https://www.prisma.io/docs/reference/database-reference/connection-urls#env
DIRECT_URL="postgresql://postgres:postgres@localhost:5432/postgres"
DATABASE_URL="postgresql://postgres:postgres@localhost:5432/postgres"
# Clickhouse
CLICKHOUSE_MIGRATION_URL="clickhouse://localhost:9000"
CLICKHOUSE_URL="http://localhost:8123"
CLICKHOUSE_USER="clickhouse"
CLICKHOUSE_PASSWORD="clickhouse"
CLICKHOUSE_CLUSTER_ENABLED="false"
# Next Auth
# You can generate a new secret on the command line with:
# openssl rand -base64 32
# https://next-auth.js.org/configuration/options#secret
# NEXTAUTH_SECRET=""
NEXTAUTH_URL="http://localhost:3000"
NEXTAUTH_SECRET="secret"
# Langfuse Cloud Environment
NEXT_PUBLIC_LANGFUSE_CLOUD_REGION="DEV"
# Langfuse experimental features
LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES="true"
# Salt for API key hashing
SALT="salt"
# Email
EMAIL_FROM_ADDRESS="" # Defines the email address to use as the from address.
SMTP_CONNECTION_URL="" # Defines the connection url for smtp server.
# S3 Batch Exports
LANGFUSE_S3_BATCH_EXPORT_ENABLED=true
LANGFUSE_S3_BATCH_EXPORT_BUCKET=langfuse
LANGFUSE_S3_BATCH_EXPORT_ACCESS_KEY_ID=minio
LANGFUSE_S3_BATCH_EXPORT_SECRET_ACCESS_KEY=miniosecret
LANGFUSE_S3_BATCH_EXPORT_REGION=us-east-1
LANGFUSE_S3_BATCH_EXPORT_ENDPOINT=http://localhost:9090
## Necessary for minio compatibility
LANGFUSE_S3_BATCH_EXPORT_FORCE_PATH_STYLE=true
LANGFUSE_S3_BATCH_EXPORT_PREFIX=exports/
# # S3 Media Upload LOCAL
LANGFUSE_S3_MEDIA_UPLOAD_BUCKET=langfuse
LANGFUSE_S3_MEDIA_UPLOAD_ACCESS_KEY_ID=minio
LANGFUSE_S3_MEDIA_UPLOAD_SECRET_ACCESS_KEY=miniosecret
LANGFUSE_S3_MEDIA_UPLOAD_REGION=us-east-1
LANGFUSE_S3_MEDIA_UPLOAD_ENDPOINT=http://localhost:9090
## Necessary for minio compatibility
LANGFUSE_S3_MEDIA_UPLOAD_FORCE_PATH_STYLE=true
LANGFUSE_S3_MEDIA_UPLOAD_PREFIX=media/
# S3 Event Bucket Upload
## Set to true to test uploading all events to S3
LANGFUSE_S3_EVENT_UPLOAD_BUCKET=langfuse
LANGFUSE_S3_EVENT_UPLOAD_ACCESS_KEY_ID=minio
LANGFUSE_S3_EVENT_UPLOAD_SECRET_ACCESS_KEY=miniosecret
LANGFUSE_S3_EVENT_UPLOAD_REGION=us-east-1
LANGFUSE_S3_EVENT_UPLOAD_ENDPOINT=http://localhost:9090
## Necessary for minio compatibility
LANGFUSE_S3_EVENT_UPLOAD_FORCE_PATH_STYLE=true
LANGFUSE_S3_EVENT_UPLOAD_PREFIX=events/
# Set during docker build of application
# Used to disable environment verification at build time
# DOCKER_BUILD=1
REDIS_HOST="127.0.0.1"
REDIS_PORT=6379
REDIS_AUTH="myredissecret"
# openssl rand -hex 32 used only here
ENCRYPTION_KEY=0000000000000000000000000000000000000000000000000000000000000000
# speeds up local development by not executing init scripts on server startup
NEXT_PUBLIC_LANGFUSE_RUN_NEXT_INIT="false"
LANGFUSE_READ_FROM_POSTGRES_ONLY=true
LANGFUSE_RETURN_FROM_CLICKHOUSE=false
LANGFUSE_READ_DASHBOARDS_FROM_CLICKHOUSE=false
LANGFUSE_READ_FROM_CLICKHOUSE_ONLY=false
+3
View File
@@ -55,6 +55,7 @@ OTEL_SERVICE_NAME="langfuse"
# Auth, optional configuration
# AUTH_DOMAINS_WITH_SSO_ENFORCEMENT=domain1.com,domain2.com
# AUTH_IGNORE_ACCOUNT_FIELDS=foo,bar
# AUTH_DISABLE_USERNAME_PASSWORD=true
# AUTH_DISABLE_SIGNUP=true
# AUTH_SESSION_MAX_AGE=43200 # 30 days in minutes (default)
@@ -100,6 +101,7 @@ OTEL_SERVICE_NAME="langfuse"
# AUTH_CUSTOM_ISSUER=
# AUTH_CUSTOM_NAME=
# AUTH_CUSTOM_SCOPE="openid email profile" # optional
# AUTH_CUSTOM_CLIENT_AUTH_METHOD="client_secret_basic" # optional
# AUTH_CUSTOM_ALLOW_ACCOUNT_LINKING=false
# Transactional email, optional
@@ -243,6 +245,7 @@ OTEL_SERVICE_NAME="langfuse"
# CLICKHOUSE_URL=
# CLICKHOUSE_USER=
# CLICKHOUSE_PASSWORD=
# CLICKHOUSE_DB=
# Ingestion
# LANGFUSE_INGESTION_QUEUE_DELAY_MS=
+12 -9
View File
@@ -111,8 +111,9 @@ jobs:
- name: Load default env
run: |
cp .env.dev.example .env
grep -v -e '^LANGFUSE_S3_BATCH_EXPORT_ENABLED=' -e '^REDIS_HOST=' -e '^NEXT_PUBLIC_LANGFUSE_RUN_NEXT_INIT=' .env.dev.legacy.example > .env
echo "LANGFUSE_POSTGRES_INGESTION_ENABLED=true" >> .env
grep -v -e '^LANGFUSE_S3_BATCH_EXPORT_ENABLED=' -e '^NEXT_PUBLIC_LANGFUSE_RUN_NEXT_INIT=' .env.dev.example > .env
echo "LANGFUSE_INGESTION_QUEUE_DELAY_MS=1" >> .env
echo "LANGFUSE_INGESTION_CLICKHOUSE_WRITE_INTERVAL_MS=1" >> .env
- name: Run + migrate
run: |
docker compose -f docker-compose.dev.yml up -d
@@ -184,7 +185,9 @@ jobs:
- name: Load default env
run: |
cp .env.dev${{ matrix.blob-provider }}.example .env
grep -v -e '^LANGFUSE_S3_BATCH_EXPORT_ENABLED=' -e '^REDIS_HOST=' -e '^NEXT_PUBLIC_LANGFUSE_RUN_NEXT_INIT=' .env.dev${{ matrix.blob-provider }}.example > .env
grep -v -e '^LANGFUSE_S3_BATCH_EXPORT_ENABLED=' -e '^NEXT_PUBLIC_LANGFUSE_RUN_NEXT_INIT=' .env.dev${{ matrix.blob-provider }}.example > .env
echo "LANGFUSE_INGESTION_QUEUE_DELAY_MS=1" >> .env
echo "LANGFUSE_INGESTION_CLICKHOUSE_WRITE_INTERVAL_MS=1" >> .env
- name: Run + migrate
run: |
docker compose -f docker-compose.dev${{ matrix.blob-provider }}.yml up -d
@@ -459,9 +462,9 @@ jobs:
type=ref,event=pr
type=sha
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
type=raw,value=latest,enable=${{ startsWith(github.ref, 'refs/tags/v3') }}
type=semver,pattern={{major}}.{{minor}},enable=${{ !contains(github.ref, '-rc') }}
type=semver,pattern={{major}},enable=${{ !contains(github.ref, '-rc') }}
type=raw,value=latest,enable=${{ startsWith(github.ref, 'refs/tags/v3') && !contains(github.ref, '-rc') }}
- name: Build and push Docker image (web)
uses: docker/build-push-action@v4
with:
@@ -487,9 +490,9 @@ jobs:
type=ref,event=pr
type=sha
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
type=raw,value=latest,enable=${{ startsWith(github.ref, 'refs/tags/v3') }}
type=semver,pattern={{major}}.{{minor}},enable=${{ !contains(github.ref, '-rc') }}
type=semver,pattern={{major}},enable=${{ !contains(github.ref, '-rc') }}
type=raw,value=latest,enable=${{ startsWith(github.ref, 'refs/tags/v3') && !contains(github.ref, '-rc') }}
- name: Build and push Docker image (worker)
uses: docker/build-push-action@v4
with:
-1
View File
@@ -39,7 +39,6 @@ yarn-error.log*
!.env.dev.example
!.env.dev-azure.example
!.env.prod.example
!.env.dev.legacy.example
# vercel
.vercel
+33 -29
View File
@@ -121,36 +121,40 @@ flowchart TB
end
end
DB[Postgres Database]
Redis[Redis Cache/Queue]
Clickhouse[Clickhouse Database]
subgraph s9 ["VPC (US and EU separated)"]
DB[Postgres Database]
Redis[Redis Cache/Queue]
Clickhouse[Clickhouse Database]
subgraph s1["Application (langfuse/langfuse/web)"]
API[Public HTTP API]
G[TRPC API]
I[NextAuth]
H[React Frontend]
ORM
H --> G
H --> I
G --> I
G --- ORM
API --- ORM
I --- ORM
end
subgraph s5["Application (langfuse/langfuse/worker)"]
Worker
end
Worker --- DB
Worker --- Redis
Worker --- Clickhouse
ORM --- DB
ORM --- Redis
ORM --- Clickhouse
subgraph s1["Application (langfuse/langfuse/web)"]
API[Public HTTP API]
G[TRPC API]
I[NextAuth]
H[React Frontend]
ORM
H --> G
H --> I
G --> I
G --- ORM
API --- ORM
I --- ORM
end
subgraph s5["Application (langfuse/langfuse/worker)"]
Worker
end
Worker --- DB
Worker --- Redis
Worker --- Clickhouse
ORM --- DB
ORM --- Redis
ORM --- Clickhouse
JS --- API
Python --- API
```
@@ -397,7 +401,7 @@ The background color of the following component will be `hsl(var(--primary))` an
| --primary-accent | Primary accent color used for branding | Layout |
| --hover-primary-accent | Primary accent color used for hover effects for links | SignIn and AuthCloudRegionSwitch |
| --muted-green | Muted green for Event label | ObservationTree |
| --muted-orange | Muted orange for Generation label | ObservationTree |
| --muted-magenta | Muted magenta for Generation label | ObservationTree |
| --muted-blue | Muted blue for Span label | ObservationTree |
| --muted-gray | Muted gray for disabled status badges | StatusBadge |
| --accent-light-green | Light green accent for background of output and assistant messages | IOPreview, Generations, Traces |
@@ -439,7 +443,7 @@ You can update the default AI models and prices by adding or updating an entry i
Please note that
- prices are in USD
- the list is ordered by ID, so make sure to keep this order
- the list is ordered by ID, so make sure to keep this order and insert new models at the end of the list
- the `updated_at` field must be updated with the current date in ISO 8601 format. Otherwise, the change will be ignored.
### Transition period until V3 release
+45 -33
View File
@@ -42,9 +42,7 @@
## Langfuse Overview
_Unmute video for voice-over_
https://github.com/langfuse/langfuse/assets/2834609/a94062e9-c782-4ee9-af59-dee6370149a8
[![Langfuse Overview Video](https://github.com/user-attachments/assets/3926b288-ff61-4b95-8aa1-45d041c70866)](https://langfuse.com/watch-demo)
### Develop
@@ -194,37 +192,51 @@ You can opt-out by setting `TELEMETRY_ENABLED=false`.
### Open Source Projects Using Langfuse
Top open-source Python projects that use Langfuse, ranked by stars ([Source](https://github.com/langfuse/langfuse-docs/blob/main/components-mdx/dependents/python.md)):
Top open-source Python projects that use Langfuse, ranked by stars ([Source](https://github.com/langfuse/langfuse-docs/blob/main/components-mdx/dependents)):
| Repository | Stars |
| :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----: |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/127165244?s=40&v=4" width="20" height="20" alt=""> &nbsp; [langgenius](https://github.com/langgenius) / [dify](https://github.com/langgenius/dify) | 53464 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/158137808?s=40&v=4" width="20" height="20" alt=""> &nbsp; [open-webui](https://github.com/open-webui) / [open-webui](https://github.com/open-webui/open-webui) | 49387 |
| <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) | 37087 |
| <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) | 36690 |
| <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) | 26871 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/60330232?s=40&v=4" width="20" height="20" alt=""> &nbsp; [PostHog](https://github.com/PostHog) / [posthog](https://github.com/PostHog/posthog) | 22348 |
| <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) | 14508 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/78410652?s=40&v=4" width="20" height="20" alt=""> &nbsp; [GreyDGL](https://github.com/GreyDGL) / [PentestGPT](https://github.com/GreyDGL/PentestGPT) | 7319 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/152537519?s=40&v=4" width="20" height="20" alt=""> &nbsp; [superagent-ai](https://github.com/superagent-ai) / [superagent](https://github.com/superagent-ai/superagent) | 5352 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/7250217?s=40&v=4" width="20" height="20" alt=""> &nbsp; [Canner](https://github.com/Canner) / [WrenAI](https://github.com/Canner/WrenAI) | 2084 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/11855343?s=40&v=4" width="20" height="20" alt=""> &nbsp; [pingcap](https://github.com/pingcap) / [autoflow](https://github.com/pingcap/autoflow) | 1936 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/85268109?s=40&v=4" width="20" height="20" alt=""> &nbsp; [MLSysOps](https://github.com/MLSysOps) / [MLE-agent](https://github.com/MLSysOps/MLE-agent) | 1140 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/158137808?s=40&v=4" width="20" height="20" alt=""> &nbsp; [open-webui](https://github.com/open-webui) / [pipelines](https://github.com/open-webui/pipelines) | 1044 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/125468716?s=40&v=4" width="20" height="20" alt=""> &nbsp; [topoteretes](https://github.com/topoteretes) / [cognee](https://github.com/topoteretes/cognee) | 945 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/188657705?s=40&v=4" width="20" height="20" alt=""> &nbsp; [bRAGAI](https://github.com/bRAGAI) / [bRAG-langchain](https://github.com/bRAGAI/bRAG-langchain) | 770 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/169500408?s=40&v=4" width="20" height="20" alt=""> &nbsp; [opslane](https://github.com/opslane) / [opslane](https://github.com/opslane/opslane) | 675 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/151867818?s=40&v=4" width="20" height="20" alt=""> &nbsp; [dynamiq-ai](https://github.com/dynamiq-ai) / [dynamiq](https://github.com/dynamiq-ai/dynamiq) | 583 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/48585267?s=40&v=4" width="20" height="20" alt=""> &nbsp; [theopenconversationkit](https://github.com/theopenconversationkit) / [tock](https://github.com/theopenconversationkit/tock) | 505 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/20493493?s=40&v=4" width="20" height="20" alt=""> &nbsp; [andysingal](https://github.com/andysingal) / [llm-course](https://github.com/andysingal/llm-course) | 383 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/132396805?s=40&v=4" width="20" height="20" alt=""> &nbsp; [phospho-app](https://github.com/phospho-app) / [phospho](https://github.com/phospho-app/phospho) | 379 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/178644984?s=40&v=4" width="20" height="20" alt=""> &nbsp; [sentient-engineering](https://github.com/sentient-engineering) / [agent-q](https://github.com/sentient-engineering/agent-q) | 357 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/127165244?s=40&v=4" width="20" height="20" alt=""> &nbsp; [langgenius](https://github.com/langgenius) / [dify](https://github.com/langgenius/dify) | 54865 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/158137808?s=40&v=4" width="20" height="20" alt=""> &nbsp; [open-webui](https://github.com/open-webui) / [open-webui](https://github.com/open-webui/open-webui) | 51531 |
| <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/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 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/119600397?s=40&v=4" width="20" height="20" alt=""> &nbsp; [twentyhq](https://github.com/twentyhq) / [twenty](https://github.com/twentyhq/twenty) | 24195 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/60330232?s=40&v=4" width="20" height="20" alt=""> &nbsp; [PostHog](https://github.com/PostHog) / [posthog](https://github.com/PostHog/posthog) | 22618 |
| <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) | 15151 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/179202840?s=40&v=4" width="20" height="20" alt=""> &nbsp; [mediar-ai](https://github.com/mediar-ai) / [screenpipe](https://github.com/mediar-ai/screenpipe) | 11037 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/105877416?s=40&v=4" width="20" height="20" alt=""> &nbsp; [formbricks](https://github.com/formbricks) / [formbricks](https://github.com/formbricks/formbricks) | 9386 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/76263028?s=40&v=4" width="20" height="20" alt=""> &nbsp; [anthropics](https://github.com/anthropics) / [courses](https://github.com/anthropics/courses) | 8385 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/78410652?s=40&v=4" width="20" height="20" alt=""> &nbsp; [GreyDGL](https://github.com/GreyDGL) / [PentestGPT](https://github.com/GreyDGL/PentestGPT) | 7374 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/152537519?s=40&v=4" width="20" height="20" alt=""> &nbsp; [superagent-ai](https://github.com/superagent-ai) / [superagent](https://github.com/superagent-ai/superagent) | 5391 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/137907881?s=40&v=4" width="20" height="20" alt=""> &nbsp; [promptfoo](https://github.com/promptfoo) / [promptfoo](https://github.com/promptfoo/promptfoo) | 4976 |
| <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) | 4141 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/7250217?s=40&v=4" width="20" height="20" alt=""> &nbsp; [Canner](https://github.com/Canner) / [WrenAI](https://github.com/Canner/WrenAI) | 2526 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/11855343?s=40&v=4" width="20" height="20" alt=""> &nbsp; [pingcap](https://github.com/pingcap) / [autoflow](https://github.com/pingcap/autoflow) | 2061 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/85268109?s=40&v=4" width="20" height="20" alt=""> &nbsp; [MLSysOps](https://github.com/MLSysOps) / [MLE-agent](https://github.com/MLSysOps/MLE-agent) | 1161 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/158137808?s=40&v=4" width="20" height="20" alt=""> &nbsp; [open-webui](https://github.com/open-webui) / [pipelines](https://github.com/open-webui/pipelines) | 1100 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/18422723?s=40&v=4" width="20" height="20" alt=""> &nbsp; [alishobeiri](https://github.com/alishobeiri) / [thread](https://github.com/alishobeiri/thread) | 1074 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/125468716?s=40&v=4" width="20" height="20" alt=""> &nbsp; [topoteretes](https://github.com/topoteretes) / [cognee](https://github.com/topoteretes/cognee) | 971 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/188657705?s=40&v=4" width="20" height="20" alt=""> &nbsp; [bRAGAI](https://github.com/bRAGAI) / [bRAG-langchain](https://github.com/bRAGAI/bRAG-langchain) | 823 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/169500408?s=40&v=4" width="20" height="20" alt=""> &nbsp; [opslane](https://github.com/opslane) / [opslane](https://github.com/opslane/opslane) | 677 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/151867818?s=40&v=4" width="20" height="20" alt=""> &nbsp; [dynamiq-ai](https://github.com/dynamiq-ai) / [dynamiq](https://github.com/dynamiq-ai/dynamiq) | 639 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/48585267?s=40&v=4" width="20" height="20" alt=""> &nbsp; [theopenconversationkit](https://github.com/theopenconversationkit) / [tock](https://github.com/theopenconversationkit/tock) | 514 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/20493493?s=40&v=4" width="20" height="20" alt=""> &nbsp; [andysingal](https://github.com/andysingal) / [llm-course](https://github.com/andysingal/llm-course) | 394 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/132396805?s=40&v=4" width="20" height="20" alt=""> &nbsp; [phospho-app](https://github.com/phospho-app) / [phospho](https://github.com/phospho-app/phospho) | 384 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/178644984?s=40&v=4" width="20" height="20" alt=""> &nbsp; [sentient-engineering](https://github.com/sentient-engineering) / [agent-q](https://github.com/sentient-engineering/agent-q) | 370 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/168552753?s=40&v=4" width="20" height="20" alt=""> &nbsp; [sql-agi](https://github.com/sql-agi) / [DB-GPT](https://github.com/sql-agi/DB-GPT) | 324 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/60330232?s=40&v=4" width="20" height="20" alt=""> &nbsp; [PostHog](https://github.com/PostHog) / [posthog-foss](https://github.com/PostHog/posthog-foss) | 300 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/609489?s=40&v=4" width="20" height="20" alt=""> &nbsp; [aorwall](https://github.com/aorwall) / [moatless-tools](https://github.com/aorwall/moatless-tools) | 278 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/185116535?s=40&v=4" width="20" height="20" alt=""> &nbsp; [block](https://github.com/block) / [goose](https://github.com/block/goose) | 237 |
| <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) | 164 |
| <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) | 133 |
| <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) | 122 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/134601687?s=40&v=4" width="20" height="20" alt=""> &nbsp; [langfuse](https://github.com/langfuse) / [langfuse-python](https://github.com/langfuse/langfuse-python) | 108 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/42655961?s=40&v=4" width="20" height="20" alt=""> &nbsp; [zozoheir](https://github.com/zozoheir) / [tinyllm](https://github.com/zozoheir/tinyllm) | 95 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/60330232?s=40&v=4" width="20" height="20" alt=""> &nbsp; [PostHog](https://github.com/PostHog) / [posthog-foss](https://github.com/PostHog/posthog-foss) | 305 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/154247157?s=40&v=4" width="20" height="20" alt=""> &nbsp; [vespperhq](https://github.com/vespperhq) / [vespper](https://github.com/vespperhq/vespper) | 304 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/185116535?s=40&v=4" width="20" height="20" alt=""> &nbsp; [block](https://github.com/block) / [goose](https://github.com/block/goose) | 295 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/609489?s=40&v=4" width="20" height="20" alt=""> &nbsp; [aorwall](https://github.com/aorwall) / [moatless-tools](https://github.com/aorwall/moatless-tools) | 291 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/2357342?s=40&v=4" width="20" height="20" alt=""> &nbsp; [dmayboroda](https://github.com/dmayboroda) / [minima](https://github.com/dmayboroda/minima) | 221 |
| <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) | 172 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/148684274?s=40&v=4" width="20" height="20" alt=""> &nbsp; [i-am-alice](https://github.com/i-am-alice) / [3rd-devs](https://github.com/i-am-alice/3rd-devs) | 148 |
| <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 |
+9 -3
View File
@@ -1,13 +1,19 @@
_We are Hiring_
Join us in scaling Langfuse in Berlin, Germany. We are an open source company, we hire in person, we are only hiring technical talent.
Join us in building out Langfuse in Berlin, Germany. Langfuse is the open source LLM engineering platform: we build tooling to help developers [build & improve LLM applications](https://langfuse.com/docs).
We are an open source company, we hire in person (4+ days a week), we only hire excellent technical talent. Find more information on our [careers page](https://langfuse.com/careers)
_Open Roles_
- Product Engineer, 70-130k EUR, 0.25-0.75% Equity, https://www.ycombinator.com/companies/langfuse/jobs/aAvmoFB-product-engineer
- Developer Advocate, 60-110k EUR, 0.25-0.5% Equity, https://www.ycombinator.com/companies/langfuse/jobs/uHysbKH-developer-advocate-devrel
- Product Engineer, 70-130k EUR, 0.1-0.35% Equity, https://www.ycombinator.com/companies/langfuse/jobs/aAvmoFB-product-engineer
- Backend Engineer, 70-130k EUR, 0.1-0.35% Equity, https://www.ycombinator.com/companies/langfuse/jobs/1bO16H6-backend-engineer
- Design Engineer, 70-130k EUR, 0.1-0.35% Equity, https://www.ycombinator.com/companies/langfuse/jobs/mDquP95-design-engineer
- Developer Advocate, 70-130k EUR, 0.1-0.35% Equity, https://www.ycombinator.com/companies/langfuse/jobs/uHysbKH-developer-advocate-devrel
_More Info_
- https://langfuse.com/careers
- https://langfuse.com/docs
- https://langfuse.com/why
- https://langfuse.com/changelog
-1
View File
@@ -155,4 +155,3 @@ volumes:
driver: local
langfuse_minio_data:
driver: local
+2 -4
View File
@@ -38,6 +38,8 @@ services:
LANGFUSE_S3_MEDIA_UPLOAD_ENDPOINT: ${LANGFUSE_S3_MEDIA_UPLOAD_ENDPOINT:-http://minio:9000}
LANGFUSE_S3_MEDIA_UPLOAD_FORCE_PATH_STYLE: ${LANGFUSE_S3_MEDIA_UPLOAD_FORCE_PATH_STYLE:-true}
LANGFUSE_S3_MEDIA_UPLOAD_PREFIX: ${LANGFUSE_S3_MEDIA_UPLOAD_PREFIX:-media/}
LANGFUSE_INGESTION_QUEUE_DELAY_MS: ${LANGFUSE_INGESTION_QUEUE_DELAY_MS:-}
LANGFUSE_INGESTION_CLICKHOUSE_WRITE_INTERVAL_MS: ${LANGFUSE_INGESTION_CLICKHOUSE_WRITE_INTERVAL_MS:-}
REDIS_HOST: ${REDIS_HOST:-redis}
REDIS_PORT: ${REDIS_PORT:-6379}
REDIS_AUTH: ${REDIS_AUTH:-myredissecret}
@@ -61,10 +63,6 @@ services:
LANGFUSE_INIT_USER_EMAIL: ${LANGFUSE_INIT_USER_EMAIL:-}
LANGFUSE_INIT_USER_NAME: ${LANGFUSE_INIT_USER_NAME:-}
LANGFUSE_INIT_USER_PASSWORD: ${LANGFUSE_INIT_USER_PASSWORD:-}
LANGFUSE_SDK_CI_SYNC_PROCESSING_ENABLED: ${LANGFUSE_SDK_CI_SYNC_PROCESSING_ENABLED:-false}
LANGFUSE_READ_FROM_POSTGRES_ONLY: ${LANGFUSE_READ_FROM_POSTGRES_ONLY:-false}
LANGFUSE_READ_FROM_CLICKHOUSE_ONLY: ${LANGFUSE_READ_FROM_CLICKHOUSE_ONLY:-true}
LANGFUSE_RETURN_FROM_CLICKHOUSE: ${LANGFUSE_RETURN_FROM_CLICKHOUSE:-true}
clickhouse:
image: clickhouse/clickhouse-server
+3 -2
View File
@@ -27,7 +27,7 @@
"@langfuse/shared": "workspace:*",
"@opentelemetry/api": ">=1.0.0 <1.10.0",
"axios": "^1.7.7",
"next": "^14.2.15",
"next": "^14.2.21",
"next-auth": "^4.24.11",
"zod": "^3.23.8"
},
@@ -47,7 +47,8 @@
},
"pnpm": {
"overrides": {
"jsonpath-plus": "10.0.7"
"jsonpath-plus": "10.0.7",
"nanoid": "^3.3.8"
}
}
}
+2 -2
View File
@@ -378,10 +378,10 @@ types:
type: string
startDate:
docs: Apply only to generations which are newer than this ISO date.
type: optional<date>
type: optional<datetime>
unit:
docs: Unit used by this model.
type: ModelUsageUnit
type: optional<ModelUsageUnit>
inputPrice:
docs: Price (USD) per input unit
type: optional<double>
@@ -11,6 +11,7 @@ service:
Batched ingestion for Langfuse Tracing. If you want to use tracing via the API, such as to build your own Langfuse client implementation, this is the only API route you need to implement.
Notes:
- Introduction to data model: https://langfuse.com/docs/tracing-data-model
- Batch sizes are limited to 3.5 MB in total. You need to adjust the number of events per batch accordingly.
- The API does not return a 4xx status code for input errors. Instead, it responds with a 207 status code, which includes a list of the encountered errors.
method: POST
+1 -1
View File
@@ -58,7 +58,7 @@ types:
type: optional<datetime>
unit:
docs: Unit used by this model.
type: commons.ModelUsageUnit
type: optional<commons.ModelUsageUnit>
inputPrice:
docs: Price (USD) per input unit
type: optional<double>
+3 -2
View File
@@ -1,4 +1,5 @@
{
"organization": "finto",
"organization": "langfuse",
"version": "0.43.7"
}
}
+3 -2
View File
@@ -1,6 +1,6 @@
{
"name": "langfuse",
"version": "3.2.0",
"version": "3.9.0",
"author": "engineering@langfuse.com",
"license": "MIT",
"private": true,
@@ -84,7 +84,8 @@
"packageManager": "pnpm@9.5.0",
"pnpm": {
"overrides": {
"jsonpath-plus": "10.0.7"
"jsonpath-plus": "10.0.7",
"nanoid": "^3.3.8"
}
}
}
+14 -4
View File
@@ -18,21 +18,31 @@ then
exit 1
fi
# Ensure CLICKHOUSE_DB is set
if [ -z "${CLICKHOUSE_DB}" ]; then
export CLICKHOUSE_DB="default"
fi
# Ensure CLICKHOUSE_CLUSTER_NAME is set
if [ -z "${CLICKHOUSE_CLUSTER_NAME}" ]; then
export CLICKHOUSE_CLUSTER_NAME="default"
fi
# Construct the database URL
if [ "$CLICKHOUSE_CLUSTER_ENABLED" == "false" ] ; then
if [ "$CLICKHOUSE_MIGRATION_SSL" = true ] ; then
DATABASE_URL="${CLICKHOUSE_MIGRATION_URL}?username=${CLICKHOUSE_USER}&password=${CLICKHOUSE_PASSWORD}&database=default&x-multi-statement=true&secure=true&skip_verify=true&x-migrations-table-engine=MergeTree"
DATABASE_URL="${CLICKHOUSE_MIGRATION_URL}?username=${CLICKHOUSE_USER}&password=${CLICKHOUSE_PASSWORD}&database=${CLICKHOUSE_DB}&x-multi-statement=true&secure=true&skip_verify=true&x-migrations-table-engine=MergeTree"
else
DATABASE_URL="${CLICKHOUSE_MIGRATION_URL}?username=${CLICKHOUSE_USER}&password=${CLICKHOUSE_PASSWORD}&database=default&x-multi-statement=true&x-migrations-table-engine=MergeTree"
DATABASE_URL="${CLICKHOUSE_MIGRATION_URL}?username=${CLICKHOUSE_USER}&password=${CLICKHOUSE_PASSWORD}&database=${CLICKHOUSE_DB}&x-multi-statement=true&x-migrations-table-engine=MergeTree"
fi
# Execute the up command
migrate -source file://clickhouse/migrations/unclustered -database "$DATABASE_URL" down
else
if [ "$CLICKHOUSE_MIGRATION_SSL" = true ] ; then
DATABASE_URL="${CLICKHOUSE_MIGRATION_URL}?username=${CLICKHOUSE_USER}&password=${CLICKHOUSE_PASSWORD}&database=default&x-multi-statement=true&secure=true&skip_verify=true&x-cluster-name=default&x-migrations-table-engine=ReplicatedMergeTree"
DATABASE_URL="${CLICKHOUSE_MIGRATION_URL}?username=${CLICKHOUSE_USER}&password=${CLICKHOUSE_PASSWORD}&database=${CLICKHOUSE_DB}&x-multi-statement=true&secure=true&skip_verify=true&x-cluster-name=${CLICKHOUSE_CLUSTER_NAME}&x-migrations-table-engine=ReplicatedMergeTree"
else
DATABASE_URL="${CLICKHOUSE_MIGRATION_URL}?username=${CLICKHOUSE_USER}&password=${CLICKHOUSE_PASSWORD}&database=default&x-multi-statement=true&x-cluster-name=default&x-migrations-table-engine=ReplicatedMergeTree"
DATABASE_URL="${CLICKHOUSE_MIGRATION_URL}?username=${CLICKHOUSE_USER}&password=${CLICKHOUSE_PASSWORD}&database=${CLICKHOUSE_DB}&x-multi-statement=true&x-cluster-name=${CLICKHOUSE_CLUSTER_NAME}&x-migrations-table-engine=ReplicatedMergeTree"
fi
# Execute the up command
+7 -2
View File
@@ -12,11 +12,16 @@ then
exit 1
fi
# Ensure CLICKHOUSE_DB is set
if [ -z "${CLICKHOUSE_DB}" ]; then
export CLICKHOUSE_DB="default"
fi
# Construct the database URL
if [ "$CLICKHOUSE_MIGRATION_SSL" = true ] ; then
DATABASE_URL="${CLICKHOUSE_MIGRATION_URL}?username=${CLICKHOUSE_USER}&password=${CLICKHOUSE_PASSWORD}&database=default&x-multi-statement=true&secure=true&skip_verify=true&x-migrations-table-engine=MergeTree"
DATABASE_URL="${CLICKHOUSE_MIGRATION_URL}?username=${CLICKHOUSE_USER}&password=${CLICKHOUSE_PASSWORD}&database=${CLICKHOUSE_DB}&x-multi-statement=true&secure=true&skip_verify=true&x-migrations-table-engine=MergeTree"
else
DATABASE_URL="${CLICKHOUSE_MIGRATION_URL}?username=${CLICKHOUSE_USER}&password=${CLICKHOUSE_PASSWORD}&database=default&x-multi-statement=true&x-migrations-table-engine=MergeTree"
DATABASE_URL="${CLICKHOUSE_MIGRATION_URL}?username=${CLICKHOUSE_USER}&password=${CLICKHOUSE_PASSWORD}&database=${CLICKHOUSE_DB}&x-multi-statement=true&x-migrations-table-engine=MergeTree"
fi
# Execute the drop command
migrate -source file://clickhouse/migrations -database "$DATABASE_URL" drop
+14 -4
View File
@@ -18,21 +18,31 @@ then
exit 1
fi
# Ensure CLICKHOUSE_DB is set
if [ -z "${CLICKHOUSE_DB}" ]; then
export CLICKHOUSE_DB="default"
fi
# Ensure CLICKHOUSE_CLUSTER_NAME is set
if [ -z "${CLICKHOUSE_CLUSTER_NAME}" ]; then
export CLICKHOUSE_CLUSTER_NAME="default"
fi
# Construct the database URL
if [ "$CLICKHOUSE_CLUSTER_ENABLED" == "false" ] ; then
if [ "$CLICKHOUSE_MIGRATION_SSL" = true ] ; then
DATABASE_URL="${CLICKHOUSE_MIGRATION_URL}?username=${CLICKHOUSE_USER}&password=${CLICKHOUSE_PASSWORD}&database=default&x-multi-statement=true&secure=true&skip_verify=true&x-migrations-table-engine=MergeTree"
DATABASE_URL="${CLICKHOUSE_MIGRATION_URL}?username=${CLICKHOUSE_USER}&password=${CLICKHOUSE_PASSWORD}&database=${CLICKHOUSE_DB}&x-multi-statement=true&secure=true&skip_verify=true&x-migrations-table-engine=MergeTree"
else
DATABASE_URL="${CLICKHOUSE_MIGRATION_URL}?username=${CLICKHOUSE_USER}&password=${CLICKHOUSE_PASSWORD}&database=default&x-multi-statement=true&x-migrations-table-engine=MergeTree"
DATABASE_URL="${CLICKHOUSE_MIGRATION_URL}?username=${CLICKHOUSE_USER}&password=${CLICKHOUSE_PASSWORD}&database=${CLICKHOUSE_DB}&x-multi-statement=true&x-migrations-table-engine=MergeTree"
fi
# Execute the up command
migrate -source file://clickhouse/migrations/unclustered -database "$DATABASE_URL" up
else
if [ "$CLICKHOUSE_MIGRATION_SSL" = true ] ; then
DATABASE_URL="${CLICKHOUSE_MIGRATION_URL}?username=${CLICKHOUSE_USER}&password=${CLICKHOUSE_PASSWORD}&database=default&x-multi-statement=true&secure=true&skip_verify=true&x-cluster-name=default&x-migrations-table-engine=ReplicatedMergeTree"
DATABASE_URL="${CLICKHOUSE_MIGRATION_URL}?username=${CLICKHOUSE_USER}&password=${CLICKHOUSE_PASSWORD}&database=${CLICKHOUSE_DB}&x-multi-statement=true&secure=true&skip_verify=true&x-cluster-name=${CLICKHOUSE_CLUSTER_NAME}&x-migrations-table-engine=ReplicatedMergeTree"
else
DATABASE_URL="${CLICKHOUSE_MIGRATION_URL}?username=${CLICKHOUSE_USER}&password=${CLICKHOUSE_PASSWORD}&database=default&x-multi-statement=true&x-cluster-name=default&x-migrations-table-engine=ReplicatedMergeTree"
DATABASE_URL="${CLICKHOUSE_MIGRATION_URL}?username=${CLICKHOUSE_USER}&password=${CLICKHOUSE_PASSWORD}&database=${CLICKHOUSE_DB}&x-multi-statement=true&x-cluster-name=${CLICKHOUSE_CLUSTER_NAME}&x-migrations-table-engine=ReplicatedMergeTree"
fi
# Execute the up command
+3 -2
View File
@@ -73,7 +73,7 @@
"@types/bcryptjs": "^2.4.6",
"axios": "^1.7.7",
"bcryptjs": "^2.4.3",
"bullmq": "^5.12.10",
"bullmq": "^5.34.10",
"dd-trace": "^5.23.1",
"decimal.js": "^10.4.3",
"exponential-backoff": "^3.1.1",
@@ -121,7 +121,8 @@
},
"pnpm": {
"overrides": {
"jsonpath-plus": "10.0.7"
"jsonpath-plus": "10.0.7",
"nanoid": "^3.3.8"
}
}
}
+20
View File
@@ -170,6 +170,17 @@ export type BatchExport = {
url: string | null;
log: string | null;
};
export type BillingMeterBackup = {
stripe_customer_id: string;
meter_id: string;
start_time: Timestamp;
end_time: Timestamp;
aggregated_value: number;
event_name: string;
org_id: string;
created_at: Generated<Timestamp>;
updated_at: Generated<Timestamp>;
};
export type Comment = {
id: string;
project_id: string;
@@ -479,6 +490,13 @@ export type Prompt = {
tags: Generated<string[]>;
labels: Generated<string[]>;
};
export type QueueBackUp = {
id: string;
project_id: string | null;
queue_name: string;
content: unknown;
created_at: Generated<Timestamp>;
};
export type Score = {
id: string;
timestamp: Generated<Timestamp>;
@@ -604,6 +622,7 @@ export type DB = {
audit_logs: AuditLog;
background_migrations: BackgroundMigration;
batch_exports: BatchExport;
billing_meter_backups: BillingMeterBackup;
comments: Comment;
cron_jobs: CronJobs;
dataset_items: DatasetItem;
@@ -628,6 +647,7 @@ export type DB = {
project_memberships: ProjectMembership;
projects: Project;
prompts: Prompt;
queue_backups: QueueBackUp;
score_configs: ScoreConfig;
scores: Score;
Session: Session;
@@ -0,0 +1,13 @@
-- CreateTable
CREATE TABLE "queue_backups" (
"id" TEXT NOT NULL,
"project_id" TEXT,
"queue_name" TEXT NOT NULL,
"content" JSONB NOT NULL,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "queue_backups_pkey" PRIMARY KEY ("id")
);
-- AddForeignKey
ALTER TABLE "traces" ADD CONSTRAINT "traces_session_id_project_id_fkey" FOREIGN KEY ("session_id", "project_id") REFERENCES "trace_sessions"("id", "project_id") ON DELETE RESTRICT ON UPDATE CASCADE;
@@ -0,0 +1,2 @@
-- DropForeignKey
ALTER TABLE "traces" DROP CONSTRAINT "traces_session_id_project_id_fkey";
@@ -0,0 +1,21 @@
-- CreateTable
CREATE TABLE "billing_meter_backups" (
"stripe_customer_id" TEXT NOT NULL,
"meter_id" TEXT NOT NULL,
"start_time" TIMESTAMP(3) NOT NULL,
"end_time" TIMESTAMP(3) NOT NULL,
"aggregated_value" INTEGER NOT NULL,
"event_name" TEXT NOT NULL,
"org_id" TEXT NOT NULL,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP
);
-- CreateIndex
CREATE INDEX "billing_meter_backups_stripe_customer_id_meter_id_start_tim_idx" ON "billing_meter_backups"("stripe_customer_id", "meter_id", "start_time", "end_time");
-- CreateIndex
CREATE UNIQUE INDEX "billing_meter_backups_stripe_customer_id_meter_id_start_tim_key" ON "billing_meter_backups"("stripe_customer_id", "meter_id", "start_time", "end_time");
+46 -14
View File
@@ -272,7 +272,6 @@ model TraceSession {
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
bookmarked Boolean @default(false)
public Boolean @default(false)
traces Trace[]
@@id([id, projectId])
@@index([projectId])
@@ -284,25 +283,24 @@ model TraceSession {
// Update TraceView below when making changes to this model!
model Trace {
id String @id @default(cuid())
externalId String? @map("external_id")
timestamp DateTime @default(now())
id String @id @default(cuid())
externalId String? @map("external_id")
timestamp DateTime @default(now())
name String?
userId String? @map("user_id")
userId String? @map("user_id")
metadata Json?
release String?
version String?
projectId String @map("project_id")
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
public Boolean @default(false)
bookmarked Boolean @default(false)
tags String[] @default([])
projectId String @map("project_id")
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
public Boolean @default(false)
bookmarked Boolean @default(false)
tags String[] @default([])
input Json?
output Json?
sessionId String? @map("session_id")
session TraceSession? @relation(fields: [sessionId, projectId], references: [id, projectId])
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
sessionId String? @map("session_id")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
@@index([projectId, timestamp])
@@index([sessionId])
@@ -1017,3 +1015,37 @@ model ObservationMedia {
@@index([projectId, observationId])
@@map("observation_media")
}
model QueueBackUp {
id String @id @default(cuid())
projectId String? @map("project_id")
queueName String @map("queue_name")
content Json
createdAt DateTime @default(now()) @map("created_at")
@@map("queue_backups")
}
model BillingMeterBackup {
// unique
stripeCustomerId String @map("stripe_customer_id")
meterId String @map("meter_id")
startTime DateTime @map("start_time")
endTime DateTime @map("end_time")
// value
aggregatedValue Int @map("aggregated_value")
// labels
eventName String @map("event_name")
orgId String @map("org_id")
// ts
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
@@unique([stripeCustomerId, meterId, startTime, endTime])
@@index([stripeCustomerId, meterId, startTime, endTime])
@@map("billing_meter_backups")
}
+9 -9
View File
@@ -29,17 +29,10 @@ const EnvSchema = z.object({
LANGFUSE_CACHE_PROMPT_ENABLED: z.enum(["true", "false"]).default("false"),
LANGFUSE_CACHE_PROMPT_TTL_SECONDS: z.coerce.number().default(60 * 60),
CLICKHOUSE_URL: z.string().url(),
CLICKHOUSE_CLUSTER_NAME: z.string().default("default"),
CLICKHOUSE_DB: z.string().default("default"),
CLICKHOUSE_USER: z.string(),
CLICKHOUSE_PASSWORD: z.string(),
LANGFUSE_SDK_CI_SYNC_PROCESSING_ENABLED: z
.enum(["true", "false"])
.default("false"),
LANGFUSE_CLICKHOUSE_INGESTION_ENABLED: z
.enum(["true", "false"])
.default("true"),
LANGFUSE_POSTGRES_INGESTION_ENABLED: z
.enum(["true", "false"])
.default("false"),
LANGFUSE_INGESTION_QUEUE_DELAY_MS: z.coerce
.number()
@@ -66,6 +59,13 @@ const EnvSchema = z.object({
.default("false"),
LANGFUSE_USE_AZURE_BLOB: z.enum(["true", "false"]).default("false"),
STRIPE_SECRET_KEY: z.string().optional(),
LANGFUSE_S3_CORE_DATA_EXPORT_IS_ENABLED: z
.enum(["true", "false"])
.default("false"),
LANGFUSE_POSTGRES_METERING_DATA_EXPORT_IS_ENABLED: z
.enum(["true", "false"])
.default("false"),
});
export const env: z.infer<typeof EnvSchema> =
+2 -2
View File
@@ -1,7 +1,7 @@
import { BaseError } from "./BaseError";
export class ApiError extends BaseError {
constructor(description = "Api call failed") {
super("ApiError", 500, description, true);
constructor(description = "Api call failed", status = 500) {
super("ApiError", status, description, true);
}
}
+13 -1
View File
@@ -112,6 +112,14 @@ export const observationsTableCols: ColumnDefinition[] = [
options: [], // to be added at runtime
nullable: true,
},
{
name: "Model ID",
id: "modelId",
type: "stringOptions",
internal: 'o."internal_model_id"',
options: [], // to be added at runtime
nullable: true,
},
{
name: "Input Tokens",
id: "inputTokens",
@@ -186,6 +194,7 @@ export const observationsTableCols: ColumnDefinition[] = [
// allows for undefined options, to offer filters while options are still loading
export type ObservationOptions = {
model: Array<OptionsDefinition>;
modelId: Array<OptionsDefinition>;
name: Array<OptionsDefinition>;
traceName: Array<OptionsDefinition>;
scores_avg: Array<string>;
@@ -194,12 +203,15 @@ export type ObservationOptions = {
};
export function observationsTableColsWithOptions(
options?: ObservationOptions
options?: ObservationOptions,
): ColumnDefinition[] {
return observationsTableCols.map((col) => {
if (col.id === "model") {
return { ...col, options: options?.model ?? [] };
}
if (col.id === "modelId") {
return { ...col, options: options?.modelId ?? [] };
}
if (col.id === "name") {
return { ...col, options: options?.name ?? [] };
}
@@ -10,7 +10,7 @@ export const clickhouseClient = (opts?: NodeClickHouseClientConfigOptions) =>
url: env.CLICKHOUSE_URL,
username: env.CLICKHOUSE_USER,
password: env.CLICKHOUSE_PASSWORD,
database: "default",
database: env.CLICKHOUSE_DB,
clickhouse_settings: {
async_insert: 1,
wait_for_async_insert: 1, // if disabled, we won't get errors from clickhouse
+3
View File
@@ -32,6 +32,8 @@ export * from "./redis/legacyIngestion";
export * from "./redis/ingestionQueue";
export * from "./redis/postHogIntegrationQueue";
export * from "./redis/postHogIntegrationProcessingQueue";
export * from "./redis/coreDataS3ExportQueue";
export * from "./redis/meteringDataPostgresExportQueue";
export * from "./redis/experimentCreateQueue";
export * from "./auth/types";
export * from "./ingestion/legacy/index";
@@ -44,6 +46,7 @@ export * from "./logger";
export * from "./queries";
export * from "./repositories";
export * from "./redis/evalExecutionQueue";
export * from "./services/sessions-ui-table-service";
// test utils
export * from "./test-utils";
@@ -394,7 +394,7 @@ export class ObservationProcessor implements EventProcessor {
model?: Model,
existingObservation?: Omit<Observation, "input" | "output">,
) {
let newPromptTokens = body.usage?.input || body.usageDetails?.input;
let newPromptTokens = body.usage?.input ?? body.usageDetails?.input;
if (newPromptTokens === undefined && model && model.tokenizerId) {
if (body.input) {
newPromptTokens = calculateTokenDelegate({
@@ -419,7 +419,7 @@ export class ObservationProcessor implements EventProcessor {
}
}
let newCompletionTokens = body.usage?.output || body.usageDetails?.output;
let newCompletionTokens = body.usage?.output ?? body.usageDetails?.output;
if (newCompletionTokens === undefined && model && model.tokenizerId) {
if (body.output) {
@@ -18,11 +18,9 @@ import {
traceException,
} from "../instrumentation";
import { logger } from "../logger";
import { LegacyIngestionEventType, QueueJobs } from "../queues";
import { QueueJobs } from "../queues";
import { IngestionQueue } from "../redis/ingestionQueue";
import { LegacyIngestionQueue } from "../redis/legacyIngestion";
import { redis } from "../redis/redis";
import { handleBatch } from "./legacy";
import {
StorageService,
StorageServiceFactory,
@@ -185,140 +183,47 @@ export const processEventBatch = async (
});
});
// This is a workaround to allow us to disable async ingestion processing for SDK CI testing
// TODO: remove this block after SDKs are ready for V3 async ingestion processing
if (env.LANGFUSE_SDK_CI_SYNC_PROCESSING_ENABLED === "true") {
const result = await handleBatch(
sortedBatch,
authCheck,
tokenCountDelegate,
);
// in case we did not return early, we return the result here
return aggregateBatchResult(
[...validationErrors, ...authenticationErrors, ...result.errors],
result.results,
authCheck.scope.projectId,
);
}
// Send each event individually to IngestionQueue for ClickHouse processing
if (env.LANGFUSE_CLICKHOUSE_INGESTION_ENABLED === "true") {
if (s3UploadErrored) {
throw new Error(
"Failed to upload events to blob storage, aborting event processing",
);
}
if (redis) {
const queue = IngestionQueue.getInstance();
await Promise.all(
Object.keys(sortedBatchByEventBodyId).map(async (id) =>
queue
? queue.add(
QueueJobs.IngestionJob,
{
id: randomUUID(),
timestamp: new Date(),
name: QueueJobs.IngestionJob as const,
payload: {
data: {
type: sortedBatchByEventBodyId[id].type,
eventBodyId: sortedBatchByEventBodyId[id].eventBodyId,
},
authCheck,
},
},
{
delay: env.LANGFUSE_INGESTION_QUEUE_DELAY_MS,
},
)
: Promise.reject("Failed to instantiate queue"),
),
);
if (env.LANGFUSE_POSTGRES_INGESTION_ENABLED !== "true") {
// If postgres ingestion is disabled, we return early
return aggregateBatchResult(
[...validationErrors, ...authenticationErrors],
sortedBatch.map((event) => ({ id: event.id, result: event })),
authCheck.scope.projectId,
);
}
}
}
if (env.LANGFUSE_POSTGRES_INGESTION_ENABLED === "true") {
// As part of the legacy processing we sent the entire batch to the worker.
if (redis) {
const queue = LegacyIngestionQueue.getInstance();
if (queue) {
let addToQueueFailed = false;
const queuePayload: LegacyIngestionEventType = !s3UploadErrored
? {
data: Object.keys(sortedBatchByEventBodyId).map((id) => {
const { key, type, eventBodyId } = sortedBatchByEventBodyId[id];
return {
type,
eventBodyId,
eventId: key,
};
}),
authCheck,
useS3EventStore: true,
}
: { data: sortedBatch, authCheck, useS3EventStore: false };
try {
await queue.add(QueueJobs.LegacyIngestionJob, {
payload: queuePayload,
id: randomUUID(),
timestamp: new Date(),
name: QueueJobs.LegacyIngestionJob as const,
});
} catch (e: unknown) {
logger.warn(
"Failed to add batch to queue, falling back to sync processing",
e,
);
addToQueueFailed = true;
}
if (!addToQueueFailed) {
return aggregateBatchResult(
// we are not sending additional server errors to the client in case of early return
[...validationErrors, ...authenticationErrors],
sortedBatch.map((event) => ({ id: event.id, result: event })),
authCheck.scope.projectId,
);
}
} else {
logger.warn(
"Ingestion queue not initialized, falling back to sync processing",
);
}
}
/*******************
* SYNC PROCESSING *
*******************/
const result = await handleBatch(
sortedBatch,
authCheck,
tokenCountDelegate,
);
// in case we did not return early, we return the result here
return aggregateBatchResult(
[...validationErrors, ...authenticationErrors, ...result.errors],
result.results,
authCheck.scope.projectId,
if (s3UploadErrored) {
throw new Error(
"Failed to upload events to blob storage, aborting event processing",
);
}
throw new Error(
"Either Clickhouse or Postgres ingestion (or both) must be enabled",
if (!redis) {
throw new Error("Redis not initialized, aborting event processing");
}
const queue = IngestionQueue.getInstance();
await Promise.all(
Object.keys(sortedBatchByEventBodyId).map(async (id) =>
queue
? queue.add(
QueueJobs.IngestionJob,
{
id: randomUUID(),
timestamp: new Date(),
name: QueueJobs.IngestionJob as const,
payload: {
data: {
type: sortedBatchByEventBodyId[id].type,
eventBodyId: sortedBatchByEventBodyId[id].eventBodyId,
},
authCheck,
},
},
{
delay: env.LANGFUSE_INGESTION_QUEUE_DELAY_MS,
},
)
: Promise.reject("Failed to instantiate queue"),
),
);
return aggregateBatchResult(
[...validationErrors, ...authenticationErrors],
sortedBatch.map((event) => ({ id: event.id, result: event })),
authCheck.scope.projectId,
);
};
@@ -112,13 +112,16 @@ export const UsageOrCostDetails = z
.union([OpenAIUsageSchema, RawUsageOrCostDetails])
.nullish();
// Using z.any instead of jsonSchema for input/output as we saw huge CPU overhead for large numeric arrays.
// With this setup parsing should be more lightweight and doesn't block other requests.
// As we allow plain values, arrays, and objects the JSON parse via bodyParser should suffice.
export const TraceBody = z.object({
id: z.string().nullish(),
timestamp: stringDateTime,
name: z.string().max(1000).nullish(),
externalId: z.string().nullish(),
input: jsonSchema.nullish(),
output: jsonSchema.nullish(),
input: z.any().nullish(),
output: z.any().nullish(),
sessionId: z.string().nullish(),
userId: z.string().nullish(),
metadata: jsonSchema.nullish(),
@@ -133,8 +136,8 @@ export const OptionalObservationBody = z.object({
name: z.string().nullish(),
startTime: stringDateTime,
metadata: jsonSchema.nullish(),
input: jsonSchema.nullish(),
output: jsonSchema.nullish(),
input: z.any().nullish(),
output: z.any().nullish(),
level: z.nativeEnum(ObservationLevel).nullish(),
statusMessage: z.string().nullish(),
parentObservationId: z.string().nullish(),
+12
View File
@@ -112,11 +112,14 @@ export enum QueueName {
DatasetRunItemUpsert = "dataset-run-item-upsert-queue",
BatchExport = "batch-export-queue",
IngestionQueue = "ingestion-queue", // Process single events with S3-merge
IngestionSecondaryQueue = "secondary-ingestion-queue", // Separates high priority + high throughput projects from other projects.
LegacyIngestionQueue = "legacy-ingestion-queue", // Used for batch processing of Ingestion
CloudUsageMeteringQueue = "cloud-usage-metering-queue",
ExperimentCreate = "experiment-create-queue",
PostHogIntegrationQueue = "posthog-integration-queue",
PostHogIntegrationProcessingQueue = "posthog-integration-processing-queue",
CoreDataS3ExportQueue = "core-data-s3-export-queue",
MeteringDataPostgresExportQueue = "metering-data-postgres-export-queue",
}
export enum QueueJobs {
@@ -129,9 +132,12 @@ export enum QueueJobs {
LegacyIngestionJob = "legacy-ingestion-job",
CloudUsageMeteringJob = "cloud-usage-metering-job",
IngestionJob = "ingestion-job",
IngestionSecondaryJob = "secondary-ingestion-job",
ExperimentCreateJob = "experiment-create-job",
PostHogIntegrationJob = "posthog-integration-job",
PostHogIntegrationProcessingJob = "posthog-integration-processing-job",
CoreDataS3ExportJob = "core-data-s3-export-job",
MeteringDataPostgresExportJob = "metering-data-postgres-export-job",
}
export type TQueueJobTypes = {
@@ -183,6 +189,12 @@ export type TQueueJobTypes = {
payload: IngestionEventQueueType;
name: QueueJobs.IngestionJob;
};
[QueueName.IngestionSecondaryQueue]: {
timestamp: Date;
id: string;
payload: IngestionEventQueueType;
name: QueueJobs.IngestionJob;
};
[QueueName.ExperimentCreate]: {
timestamp: Date;
id: string;
@@ -45,6 +45,7 @@ export class CloudUsageMeteringQueue {
QueueJobs.CloudUsageMeteringJob,
{},
{
// Run at minute 5 of every hour (e.g. 1:05, 2:05, 3:05, etc)
repeat: { pattern: "5 * * * *" },
},
);
@@ -0,0 +1,60 @@
import { Queue } from "bullmq";
import { QueueName, QueueJobs } from "../queues";
import { createNewRedisInstance, redisQueueRetryOptions } from "./redis";
import { logger } from "../logger";
import { env } from "../../env";
export class CoreDataS3ExportQueue {
private static instance: Queue | null = null;
public static getInstance(): Queue | null {
if (env.LANGFUSE_S3_CORE_DATA_EXPORT_IS_ENABLED !== "true") {
return null;
}
if (CoreDataS3ExportQueue.instance) {
return CoreDataS3ExportQueue.instance;
}
const newRedis = createNewRedisInstance({
enableOfflineQueue: false,
...redisQueueRetryOptions,
});
CoreDataS3ExportQueue.instance = newRedis
? new Queue(QueueName.CoreDataS3ExportQueue, {
connection: newRedis,
defaultJobOptions: {
removeOnComplete: true,
removeOnFail: 100,
attempts: 5,
backoff: {
type: "exponential",
delay: 5000,
},
},
})
: null;
CoreDataS3ExportQueue.instance?.on("error", (err) => {
logger.error("CoreDataS3ExportQueue error", err);
});
if (CoreDataS3ExportQueue.instance) {
logger.debug("Scheduling jobs for CoreDataS3ExportQueue");
CoreDataS3ExportQueue.instance
.add(
QueueJobs.CoreDataS3ExportJob,
{},
{
repeat: { pattern: "30 4 * * *" }, // every day at 4:30am
},
)
.catch((err) => {
logger.error("Error adding CoreDataS3ExportJob schedule", err);
});
}
return CoreDataS3ExportQueue.instance;
}
}
+9 -1
View File
@@ -5,13 +5,15 @@ import { CloudUsageMeteringQueue } from "./cloudUsageMeteringQueue";
import { DatasetRunItemUpsertQueue } from "./datasetRunItemUpsert";
import { EvalExecutionQueue } from "./evalExecutionQueue";
import { ExperimentCreateQueue } from "./experimentCreateQueue";
import { IngestionQueue } from "./ingestionQueue";
import { IngestionQueue, SecondaryIngestionQueue } from "./ingestionQueue";
import { LegacyIngestionQueue } from "./legacyIngestion";
import { TraceUpsertQueue } from "./traceUpsert";
import { TraceDeleteQueue } from "./traceDelete";
import { ProjectDeleteQueue } from "./projectDelete";
import { PostHogIntegrationQueue } from "./postHogIntegrationQueue";
import { PostHogIntegrationProcessingQueue } from "./postHogIntegrationProcessingQueue";
import { CoreDataS3ExportQueue } from "./coreDataS3ExportQueue";
import { MeteringDataPostgresExportQueue } from "./meteringDataPostgresExportQueue";
export function getQueue(queueName: QueueName): Queue | null {
switch (queueName) {
@@ -39,6 +41,12 @@ export function getQueue(queueName: QueueName): Queue | null {
return PostHogIntegrationQueue.getInstance();
case QueueName.PostHogIntegrationProcessingQueue:
return PostHogIntegrationProcessingQueue.getInstance();
case QueueName.IngestionSecondaryQueue:
return SecondaryIngestionQueue.getInstance();
case QueueName.CoreDataS3ExportQueue:
return CoreDataS3ExportQueue.getInstance();
case QueueName.MeteringDataPostgresExportQueue:
return MeteringDataPostgresExportQueue.getInstance();
default:
const exhaustiveCheckDefault: never = queueName;
throw new Error(`Queue ${queueName} not found`);
@@ -43,3 +43,45 @@ export class IngestionQueue {
return IngestionQueue.instance;
}
}
export class SecondaryIngestionQueue {
private static instance: Queue<
TQueueJobTypes[QueueName.IngestionSecondaryQueue]
> | null = null;
public static getInstance(): Queue<
TQueueJobTypes[QueueName.IngestionSecondaryQueue]
> | null {
if (SecondaryIngestionQueue.instance)
return SecondaryIngestionQueue.instance;
const newRedis = createNewRedisInstance({
enableOfflineQueue: false,
...redisQueueRetryOptions,
});
SecondaryIngestionQueue.instance = newRedis
? new Queue<TQueueJobTypes[QueueName.IngestionSecondaryQueue]>(
QueueName.IngestionSecondaryQueue,
{
connection: newRedis,
defaultJobOptions: {
removeOnComplete: true,
removeOnFail: 100_000,
attempts: 5,
backoff: {
type: "exponential",
delay: 5000,
},
},
},
)
: null;
SecondaryIngestionQueue.instance?.on("error", (err) => {
logger.error("SecondaryIngestionQueue error", err);
});
return SecondaryIngestionQueue.instance;
}
}
@@ -0,0 +1,64 @@
import { Queue } from "bullmq";
import { QueueName, QueueJobs } from "../queues";
import { createNewRedisInstance, redisQueueRetryOptions } from "./redis";
import { logger } from "../logger";
import { env } from "../../env";
export class MeteringDataPostgresExportQueue {
private static instance: Queue | null = null;
public static getInstance(): Queue | null {
if (env.LANGFUSE_POSTGRES_METERING_DATA_EXPORT_IS_ENABLED !== "true") {
return null;
}
if (MeteringDataPostgresExportQueue.instance) {
return MeteringDataPostgresExportQueue.instance;
}
const newRedis = createNewRedisInstance({
enableOfflineQueue: false,
...redisQueueRetryOptions,
});
MeteringDataPostgresExportQueue.instance = newRedis
? new Queue(QueueName.MeteringDataPostgresExportQueue, {
connection: newRedis,
defaultJobOptions: {
removeOnComplete: true,
removeOnFail: 100,
attempts: 5,
backoff: {
type: "exponential",
delay: 5000,
},
},
})
: null;
MeteringDataPostgresExportQueue.instance?.on("error", (err) => {
logger.error("MeteringDataPostgresExportQueue error", err);
});
if (MeteringDataPostgresExportQueue.instance) {
logger.debug("Scheduling jobs for MeteringDataPostgresExportQueue");
MeteringDataPostgresExportQueue.instance
.add(
QueueJobs.MeteringDataPostgresExportJob,
{},
{
// repeat: { pattern: "30 3 * * *" }, // every day at 3:30am UTC
repeat: { pattern: "0 * * * *" }, // initially, run every hour
},
)
.catch((err) => {
logger.error(
"Error adding MeteringDataPostgresExportJob schedule",
err,
);
});
}
return MeteringDataPostgresExportQueue.instance;
}
}
@@ -4,7 +4,7 @@ import {
convertDateToClickhouseDateTime,
} from "../clickhouse/client";
import { logger } from "../logger";
import { instrumentAsync } from "../instrumentation";
import { getTracer, instrumentAsync } from "../instrumentation";
import {
StorageService,
StorageServiceFactory,
@@ -12,6 +12,7 @@ import {
import { randomUUID } from "crypto";
import { getClickhouseEntityType } from "../clickhouse/schemaUtils";
import { NodeClickHouseClientConfigOptions } from "@clickhouse/client/dist/config";
import { context, trace } from "@opentelemetry/api";
let s3StorageServiceClient: StorageService;
@@ -101,6 +102,64 @@ export async function upsertClickhouse<
});
}
export async function* queryClickhouseStream<T>(opts: {
query: string;
params?: Record<string, unknown> | undefined;
clickhouseConfigs?: NodeClickHouseClientConfigOptions;
}): AsyncGenerator<T> {
const tracer = getTracer("clickhouse-query-stream");
const span = tracer.startSpan("clickhouse-query-stream");
try {
const res = await context.with(
trace.setSpan(context.active(), span),
async () => {
// https://opentelemetry.io/docs/specs/semconv/database/database-spans/
span.setAttribute("ch.query.text", opts.query);
const res = await clickhouseClient(opts.clickhouseConfigs).query({
query: opts.query,
format: "JSONEachRow",
query_params: opts.params,
});
// same logic as for prisma. we want to see queries in development
if (env.NODE_ENV === "development") {
logger.info(`clickhouse:query ${res.query_id} ${opts.query}`);
}
span.setAttribute("ch.queryId", res.query_id);
// add summary headers to the span. Helps to tune performance
const summaryHeader = res.response_headers["x-clickhouse-summary"];
if (summaryHeader) {
try {
const summary = Array.isArray(summaryHeader)
? JSON.parse(summaryHeader[0])
: JSON.parse(summaryHeader);
for (const key in summary) {
span.setAttribute(`ch.${key}`, summary[key]);
}
} catch (error) {
logger.debug(
`Failed to parse clickhouse summary header ${summaryHeader}`,
error,
);
}
}
return res;
},
);
for await (const rows of res.stream<T>()) {
for (const row of rows) {
yield row.json();
}
}
} finally {
span.end();
}
}
export async function queryClickhouse<T>(opts: {
query: string;
params?: Record<string, unknown> | undefined;
@@ -418,7 +418,7 @@ export const getObservationLatencies = async (
// Skipping FINAL here, as the quantiles are approximate to begin with.
const query = `
SELECT
quantiles(0.5, 0.9, 0.95, 0.99)(date_diff('milliseconds', o.start_time, o.end_time)) as quantiles,
quantiles(0.5, 0.9, 0.95, 0.99)(date_diff('millisecond', o.start_time, o.end_time)) as quantiles,
name
FROM observations o
${chFilter.find((f) => f.clickhouseTable === "traces") ? "LEFT JOIN traces t ON o.trace_id = t.id AND o.project_id = t.project_id" : ""}
@@ -465,7 +465,7 @@ export const getTracesLatencies = async (
select o.trace_id,
t.name,
o.project_id,
date_diff('milliseconds', min(o.start_time), coalesce(max(o.end_time), max(o.start_time))) as duration
date_diff('millisecond', min(o.start_time), coalesce(max(o.end_time), max(o.start_time))) as duration
FROM traces t
JOIN observations o
ON o.trace_id = t.id AND o.project_id = t.project_id
@@ -521,7 +521,7 @@ export const getModelLatenciesOverTime = async (
SELECT
${selectTimeseriesColumn(groupBy, "o.start_time", "start_time_bucket")},
provided_model_name,
quantiles(0.5, 0.75, 0.9, 0.95, 0.99)(date_diff('milliseconds', o.start_time, o.end_time)) as quantiles
quantiles(0.5, 0.75, 0.9, 0.95, 0.99)(date_diff('millisecond', o.start_time, o.end_time)) as quantiles
FROM observations o
${traceFilter ? "JOIN traces t ON o.trace_id = t.id AND o.project_id = t.project_id" : ""}
WHERE project_id = {projectId: String}
@@ -2,6 +2,7 @@ import {
commandClickhouse,
parseClickhouseUTCDateTimeFormat,
queryClickhouse,
queryClickhouseStream,
upsertClickhouse,
} from "./clickhouse";
import { ObservationLevel } from "@prisma/client";
@@ -536,9 +537,12 @@ const getObservationsTableInternal = async <T>(
o.updated_at as "updated_at",
o.provided_model_name as "provided_model_name",
o.total_cost as "total_cost",
o.prompt_id as "prompt_id",
o.prompt_name as "prompt_name",
o.prompt_version as "prompt_version",
internal_model_id as "internal_model_id",
if(isNull(end_time), NULL, date_diff('milliseconds', start_time, end_time)) as latency,
if(isNull(completion_start_time), NULL, date_diff('milliseconds', start_time, completion_start_time)) as "time_to_first_token"`;
if(isNull(end_time), NULL, date_diff('millisecond', start_time, end_time)) as latency,
if(isNull(completion_start_time), NULL, date_diff('millisecond', start_time, completion_start_time)) as "time_to_first_token"`;
const { projectId, filter, selectIOAndMetadata, limit, offset, orderBy } =
opts;
@@ -747,6 +751,50 @@ export const getObservationsGroupedByModel = async (
return res.map((r) => ({ model: r.name }));
};
export const getObservationsGroupedByModelId = async (
projectId: string,
filter: FilterState,
) => {
const observationsFilter = new FilterList([
new StringFilter({
clickhouseTable: "observations",
field: "project_id",
operator: "=",
value: projectId,
tablePrefix: "o",
}),
]);
observationsFilter.push(
...createFilterFromFilterState(
filter,
observationsTableUiColumnDefinitions,
),
);
const appliedObservationsFilter = observationsFilter.apply();
// We mainly use queries like this to retrieve filter options.
// Therefore, we can skip final as some inaccuracy in count is acceptable.
const query = `
SELECT o.internal_model_id as modelId
FROM observations o
WHERE ${appliedObservationsFilter.query}
AND o.type = 'GENERATION'
GROUP BY o.internal_model_id
ORDER BY count() DESC
LIMIT 1000;
`;
const res = await queryClickhouse<{ modelId: string }>({
query,
params: {
...appliedObservationsFilter.params,
},
});
return res.map((r) => ({ modelId: r.modelId }));
};
export const getObservationsGroupedByName = async (
projectId: string,
filter: FilterState,
@@ -963,7 +1011,7 @@ export const getObservationMetricsForPrompts = async (
end_time,
usage_details,
cost_details,
dateDiff('milliseconds', start_time, end_time) AS latency_ms
dateDiff('millisecond', start_time, end_time) AS latency_ms
FROM observations
FINAL
WHERE (type = 'GENERATION')
@@ -1026,7 +1074,7 @@ export const getLatencyAndTotalCostForObservations = async (
SELECT
id,
cost_details['total'] AS total_cost,
dateDiff('milliseconds', start_time, end_time) AS latency_ms
dateDiff('millisecond', start_time, end_time) AS latency_ms
FROM observations FINAL
WHERE project_id = {projectId: String}
AND id IN ({observationIds: Array(String)})
@@ -1058,7 +1106,7 @@ export const getLatencyAndTotalCostForObservationsByTraces = async (
SELECT
trace_id,
sumMap(cost_details)['total'] AS total_cost,
dateDiff('milliseconds', min(start_time), max(end_time)) AS latency_ms
dateDiff('millisecond', min(start_time), max(end_time)) AS latency_ms
FROM observations FINAL
WHERE project_id = {projectId: String}
AND trace_id IN ({traceIds: Array(String)})
@@ -1167,23 +1215,23 @@ export const getTraceIdsForObservations = async (
}));
};
export const getGenerationsForPostHog = async (
export const getGenerationsForPostHog = async function* (
projectId: string,
minTimestamp: Date,
maxTimestamp: Date,
) => {
) {
const query = `
SELECT
o.name as name,
o.start_time as start_time,
o.id as id,
o.total_cost as total_cost,
if(isNull(completion_start_time), NULL, date_diff('milliseconds', start_time, completion_start_time)) as time_to_first_token,
if(isNull(completion_start_time), NULL, date_diff('millisecond', start_time, completion_start_time)) as time_to_first_token,
o.usage_details['total'] as input_tokens,
o.usage_details['output'] as output_tokens,
o.cost_details['total'] as total_tokens,
o.project_id as project_id,
if(isNull(end_time), NULL, date_diff('milliseconds', start_time, end_time) / 1000) as latency,
if(isNull(end_time), NULL, date_diff('millisecond', start_time, end_time) / 1000) as latency,
o.provided_model_name as model,
o.level as level,
o.version as version,
@@ -1205,7 +1253,7 @@ export const getGenerationsForPostHog = async (
AND o.type = 'GENERATION'
`;
const records = await queryClickhouse<Record<string, unknown>>({
const records = queryClickhouseStream<Record<string, unknown>>({
query,
params: {
projectId,
@@ -1215,32 +1263,34 @@ export const getGenerationsForPostHog = async (
});
const baseUrl = env.NEXTAUTH_URL?.replace("/api/auth", "");
return records.map((record) => ({
timestamp: record.start_time,
langfuse_generation_name: record.name,
langfuse_trace_name: record.trace_name,
langfuse_url: `${baseUrl}/project/${projectId}/traces/${encodeURIComponent(record.trace_id as string)}?observation=${encodeURIComponent(record.id as string)}`,
langfuse_id: record.id,
langfuse_cost_usd: record.total_cost,
langfuse_input_units: record.input_tokens,
langfuse_output_units: record.output_tokens,
langfuse_total_units: record.total_tokens,
langfuse_session_id: record.trace_session_id,
langfuse_project_id: projectId,
langfuse_user_id: record.trace_user_id || "langfuse_unknown_user",
langfuse_latency: record.latency,
langfuse_time_to_first_token: record.time_to_first_token,
langfuse_release: record.trace_release,
langfuse_version: record.version,
langfuse_model: record.model,
langfuse_level: record.level,
langfuse_tags: record.trace_tags,
langfuse_event_version: "1.0.0",
$session_id: record.posthog_session_id ?? null,
$set: {
langfuse_user_url: record.user_id
? `${baseUrl}/project/${projectId}/users/${encodeURIComponent(record.user_id as string)}`
: null,
},
}));
for await (const record of records) {
yield {
timestamp: record.start_time,
langfuse_generation_name: record.name,
langfuse_trace_name: record.trace_name,
langfuse_url: `${baseUrl}/project/${projectId}/traces/${encodeURIComponent(record.trace_id as string)}?observation=${encodeURIComponent(record.id as string)}`,
langfuse_id: record.id,
langfuse_cost_usd: record.total_cost,
langfuse_input_units: record.input_tokens,
langfuse_output_units: record.output_tokens,
langfuse_total_units: record.total_tokens,
langfuse_session_id: record.trace_session_id,
langfuse_project_id: projectId,
langfuse_user_id: record.trace_user_id || "langfuse_unknown_user",
langfuse_latency: record.latency,
langfuse_time_to_first_token: record.time_to_first_token,
langfuse_release: record.trace_release,
langfuse_version: record.version,
langfuse_model: record.model,
langfuse_level: record.level,
langfuse_tags: record.trace_tags,
langfuse_event_version: "1.0.0",
$session_id: record.posthog_session_id ?? null,
$set: {
langfuse_user_url: record.user_id
? `${baseUrl}/project/${projectId}/users/${encodeURIComponent(record.user_id as string)}`
: null,
},
};
}
};
@@ -9,7 +9,6 @@ import Decimal from "decimal.js";
import { parseClickhouseUTCDateTimeFormat } from "./clickhouse";
import { ObservationRecordReadType } from "./definitions";
import { parseJsonPrioritised } from "../../utils/json";
import { jsonSchema } from "../../utils/zod";
export const convertObservationToView = (
record: ObservationRecordReadType,
@@ -62,15 +61,22 @@ export const convertObservation = (
? parseClickhouseUTCDateTimeFormat(record.end_time)
: null,
name: record.name ?? null,
metadata: record.metadata,
metadata:
record.metadata &&
Object.fromEntries(
Object.entries(record.metadata ?? {}).map(([key, val]) => [
key,
val && parseJsonPrioritised(val),
]),
),
level: record.level as ObservationLevel,
statusMessage: record.status_message ?? null,
version: record.version ?? null,
input: (record.input
? jsonSchema.parse(parseJsonPrioritised(record.input))
? parseJsonPrioritised(record.input)
: null) as Prisma.JsonValue | null,
output: (record.output
? jsonSchema.parse(parseJsonPrioritised(record.output))
? parseJsonPrioritised(record.output)
: null) as Prisma.JsonValue | null,
modelParameters: record.model_parameters
? JSON.parse(record.model_parameters)
@@ -126,10 +132,11 @@ export const convertObservation = (
parseClickhouseUTCDateTimeFormat(record.start_time).getTime()
: null,
timeToFirstToken: record.completion_start_time
? parseClickhouseUTCDateTimeFormat(
? (parseClickhouseUTCDateTimeFormat(
record.completion_start_time,
).getTime() -
parseClickhouseUTCDateTimeFormat(record.start_time).getTime()
parseClickhouseUTCDateTimeFormat(record.start_time).getTime()) /
1000
: null,
};
};
@@ -3,6 +3,7 @@ import {
commandClickhouse,
parseClickhouseUTCDateTimeFormat,
queryClickhouse,
queryClickhouseStream,
upsertClickhouse,
} from "./clickhouse";
import { FilterList } from "../queries/clickhouse-sql/clickhouse-filter";
@@ -135,13 +136,16 @@ export const upsertScore = async (score: Partial<ScoreRecordReadType>) => {
});
};
export const getScoresForTraces = async (
projectId: string,
traceIds: string[],
timestamp?: Date,
limit?: number,
offset?: number,
) => {
export type GetScoresForTracesProps = {
projectId: string;
traceIds: string[];
timestamp?: Date;
limit?: number;
offset?: number;
};
export const getScoresForTraces = async (props: GetScoresForTracesProps) => {
const { projectId, traceIds, timestamp, limit, offset } = props;
const query = `
select
*
@@ -200,20 +204,24 @@ export const getScoresForObservations = async (
return rows.map(convertToScore);
};
export const getScoresGroupedByNameSourceType = async (projectId: string) => {
export const getScoresGroupedByNameSourceType = async (
projectId: string,
timestamp: Date | undefined,
) => {
// We mainly use queries like this to retrieve filter options.
// Therefore, we can skip final as some inaccuracy in count is acceptable.
const query = `
select
name,
source,
data_type
from scores s
WHERE s.project_id = {projectId: String}
GROUP BY name, source, data_type
ORDER BY count() desc
LIMIT 1000;
`;
select
name,
source,
data_type
from scores s
WHERE s.project_id = {projectId: String}
${timestamp ? `AND s.timestamp >= {timestamp: DateTime64(3)}` : ""}
GROUP BY name, source, data_type
ORDER BY count() desc
LIMIT 1000;
`;
const rows = await queryClickhouse<{
name: string;
@@ -223,6 +231,9 @@ export const getScoresGroupedByNameSourceType = async (projectId: string) => {
query: query,
params: {
projectId: projectId,
...(timestamp
? { timestamp: convertDateToClickhouseDateTime(timestamp) }
: {}),
},
});
@@ -666,11 +677,11 @@ export const getDistinctScoreNames = async (
return rows.map((row) => row.name);
};
export const getScoresForPostHog = async (
export const getScoresForPostHog = async function* (
projectId: string,
minTimestamp: Date,
maxTimestamp: Date,
) => {
) {
const query = `
SELECT
s.id as id,
@@ -694,7 +705,7 @@ export const getScoresForPostHog = async (
AND t.timestamp <= {maxTimestamp: DateTime64(3)}
`;
const records = await queryClickhouse<Record<string, unknown>>({
const records = queryClickhouseStream<Record<string, unknown>>({
query,
params: {
projectId,
@@ -704,24 +715,26 @@ export const getScoresForPostHog = async (
});
const baseUrl = env.NEXTAUTH_URL?.replace("/api/auth", "");
return records.map((record) => ({
timestamp: record.timestamp,
langfuse_score_name: record.name,
langfuse_score_value: record.value,
langfuse_score_comment: record.comment,
langfuse_trace_name: record.trace_name,
langfuse_id: record.id,
langfuse_session_id: record.trace_session_id,
langfuse_project_id: projectId,
langfuse_user_id: record.trace_user_id || "langfuse_unknown_user",
langfuse_release: record.trace_release,
langfuse_tags: record.trace_tags,
langfuse_event_version: "1.0.0",
$session_id: record.posthog_session_id ?? null,
$set: {
langfuse_user_url: record.user_id
? `${baseUrl}/project/${projectId}/users/${encodeURIComponent(record.user_id as string)}`
: null,
},
}));
for await (const record of records) {
yield {
timestamp: record.timestamp,
langfuse_score_name: record.name,
langfuse_score_value: record.value,
langfuse_score_comment: record.comment,
langfuse_trace_name: record.trace_name,
langfuse_id: record.id,
langfuse_session_id: record.trace_session_id,
langfuse_project_id: projectId,
langfuse_user_id: record.trace_user_id || "langfuse_unknown_user",
langfuse_release: record.trace_release,
langfuse_tags: record.trace_tags,
langfuse_event_version: "1.0.0",
$session_id: record.posthog_session_id ?? null,
$set: {
langfuse_user_url: record.user_id
? `${baseUrl}/project/${projectId}/users/${encodeURIComponent(record.user_id as string)}`
: null,
},
};
}
};
+57 -220
View File
@@ -2,6 +2,7 @@ import {
commandClickhouse,
parseClickhouseUTCDateTimeFormat,
queryClickhouse,
queryClickhouseStream,
upsertClickhouse,
} from "./clickhouse";
import {
@@ -16,10 +17,7 @@ import {
} from "../queries/clickhouse-sql/clickhouse-filter";
import { TraceRecordReadType } from "./definitions";
import { tracesTableUiColumnDefinitions } from "../../tableDefinitions/mapTracesTable";
import { OrderByState } from "../../interfaces/orderBy";
import { orderByToClickhouseSql } from "../queries/clickhouse-sql/orderby-factory";
import { UiColumnMapping } from "../../tableDefinitions";
import { sessionCols } from "../../tableDefinitions/mapSessionTable";
import { convertDateToClickhouseDateTime } from "../clickhouse/client";
import { convertClickhouseToDomain } from "./traces_converters";
import { clickhouseSearchCondition } from "../queries/clickhouse-sql/search";
@@ -383,196 +381,6 @@ export const getTracesGroupedByTags = async (props: GroupedTracesQueryProp) => {
return rows;
};
export type SessionDataReturnType = {
session_id: string;
max_timestamp: string;
min_timestamp: string;
trace_ids: string[];
user_ids: string[];
trace_count: number;
trace_tags: string[];
total_observations: number;
duration: number;
session_usage_details: Record<string, number>;
session_cost_details: Record<string, number>;
session_input_cost: string;
session_output_cost: string;
session_total_cost: string;
session_input_usage: string;
session_output_usage: string;
session_total_usage: string;
};
export const getSessionsTableCount = async (props: {
projectId: string;
filter: FilterState;
orderBy?: OrderByState;
limit?: number;
page?: number;
}) => {
const rows = await getSessionsTableGeneric<{ count: string }>({
select: `
count(session_id) as count
`,
projectId: props.projectId,
filter: props.filter,
orderBy: props.orderBy,
limit: props.limit,
page: props.page,
});
return rows.length > 0 ? Number(rows[0].count) : 0;
};
export const getSessionsTable = async (props: {
projectId: string;
filter: FilterState;
orderBy?: OrderByState;
limit?: number;
page?: number;
}) => {
const rows = await getSessionsTableGeneric<SessionDataReturnType>({
select: `
session_id,
max_timestamp,
min_timestamp,
trace_ids,
user_ids,
trace_count,
trace_tags,
total_observations,
duration,
session_usage_details,
session_cost_details,
session_input_cost,
session_output_cost,
session_total_cost,
session_input_usage,
session_output_usage,
session_total_usage
`,
projectId: props.projectId,
filter: props.filter,
orderBy: props.orderBy,
limit: props.limit,
page: props.page,
});
return rows;
};
export type FetchSessionsTableProps = {
select: string;
projectId: string;
filter: FilterState;
searchQuery?: string;
orderBy?: OrderByState;
limit?: number;
page?: number;
};
const getSessionsTableGeneric = async <T>(props: FetchSessionsTableProps) => {
const { select, projectId, filter, orderBy, limit, page } = props;
const { tracesFilter, scoresFilter, observationsFilter } =
getProjectIdDefaultFilter(projectId, { tracesPrefix: "s" });
tracesFilter.push(...createFilterFromFilterState(filter, sessionCols));
const tracesFilterRes = tracesFilter.apply();
const scoresAvgFilterRes = scoresFilter.apply();
const observationsStatsRes = observationsFilter.apply();
const traceTimestampFilter: DateTimeFilter | undefined = tracesFilter.find(
(f) =>
f.field === "min_timestamp" &&
(f.operator === ">=" || f.operator === ">"),
) as DateTimeFilter | undefined;
const singleTraceFilter = traceTimestampFilter
? new FilterList([
new DateTimeFilter({
clickhouseTable: "traces",
field: "timestamp",
operator: traceTimestampFilter.operator,
value: traceTimestampFilter.value,
}),
]).apply()
: undefined;
const query = `
WITH observations_agg AS (
SELECT o.trace_id,
count(*) as obs_count,
min(o.start_time) as min_start_time,
max(o.end_time) as max_end_time,
sumMap(usage_details) as sum_usage_details,
sumMap(cost_details) as sum_cost_details,
anyLast(project_id) as project_id
FROM observations o FINAL
WHERE o.project_id = {projectId: String}
${traceTimestampFilter ? `AND o.start_time >= {observationsStartTime: DateTime64(3)} - ${TRACE_TO_OBSERVATIONS_INTERVAL}` : ""}
GROUP BY o.trace_id
),
session_data AS (
SELECT
t.session_id,
anyLast(t.project_id) as project_id,
max(t.timestamp) as max_timestamp,
min(t.timestamp) as min_timestamp,
groupArray(t.id) AS trace_ids,
groupUniqArray(t.user_id) AS user_ids,
count(*) as trace_count,
groupUniqArrayArray(t.tags) as trace_tags,
-- Aggregate observations data at session level
sum(o.obs_count) as total_observations,
date_diff('milliseconds', min(min_start_time), max(max_end_time)) as duration,
sumMap(o.sum_usage_details) as session_usage_details,
sumMap(o.sum_cost_details) as session_cost_details,
arraySum(mapValues(mapFilter(x -> positionCaseInsensitive(x.1, 'input') > 0, sumMap(o.sum_cost_details)))) as session_input_cost,
arraySum(mapValues(mapFilter(x -> positionCaseInsensitive(x.1, 'output') > 0, sumMap(o.sum_cost_details)))) as session_output_cost,
sumMap(o.sum_cost_details)['total'] as session_total_cost,
arraySum(mapValues(mapFilter(x -> positionCaseInsensitive(x.1, 'input') > 0, sumMap(o.sum_usage_details)))) as session_input_usage,
arraySum(mapValues(mapFilter(x -> positionCaseInsensitive(x.1, 'output') > 0, sumMap(o.sum_usage_details)))) as session_output_usage,
sumMap(o.sum_usage_details)['total'] as session_total_usage
FROM traces t FINAL
LEFT JOIN observations_agg o
ON t.id = o.trace_id AND t.project_id = o.project_id
WHERE t.session_id IS NOT NULL
AND t.project_id = {projectId: String}
${singleTraceFilter?.query ? ` AND ${singleTraceFilter.query}` : ""}
GROUP BY t.session_id
)
SELECT ${select}
FROM session_data s
WHERE ${tracesFilterRes.query ? tracesFilterRes.query : ""}
${orderByToClickhouseSql(orderBy ?? null, sessionCols)}
${limit !== undefined && page !== undefined ? `LIMIT {limit: Int32} OFFSET {offset: Int32}` : ""}
`;
const obsStartTimeValue = traceTimestampFilter
? convertDateToClickhouseDateTime(traceTimestampFilter.value)
: null;
const res = await queryClickhouse<T>({
query: query,
params: {
projectId,
limit: limit,
offset: limit && page ? limit * page : 0,
...tracesFilterRes.params,
...observationsStatsRes.params,
...scoresAvgFilterRes.params,
...singleTraceFilter?.params,
...(obsStartTimeValue
? { observationsStartTime: obsStartTimeValue }
: {}),
},
});
return res;
};
export const getTracesIdentifierForSession = async (
projectId: string,
sessionId: string,
@@ -680,10 +488,25 @@ export const getTotalUserCount = async (
});
};
export const getUserMetrics = async (projectId: string, userIds: string[]) => {
export const getUserMetrics = async (
projectId: string,
userIds: string[],
filter: FilterState,
) => {
if (userIds.length === 0) {
return [];
}
// filter state contains date range filter for traces so far.
const chFilter = new FilterList(
createFilterFromFilterState(filter, tracesTableUiColumnDefinitions),
);
const chFilterRes = chFilter.apply();
const timestampFilter = chFilter.find(
(f) => f.field === "timestamp" && f.operator === ">=",
);
// this query uses window functions on observations + traces to always get only the first row and thereby remove deduplicates
// we filter wherever possible by project id and user id
const query = `
@@ -713,6 +536,7 @@ export const getUserMetrics = async (projectId: string, userIds: string[]) => {
observations o
WHERE
o.project_id = {projectId: String }
${timestampFilter ? `AND o.start_time >= {traceTimestamp: DateTime64(3)} - ${OBSERVATIONS_TO_TRACE_INTERVAL}` : ""}
AND o.trace_id in (
SELECT
distinct id
@@ -721,6 +545,7 @@ export const getUserMetrics = async (projectId: string, userIds: string[]) => {
where
user_id IN ({userIds: Array(String) })
AND project_id = {projectId: String }
${filter.length > 0 ? `AND ${chFilterRes.query}` : ""}
)
AND o.type = 'GENERATION'
) as o
@@ -740,6 +565,7 @@ export const getUserMetrics = async (projectId: string, userIds: string[]) => {
WHERE
t.user_id IN ({userIds: Array(String) })
AND t.project_id = {projectId: String }
${filter.length > 0 ? `AND ${chFilterRes.query}` : ""}
) as t on t.id = o.trace_id
and t.project_id = o.project_id
WHERE
@@ -778,8 +604,17 @@ export const getUserMetrics = async (projectId: string, userIds: string[]) => {
params: {
projectId,
userIds,
...chFilterRes.params,
...(timestampFilter
? {
traceTimestamp: convertDateToClickhouseDateTime(
(timestampFilter as DateTimeFilter).value,
),
}
: {}),
},
});
return rows.map((row) => ({
userId: row.user_id,
maxTimestamp: parseClickhouseUTCDateTimeFormat(row.max_timestamp),
@@ -793,18 +628,18 @@ export const getUserMetrics = async (projectId: string, userIds: string[]) => {
}));
};
export const getTracesForPostHog = async (
export const getTracesForPostHog = async function* (
projectId: string,
minTimestamp: Date,
maxTimestamp: Date,
) => {
) {
const query = `
WITH observations_agg AS (
SELECT o.project_id,
o.trace_id,
sum(total_cost) as total_cost,
count(*) as observation_count,
date_diff('milliseconds', least(min(start_time), min(end_time)), greatest(max(start_time), max(end_time))) as latency_milliseconds
date_diff('millisecond', least(min(start_time), min(end_time)), greatest(max(start_time), max(end_time))) as latency_milliseconds
FROM observations o FINAL
WHERE o.project_id = {projectId: String}
AND o.start_time >= {minTimestamp: DateTime64(3)} - ${TRACE_TO_OBSERVATIONS_INTERVAL}
@@ -831,7 +666,7 @@ export const getTracesForPostHog = async (
AND t.timestamp <= {maxTimestamp: DateTime64(3)}
`;
const records = await queryClickhouse<Record<string, unknown>>({
const records = queryClickhouseStream<Record<string, unknown>>({
query,
params: {
projectId,
@@ -841,28 +676,30 @@ export const getTracesForPostHog = async (
});
const baseUrl = env.NEXTAUTH_URL?.replace("/api/auth", "");
return records.map((record) => ({
timestamp: record.timestamp,
langfuse_id: record.id,
langfuse_trace_name: record.name,
langfuse_url: `${baseUrl}/project/${projectId}/traces/${encodeURIComponent(record.id as string)}`,
langfuse_cost_usd: record.total_cost,
langfuse_count_observations: record.observation_count,
langfuse_session_id: record.session_id,
langfuse_project_id: projectId,
langfuse_user_id: record.user_id || "langfuse_unknown_user",
langfuse_latency: record.latency,
langfuse_release: record.release,
langfuse_version: record.version,
langfuse_tags: record.tags,
langfuse_event_version: "1.0.0",
$session_id: record.posthog_session_id ?? null,
$set: {
langfuse_user_url: record.user_id
? `${baseUrl}/project/${projectId}/users/${encodeURIComponent(record.user_id as string)}`
: null,
},
}));
for await (const record of records) {
yield {
timestamp: record.timestamp,
langfuse_id: record.id,
langfuse_trace_name: record.name,
langfuse_url: `${baseUrl}/project/${projectId}/traces/${encodeURIComponent(record.id as string)}`,
langfuse_cost_usd: record.total_cost,
langfuse_count_observations: record.observation_count,
langfuse_session_id: record.session_id,
langfuse_project_id: projectId,
langfuse_user_id: record.user_id || "langfuse_unknown_user",
langfuse_latency: record.latency,
langfuse_release: record.release,
langfuse_version: record.version,
langfuse_tags: record.tags,
langfuse_event_version: "1.0.0",
$session_id: record.posthog_session_id ?? null,
$set: {
langfuse_user_url: record.user_id
? `${baseUrl}/project/${projectId}/users/${encodeURIComponent(record.user_id as string)}`
: null,
},
};
}
};
export const getTracesByIdsForAnyProject = async (traceIds: string[]) => {
@@ -1,7 +1,8 @@
import { Trace } from "@prisma/client";
import { Prisma, Trace } from "@prisma/client";
import { parseClickhouseUTCDateTimeFormat } from "./clickhouse";
import { TraceRecordReadType } from "./definitions";
import { convertDateToClickhouseDateTime } from "../clickhouse/client";
import { parseJsonPrioritised } from "../../utils/json";
export const convertTraceDomainToClickhouse = (
trace: Trace,
@@ -43,9 +44,20 @@ export const convertClickhouseToDomain = (
userId: record.user_id ?? null,
sessionId: record.session_id ?? null,
public: record.public,
input: record.input ?? null,
output: record.output ?? null,
metadata: record.metadata,
input: (record.input
? parseJsonPrioritised(record.input)
: null) as Prisma.JsonValue | null,
output: (record.output
? parseJsonPrioritised(record.output)
: null) as Prisma.JsonValue | null,
metadata:
record.metadata &&
Object.fromEntries(
Object.entries(record.metadata ?? {}).map(([key, val]) => [
key,
val && parseJsonPrioritised(val),
]),
),
createdAt: parseClickhouseUTCDateTimeFormat(record.created_at),
updatedAt: parseClickhouseUTCDateTimeFormat(record.updated_at),
externalId: null,
@@ -30,7 +30,7 @@ export interface StorageService {
download(path: string): Promise<string>;
listFiles(prefix: string): Promise<string[]>;
listFiles(prefix: string): Promise<{ file: string; createdAt: Date }[]>;
getSignedUrl(
fileName: string,
@@ -201,7 +201,9 @@ class AzureBlobStorageService implements StorageService {
}
}
public async listFiles(prefix: string): Promise<string[]> {
public async listFiles(
prefix: string,
): Promise<{ file: string; createdAt: Date }[]> {
try {
await this.createContainerIfNotExists();
@@ -209,7 +211,10 @@ class AzureBlobStorageService implements StorageService {
const files = [];
for await (const blob of result) {
if (blob.name.startsWith(prefix)) {
files.push(blob.name);
files.push({
file: blob.name,
createdAt: blob?.properties?.createdOn ?? new Date(),
});
}
}
return files;
@@ -364,7 +369,9 @@ class S3StorageService implements StorageService {
}
}
public async listFiles(prefix: string): Promise<string[]> {
public async listFiles(
prefix: string,
): Promise<{ file: string; createdAt: Date }[]> {
const listCommand = new ListObjectsV2Command({
Bucket: this.bucketName,
Prefix: prefix,
@@ -373,7 +380,11 @@ class S3StorageService implements StorageService {
try {
const response = await this.client.send(listCommand);
return (
response.Contents?.flatMap((file) => (file.Key ? [file.Key] : [])) ?? []
response.Contents?.flatMap((file) =>
file.Key
? [{ file: file.Key, createdAt: file.LastModified ?? new Date() }]
: [],
) ?? []
);
} catch (err) {
logger.error(`Failed to list files from S3 ${prefix}`, err);
@@ -0,0 +1,318 @@
import { OrderByState } from "../../interfaces/orderBy";
import { sessionCols } from "../../tableDefinitions/mapSessionTable";
import { FilterState } from "../../types";
import { convertDateToClickhouseDateTime } from "../clickhouse/client";
import { DateTimeFilter, FilterList, orderByToClickhouseSql } from "../queries";
import {
getProjectIdDefaultFilter,
createFilterFromFilterState,
} from "../queries/clickhouse-sql/factory";
import {
TRACE_TO_OBSERVATIONS_INTERVAL,
queryClickhouse,
} from "../repositories";
export type SessionDataReturnType = {
session_id: string;
max_timestamp: string;
min_timestamp: string;
trace_ids: string[];
user_ids: string[];
trace_count: number;
trace_tags: string[];
};
export type SessionWithMetricsReturnType = SessionDataReturnType & {
total_observations: number;
duration: number;
session_usage_details: Record<string, number>;
session_cost_details: Record<string, number>;
session_input_cost: string;
session_output_cost: string;
session_total_cost: string;
session_input_usage: string;
session_output_usage: string;
session_total_usage: string;
};
export const getSessionsTableCount = async (props: {
projectId: string;
filter: FilterState;
orderBy?: OrderByState;
limit?: number;
page?: number;
}) => {
const rows = await getSessionsTableGeneric<{ count: string }>({
select: "count",
projectId: props.projectId,
filter: props.filter,
orderBy: props.orderBy,
limit: props.limit,
page: props.page,
});
return rows.length > 0 ? Number(rows[0].count) : 0;
};
export const getSessionsTable = async (props: {
projectId: string;
filter: FilterState;
orderBy?: OrderByState;
limit?: number;
page?: number;
}) => {
const rows = await getSessionsTableGeneric<SessionDataReturnType>({
select: "rows",
projectId: props.projectId,
filter: props.filter,
orderBy: props.orderBy,
limit: props.limit,
page: props.page,
});
return rows.map((row) => ({
...row,
trace_count: Number(row.trace_count),
}));
};
export const getSessionsWithMetrics = async (props: {
projectId: string;
filter: FilterState;
orderBy?: OrderByState;
limit?: number;
page?: number;
}) => {
const rows = await getSessionsTableGeneric<SessionWithMetricsReturnType>({
select: "metrics",
projectId: props.projectId,
filter: props.filter,
orderBy: props.orderBy,
limit: props.limit,
page: props.page,
});
return rows.map((row) => ({
...row,
trace_count: Number(row.trace_count),
total_observations: Number(row.total_observations),
}));
};
export type FetchSessionsTableProps = {
select: "count" | "rows" | "metrics";
projectId: string;
filter: FilterState;
searchQuery?: string;
orderBy?: OrderByState;
limit?: number;
page?: number;
};
const getSessionsTableGeneric = async <T>(props: FetchSessionsTableProps) => {
const { select, projectId, filter, orderBy, limit, page } = props;
let sqlSelect: string;
switch (select) {
case "count":
sqlSelect = "count(session_id) as count";
break;
case "rows":
sqlSelect = `
session_id,
max_timestamp,
min_timestamp,
trace_ids,
user_ids,
trace_count,
trace_tags`;
break;
case "metrics":
sqlSelect = `
session_id,
max_timestamp,
min_timestamp,
trace_ids,
user_ids,
trace_count,
trace_tags,
total_observations,
duration,
session_usage_details,
session_cost_details,
session_input_cost,
session_output_cost,
session_total_cost,
session_input_usage,
session_output_usage,
session_total_usage`;
break;
default:
const exhaustiveCheckDefault: never = select;
throw new Error(`Unknown select type: ${select}`);
}
const { tracesFilter } = getProjectIdDefaultFilter(projectId, {
tracesPrefix: "s",
});
tracesFilter.push(...createFilterFromFilterState(filter, sessionCols));
const tracesFilterRes = tracesFilter.apply();
const traceTimestampFilter: DateTimeFilter | undefined = tracesFilter.find(
(f) =>
f.field === "min_timestamp" &&
(f.operator === ">=" || f.operator === ">"),
) as DateTimeFilter | undefined;
const filters = [];
if (traceTimestampFilter) {
filters.push(
new DateTimeFilter({
clickhouseTable: "traces",
field: "timestamp",
operator: traceTimestampFilter.operator,
value: traceTimestampFilter.value,
}),
);
}
const additionalSingleTraceFilter = tracesFilter.find(
(f) => f.field === "bookmarked" || f.field === "session_id",
);
if (additionalSingleTraceFilter) {
filters.push(additionalSingleTraceFilter);
}
const singleTraceFilter =
filters.length > 0 ? new FilterList(filters).apply() : undefined;
const hasMetricsFilter =
tracesFilter.find((f) =>
[
"session_total_cost",
"session_input_cost",
"session_output_cost",
"duration",
"session_total_usage",
"session_output_usage",
"session_input_usage",
].includes(f.field),
) ||
(orderBy &&
[
"totalCost",
"inputCost",
"outputCost",
"sessionDuration",
"totalUsage",
"outputUsage",
"inputUsage",
].includes(orderBy?.column));
const selectMetrics = select === "metrics" || hasMetricsFilter;
// We use deduplicated traces and observations CTEs instead of final to be able to use Skip indices in Clickhouse.
const query = `
WITH deduplicated_traces AS (
SELECT * EXCEPT input, output, metadata
FROM traces t
WHERE t.session_id IS NOT NULL
AND t.project_id = {projectId: String}
${singleTraceFilter?.query ? ` AND ${singleTraceFilter.query}` : ""}
ORDER BY event_ts DESC
LIMIT 1 BY id, project_id
),
deduplicated_observations AS (
SELECT *
FROM observations o
WHERE o.project_id = {projectId: String}
${traceTimestampFilter ? `AND o.start_time >= {observationsStartTime: DateTime64(3)} - ${TRACE_TO_OBSERVATIONS_INTERVAL}` : ""}
AND o.trace_id IN (
SELECT id
FROM deduplicated_traces
)
ORDER BY event_ts DESC
LIMIT 1 BY id, project_id
),
observations_agg AS (
SELECT o.trace_id,
count(*) as obs_count,
min(o.start_time) as min_start_time,
max(o.end_time) as max_end_time,
sumMap(usage_details) as sum_usage_details,
sumMap(cost_details) as sum_cost_details,
anyLast(project_id) as project_id
FROM deduplicated_observations o
WHERE o.project_id = {projectId: String}
${traceTimestampFilter ? `AND o.start_time >= {observationsStartTime: DateTime64(3)} - ${TRACE_TO_OBSERVATIONS_INTERVAL}` : ""}
GROUP BY o.trace_id
),
session_data AS (
SELECT
t.session_id,
anyLast(t.project_id) as project_id,
max(t.timestamp) as max_timestamp,
min(t.timestamp) as min_timestamp,
groupArray(t.id) AS trace_ids,
groupUniqArray(t.user_id) AS user_ids,
count(*) as trace_count,
groupUniqArrayArray(t.tags) as trace_tags
-- Aggregate observations data at session level
${
selectMetrics
? `
,
sum(o.obs_count) as total_observations,
date_diff('millisecond', min(min_start_time), max(max_end_time)) as duration,
sumMap(o.sum_usage_details) as session_usage_details,
sumMap(o.sum_cost_details) as session_cost_details,
arraySum(mapValues(mapFilter(x -> positionCaseInsensitive(x.1, 'input') > 0, sumMap(o.sum_cost_details)))) as session_input_cost,
arraySum(mapValues(mapFilter(x -> positionCaseInsensitive(x.1, 'output') > 0, sumMap(o.sum_cost_details)))) as session_output_cost,
sumMap(o.sum_cost_details)['total'] as session_total_cost,
arraySum(mapValues(mapFilter(x -> positionCaseInsensitive(x.1, 'input') > 0, sumMap(o.sum_usage_details)))) as session_input_usage,
arraySum(mapValues(mapFilter(x -> positionCaseInsensitive(x.1, 'output') > 0, sumMap(o.sum_usage_details)))) as session_output_usage,
sumMap(o.sum_usage_details)['total'] as session_total_usage`
: ""
}
FROM deduplicated_traces t
${
selectMetrics
? `LEFT JOIN observations_agg o
ON t.id = o.trace_id AND t.project_id = o.project_id`
: ""
}
WHERE t.session_id IS NOT NULL
AND t.project_id = {projectId: String}
${singleTraceFilter?.query ? ` AND ${singleTraceFilter.query}` : ""}
GROUP BY t.session_id
)
SELECT ${sqlSelect}
FROM session_data s
WHERE ${tracesFilterRes.query ? tracesFilterRes.query : ""}
${orderByToClickhouseSql(orderBy ?? null, sessionCols)}
${limit !== undefined && page !== undefined ? `LIMIT {limit: Int32} OFFSET {offset: Int32}` : ""}
`;
const obsStartTimeValue = traceTimestampFilter
? convertDateToClickhouseDateTime(traceTimestampFilter.value)
: null;
const res = await queryClickhouse<T>({
query: query,
params: {
projectId,
limit: limit,
offset: limit && page ? limit * page : 0,
...tracesFilterRes.params,
...singleTraceFilter?.params,
...(obsStartTimeValue
? { observationsStartTime: obsStartTimeValue }
: {}),
},
});
return res;
};
@@ -351,7 +351,7 @@ const getTracesTableGeneric = async <T>(props: FetchTracesTableProps) => {
COUNT(*) AS observation_count,
sumMap(usage_details) as usage_details,
SUM(total_cost) AS total_cost,
date_diff('milliseconds', least(min(start_time), min(end_time)), greatest(max(start_time), max(end_time))) as latency_milliseconds,
date_diff('millisecond', least(min(start_time), min(end_time)), greatest(max(start_time), max(end_time))) as latency_milliseconds,
multiIf(
arrayExists(x -> x = 'ERROR', groupArray(level)), 'ERROR',
arrayExists(x -> x = 'WARNING', groupArray(level)), 'WARNING',
@@ -68,7 +68,7 @@ export const observationsTableUiColumnDefinitions: UiColumnMapping[] = [
uiTableId: "timeToFirstToken",
clickhouseTableName: "observations",
clickhouseSelect:
"if(isNull(completion_start_time), NULL, date_diff('milliseconds', start_time, completion_start_time) / 1000)",
"if(isNull(completion_start_time), NULL, date_diff('millisecond', start_time, completion_start_time) / 1000)",
// If we use the default of Decimal64(12), we cannot filter for more than ~40min due to an overflow
clickhouseTypeOverwrite: "Decimal64(3)",
},
@@ -77,7 +77,7 @@ export const observationsTableUiColumnDefinitions: UiColumnMapping[] = [
uiTableId: "latency",
clickhouseTableName: "observations",
clickhouseSelect:
"if(isNull(end_time), NULL, date_diff('milliseconds', start_time, end_time) / 1000)",
"if(isNull(end_time), NULL, date_diff('millisecond', start_time, end_time) / 1000)",
// If we use the default of Decimal64(12), we cannot filter for more than ~40min due to an overflow
clickhouseTypeOverwrite: "Decimal64(3)",
},
@@ -86,7 +86,7 @@ export const observationsTableUiColumnDefinitions: UiColumnMapping[] = [
uiTableId: "tokensPerSecond",
clickhouseTableName: "observations",
clickhouseSelect:
"(arraySum(mapValues(mapFilter(x -> positionCaseInsensitive(x.1, 'output') > 0, usage_details))) / (date_diff('milliseconds', start_time, end_time) / 1000))",
"(arraySum(mapValues(mapFilter(x -> positionCaseInsensitive(x.1, 'output') > 0, usage_details))) / (date_diff('millisecond', start_time, end_time) / 1000))",
},
{
uiTableName: "Input Cost ($)",
@@ -127,6 +127,12 @@ export const observationsTableUiColumnDefinitions: UiColumnMapping[] = [
clickhouseTableName: "observations",
clickhouseSelect: 'o."provided_model_name"',
},
{
uiTableName: "Model ID",
uiTableId: "modelId",
clickhouseTableName: "observations",
clickhouseSelect: 'o."internal_model_id"',
},
{
uiTableName: "Input Tokens",
uiTableId: "inputTokens",
@@ -1,7 +1,7 @@
import { UiColumnMapping } from ".";
export const sessionCols: UiColumnMapping[] = [
// we do not access the traces scores in clichouse. We default back to the trace timestamps.
// we do not access the traces scores in ClickHouse. We default back to the trace timestamps.
{
uiTableName: "⭐️",
@@ -26,6 +26,8 @@ export const sessionCols: UiColumnMapping[] = [
uiTableId: "sessionDuration",
clickhouseTableName: "traces",
clickhouseSelect: "duration",
// If we use the default of Decimal64(12), we cannot filter for more than ~40min due to an overflow
clickhouseTypeOverwrite: "Decimal64(3)",
},
{
uiTableName: "Count Traces",
@@ -86,6 +88,8 @@ export const sessionCols: UiColumnMapping[] = [
uiTableId: "sessionDuration",
clickhouseTableName: "traces",
clickhouseSelect: "duration",
// If we use the default of Decimal64(12), we cannot filter for more than ~40min due to an overflow
clickhouseTypeOverwrite: "Decimal64(3)",
},
{
uiTableName: "Traces Count",
+4 -22
View File
@@ -1,6 +1,6 @@
import { z } from "zod";
import { merge } from "lodash";
import { JsonNested, jsonSchema, jsonSchemaNullable } from "./zod";
import { JsonNested, jsonSchema } from "./zod";
/**
* Deeply parses a JSON string or object for nested stringified JSON
@@ -52,29 +52,11 @@ export const mergeJson = (
export const parseJsonPrioritised = (
json: string,
): z.infer<typeof jsonSchema> | string | undefined => {
): JsonNested | string | undefined => {
try {
const parsedJson = JSON.parse(json);
if (Object.keys(parsedJson).length === 0) {
return parsedJson;
}
const parsedArray = z.array(jsonSchemaNullable).safeParse(parsedJson);
if (parsedArray.success) {
return parsedArray.data;
}
const parsedObject = z.record(jsonSchemaNullable).safeParse(parsedJson);
if (parsedObject.success) {
return parsedObject.data;
}
return jsonSchema.parse(parsedJson);
return JSON.parse(json);
} catch (error) {
const parsed = jsonSchema.safeParse(json);
return parsed.success ? parsed.data : json;
return json;
}
};
+2 -2
View File
@@ -23,18 +23,18 @@ type Json = Root | { [key: string]: JsonNested } | JsonNested[];
// Here, you define the schema recursively
export const jsonSchemaNullable: z.ZodType<JsonNested> = z.lazy(() =>
z.union([
nestedLiteralSchema,
z.array(jsonSchemaNullable),
z.record(jsonSchemaNullable),
nestedLiteralSchema,
]),
);
// Root schema that does not allow nulls at the root level
export const jsonSchema: z.ZodType<Json> = z.lazy(() =>
z.union([
rootLiteralSchema,
z.array(jsonSchemaNullable),
z.record(jsonSchemaNullable),
rootLiteralSchema,
]),
);
+162 -183
View File
@@ -6,6 +6,7 @@ settings:
overrides:
jsonpath-plus: 10.0.7
nanoid: ^3.3.8
importers:
@@ -45,11 +46,11 @@ importers:
specifier: ^1.7.7
version: 1.7.7
next:
specifier: ^14.2.15
version: 14.2.15(@babel/core@7.24.3)(@opentelemetry/api@1.9.0)(@playwright/test@1.47.2)(babel-plugin-macros@3.1.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
specifier: ^14.2.21
version: 14.2.21(@babel/core@7.24.3)(@opentelemetry/api@1.9.0)(@playwright/test@1.47.2)(babel-plugin-macros@3.1.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
next-auth:
specifier: ^4.24.11
version: 4.24.11(next@14.2.15(@babel/core@7.24.3)(@opentelemetry/api@1.9.0)(@playwright/test@1.47.2)(babel-plugin-macros@3.1.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(nodemailer@6.9.15)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
version: 4.24.11(next@14.2.21(@babel/core@7.24.3)(@opentelemetry/api@1.9.0)(@playwright/test@1.47.2)(babel-plugin-macros@3.1.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(nodemailer@6.9.15)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
zod:
specifier: ^3.23.8
version: 3.23.8
@@ -183,8 +184,8 @@ importers:
specifier: ^2.4.3
version: 2.4.3
bullmq:
specifier: ^5.12.10
version: 5.12.10
specifier: ^5.34.10
version: 5.34.10
dd-trace:
specifier: ^5.23.1
version: 5.23.1
@@ -211,7 +212,7 @@ importers:
version: 4.17.21
next-auth:
specifier: ^4.24.11
version: 4.24.11(next@14.2.15(@babel/core@7.24.3)(@opentelemetry/api@1.9.0)(@playwright/test@1.47.2)(babel-plugin-macros@3.1.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(nodemailer@6.9.15)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
version: 4.24.11(next@14.2.21(@babel/core@7.24.3)(@opentelemetry/api@1.9.0)(@playwright/test@1.47.2)(babel-plugin-macros@3.1.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(nodemailer@6.9.15)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
nodemailer:
specifier: ^6.9.15
version: 6.9.15
@@ -311,7 +312,7 @@ importers:
version: 0.0.4
'@appsignal/opentelemetry-instrumentation-bullmq':
specifier: ^0.7.3
version: 0.7.3(bullmq@5.12.10)
version: 0.7.3(bullmq@5.34.10)
'@baselime/trpc-opentelemetry-middleware':
specifier: ^0.1.2
version: 0.1.2(@opentelemetry/api@1.9.0)(@trpc/server@10.45.2)
@@ -338,7 +339,7 @@ importers:
version: 2.1.9(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
'@headlessui/tailwindcss':
specifier: 0.2.1
version: 0.2.1(tailwindcss@3.4.13(ts-node@10.9.2(@types/node@20.10.5)(typescript@5.4.5)))
version: 0.2.1(tailwindcss@3.4.17(ts-node@10.9.2(@types/node@20.10.5)(typescript@5.4.5)))
'@heroicons/react':
specifier: ^2.1.5
version: 2.1.5(react@18.2.0)
@@ -368,7 +369,7 @@ importers:
version: 7.19.0(@emotion/react@11.11.4(@types/react@18.2.79)(react@18.2.0))(@emotion/styled@11.11.5(@emotion/react@11.11.4(@types/react@18.2.79)(react@18.2.0))(@types/react@18.2.79)(react@18.2.0))(@mui/material@5.15.19(@emotion/react@11.11.4(@types/react@18.2.79)(react@18.2.0))(@emotion/styled@11.11.5(@emotion/react@11.11.4(@types/react@18.2.79)(react@18.2.0))(@types/react@18.2.79)(react@18.2.0))(@types/react@18.2.79)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@mui/system@5.16.7(@emotion/react@11.11.4(@types/react@18.2.79)(react@18.2.0))(@emotion/styled@11.11.5(@emotion/react@11.11.4(@types/react@18.2.79)(react@18.2.0))(@types/react@18.2.79)(react@18.2.0))(@types/react@18.2.79)(react@18.2.0))(@types/react@18.2.79)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
'@next-auth/prisma-adapter':
specifier: ^1.0.7
version: 1.0.7(@prisma/client@5.22.0(prisma@5.22.0))(next-auth@4.24.11(next@14.2.15(@babel/core@7.24.3)(@opentelemetry/api@1.9.0)(@playwright/test@1.47.2)(babel-plugin-macros@3.1.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(nodemailer@6.9.15)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))
version: 1.0.7(@prisma/client@5.22.0(prisma@5.22.0))(next-auth@4.24.11(next@14.2.21(@babel/core@7.24.3)(@opentelemetry/api@1.9.0)(@playwright/test@1.47.2)(babel-plugin-macros@3.1.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(nodemailer@6.9.15)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))
'@opentelemetry/api':
specifier: ^1.9.0
version: 1.9.0
@@ -491,13 +492,13 @@ importers:
version: link:../packages/config-typescript
'@sentry/nextjs':
specifier: ^8.39.0
version: 8.39.0(@opentelemetry/core@1.26.0(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.53.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.26.0(@opentelemetry/api@1.9.0))(next@14.2.15(@babel/core@7.24.3)(@opentelemetry/api@1.9.0)(@playwright/test@1.47.2)(babel-plugin-macros@3.1.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react@18.2.0)(webpack@5.95.0)
version: 8.39.0(@opentelemetry/core@1.26.0(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.53.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.26.0(@opentelemetry/api@1.9.0))(next@14.2.21(@babel/core@7.24.3)(@opentelemetry/api@1.9.0)(@playwright/test@1.47.2)(babel-plugin-macros@3.1.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react@18.2.0)(webpack@5.95.0)
'@t3-oss/env-nextjs':
specifier: ^0.11.1
version: 0.11.1(typescript@5.4.5)(zod@3.23.8)
'@tailwindcss/container-queries':
specifier: ^0.1.1
version: 0.1.1(tailwindcss@3.4.13(ts-node@10.9.2(@types/node@20.10.5)(typescript@5.4.5)))
version: 0.1.1(tailwindcss@3.4.17(ts-node@10.9.2(@types/node@20.10.5)(typescript@5.4.5)))
'@tanstack/react-query':
specifier: ^4.36.1
version: 4.36.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
@@ -506,13 +507,13 @@ importers:
version: 8.20.5(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
'@tremor/react':
specifier: 3.16.2
version: 3.16.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(tailwindcss@3.4.13(ts-node@10.9.2(@types/node@20.10.5)(typescript@5.4.5)))
version: 3.16.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(tailwindcss@3.4.17(ts-node@10.9.2(@types/node@20.10.5)(typescript@5.4.5)))
'@trpc/client':
specifier: ^10.45.0
version: 10.45.2(@trpc/server@10.45.2)
'@trpc/next':
specifier: ^10.45.0
version: 10.45.2(@tanstack/react-query@4.36.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@trpc/client@10.45.2(@trpc/server@10.45.2))(@trpc/react-query@10.45.2(@tanstack/react-query@4.36.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@trpc/client@10.45.2(@trpc/server@10.45.2))(@trpc/server@10.45.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@trpc/server@10.45.2)(next@14.2.15(@babel/core@7.24.3)(@opentelemetry/api@1.9.0)(@playwright/test@1.47.2)(babel-plugin-macros@3.1.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
version: 10.45.2(@tanstack/react-query@4.36.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@trpc/client@10.45.2(@trpc/server@10.45.2))(@trpc/react-query@10.45.2(@tanstack/react-query@4.36.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@trpc/client@10.45.2(@trpc/server@10.45.2))(@trpc/server@10.45.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@trpc/server@10.45.2)(next@14.2.21(@babel/core@7.24.3)(@opentelemetry/api@1.9.0)(@playwright/test@1.47.2)(babel-plugin-macros@3.1.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
'@trpc/react-query':
specifier: ^10.45.0
version: 10.45.2(@tanstack/react-query@4.36.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@trpc/client@10.45.2(@trpc/server@10.45.2))(@trpc/server@10.45.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
@@ -535,8 +536,8 @@ importers:
specifier: ^2.4.3
version: 2.4.3
bullmq:
specifier: ^5.12.10
version: 5.12.10
specifier: ^5.34.10
version: 5.34.10
class-variance-authority:
specifier: ^0.7.0
version: 0.7.0
@@ -589,14 +590,14 @@ importers:
specifier: ^0.462.0
version: 0.462.0(react@18.2.0)
next:
specifier: ^14.2.15
version: 14.2.15(@babel/core@7.24.3)(@opentelemetry/api@1.9.0)(@playwright/test@1.47.2)(babel-plugin-macros@3.1.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
specifier: ^14.2.21
version: 14.2.21(@babel/core@7.24.3)(@opentelemetry/api@1.9.0)(@playwright/test@1.47.2)(babel-plugin-macros@3.1.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
next-auth:
specifier: ^4.24.11
version: 4.24.11(next@14.2.15(@babel/core@7.24.3)(@opentelemetry/api@1.9.0)(@playwright/test@1.47.2)(babel-plugin-macros@3.1.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(nodemailer@6.9.15)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
version: 4.24.11(next@14.2.21(@babel/core@7.24.3)(@opentelemetry/api@1.9.0)(@playwright/test@1.47.2)(babel-plugin-macros@3.1.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(nodemailer@6.9.15)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
next-query-params:
specifier: ^5.0.1
version: 5.0.1(next@14.2.15(@babel/core@7.24.3)(@opentelemetry/api@1.9.0)(@playwright/test@1.47.2)(babel-plugin-macros@3.1.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react@18.2.0)(use-query-params@2.2.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0))
version: 5.0.1(next@14.2.21(@babel/core@7.24.3)(@opentelemetry/api@1.9.0)(@playwright/test@1.47.2)(babel-plugin-macros@3.1.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react@18.2.0)(use-query-params@2.2.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0))
next-themes:
specifier: ^0.3.0
version: 0.3.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
@@ -665,7 +666,7 @@ importers:
version: 2.5.2
tailwindcss-animate:
specifier: ^1.0.7
version: 1.0.7(tailwindcss@3.4.13(ts-node@10.9.2(@types/node@20.10.5)(typescript@5.4.5)))
version: 1.0.7(tailwindcss@3.4.17(ts-node@10.9.2(@types/node@20.10.5)(typescript@5.4.5)))
use-query-params:
specifier: ^2.2.1
version: 2.2.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
@@ -694,7 +695,7 @@ importers:
version: 1.47.2
'@tailwindcss/forms':
specifier: ^0.5.7
version: 0.5.7(tailwindcss@3.4.13(ts-node@10.9.2(@types/node@20.10.5)(typescript@5.4.5)))
version: 0.5.7(tailwindcss@3.4.17(ts-node@10.9.2(@types/node@20.10.5)(typescript@5.4.5)))
'@testing-library/jest-dom':
specifier: ^6.4.6
version: 6.4.6(@jest/globals@29.7.0)(@types/jest@29.5.12)(jest@29.7.0(@types/node@20.10.5)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.10.5)(typescript@5.4.5)))(vitest@2.1.2(@types/node@20.10.5)(jsdom@20.0.3)(msw@2.6.5(@types/node@20.10.5)(typescript@5.4.5))(terser@5.36.0))
@@ -742,7 +743,7 @@ importers:
version: 7.12.0(eslint@8.57.0)(typescript@5.4.5)
autoprefixer:
specifier: ^10.4.19
version: 10.4.19(postcss@8.4.47)
version: 10.4.19(postcss@8.4.49)
dotenv-cli:
specifier: ^7.4.2
version: 7.4.2
@@ -761,9 +762,6 @@ importers:
node-mocks-http:
specifier: ^1.14.1
version: 1.14.1
postcss:
specifier: ^8.4.47
version: 8.4.47
prettier:
specifier: ^3.3.3
version: 3.3.3
@@ -771,8 +769,8 @@ importers:
specifier: ^0.6.6
version: 0.6.6(prettier@3.3.3)
tailwindcss:
specifier: ^3.4.13
version: 3.4.13(ts-node@10.9.2(@types/node@20.10.5)(typescript@5.4.5))
specifier: ^3.4.17
version: 3.4.17(ts-node@10.9.2(@types/node@20.10.5)(typescript@5.4.5))
ts-node:
specifier: ^10.9.2
version: 10.9.2(@types/node@20.10.5)(typescript@5.4.5)
@@ -796,7 +794,7 @@ importers:
version: 0.0.4
'@appsignal/opentelemetry-instrumentation-bullmq':
specifier: ^0.7.3
version: 0.7.3(bullmq@5.12.10)
version: 0.7.3(bullmq@5.34.10)
'@clickhouse/client':
specifier: ^1.4.0
version: 1.4.0
@@ -846,11 +844,14 @@ importers:
specifier: ^2.5.0
version: 2.5.0
bullmq:
specifier: ^5.12.10
version: 5.12.10
specifier: ^5.34.10
version: 5.34.10
cors:
specifier: ^2.8.5
version: 2.8.5
csv-parse:
specifier: ^5.6.0
version: 5.6.0
dd-trace:
specifier: ^5.23.1
version: 5.23.1
@@ -2644,62 +2645,62 @@ packages:
'@prisma/client': '>=2.26.0 || >=3'
next-auth: ^4
'@next/env@14.2.15':
resolution: {integrity: sha512-S1qaj25Wru2dUpcIZMjxeMVSwkt8BK4dmWHHiBuRstcIyOsMapqT4A4jSB6onvqeygkSSmOkyny9VVx8JIGamQ==}
'@next/env@14.2.21':
resolution: {integrity: sha512-lXcwcJd5oR01tggjWJ6SrNNYFGuOOMB9c251wUNkjCpkoXOPkDeF/15c3mnVlBqrW4JJXb2kVxDFhC4GduJt2A==}
'@next/eslint-plugin-next@14.2.15':
resolution: {integrity: sha512-pKU0iqKRBlFB/ocOI1Ip2CkKePZpYpnw5bEItEkuZ/Nr9FQP1+p7VDWr4VfOdff4i9bFmrOaeaU1bFEyAcxiMQ==}
'@next/swc-darwin-arm64@14.2.15':
resolution: {integrity: sha512-Rvh7KU9hOUBnZ9TJ28n2Oa7dD9cvDBKua9IKx7cfQQ0GoYUwg9ig31O2oMwH3wm+pE3IkAQ67ZobPfEgurPZIA==}
'@next/swc-darwin-arm64@14.2.21':
resolution: {integrity: sha512-HwEjcKsXtvszXz5q5Z7wCtrHeTTDSTgAbocz45PHMUjU3fBYInfvhR+ZhavDRUYLonm53aHZbB09QtJVJj8T7g==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [darwin]
'@next/swc-darwin-x64@14.2.15':
resolution: {integrity: sha512-5TGyjFcf8ampZP3e+FyCax5zFVHi+Oe7sZyaKOngsqyaNEpOgkKB3sqmymkZfowy3ufGA/tUgDPPxpQx931lHg==}
'@next/swc-darwin-x64@14.2.21':
resolution: {integrity: sha512-TSAA2ROgNzm4FhKbTbyJOBrsREOMVdDIltZ6aZiKvCi/v0UwFmwigBGeqXDA97TFMpR3LNNpw52CbVelkoQBxA==}
engines: {node: '>= 10'}
cpu: [x64]
os: [darwin]
'@next/swc-linux-arm64-gnu@14.2.15':
resolution: {integrity: sha512-3Bwv4oc08ONiQ3FiOLKT72Q+ndEMyLNsc/D3qnLMbtUYTQAmkx9E/JRu0DBpHxNddBmNT5hxz1mYBphJ3mfrrw==}
'@next/swc-linux-arm64-gnu@14.2.21':
resolution: {integrity: sha512-0Dqjn0pEUz3JG+AImpnMMW/m8hRtl1GQCNbO66V1yp6RswSTiKmnHf3pTX6xMdJYSemf3O4Q9ykiL0jymu0TuA==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
'@next/swc-linux-arm64-musl@14.2.15':
resolution: {integrity: sha512-k5xf/tg1FBv/M4CMd8S+JL3uV9BnnRmoe7F+GWC3DxkTCD9aewFRH1s5rJ1zkzDa+Do4zyN8qD0N8c84Hu96FQ==}
'@next/swc-linux-arm64-musl@14.2.21':
resolution: {integrity: sha512-Ggfw5qnMXldscVntwnjfaQs5GbBbjioV4B4loP+bjqNEb42fzZlAaK+ldL0jm2CTJga9LynBMhekNfV8W4+HBw==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
'@next/swc-linux-x64-gnu@14.2.15':
resolution: {integrity: sha512-kE6q38hbrRbKEkkVn62reLXhThLRh6/TvgSP56GkFNhU22TbIrQDEMrO7j0IcQHcew2wfykq8lZyHFabz0oBrA==}
'@next/swc-linux-x64-gnu@14.2.21':
resolution: {integrity: sha512-uokj0lubN1WoSa5KKdThVPRffGyiWlm/vCc/cMkWOQHw69Qt0X1o3b2PyLLx8ANqlefILZh1EdfLRz9gVpG6tg==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
'@next/swc-linux-x64-musl@14.2.15':
resolution: {integrity: sha512-PZ5YE9ouy/IdO7QVJeIcyLn/Rc4ml9M2G4y3kCM9MNf1YKvFY4heg3pVa/jQbMro+tP6yc4G2o9LjAz1zxD7tQ==}
'@next/swc-linux-x64-musl@14.2.21':
resolution: {integrity: sha512-iAEBPzWNbciah4+0yI4s7Pce6BIoxTQ0AGCkxn/UBuzJFkYyJt71MadYQkjPqCQCJAFQ26sYh7MOKdU+VQFgPg==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
'@next/swc-win32-arm64-msvc@14.2.15':
resolution: {integrity: sha512-2raR16703kBvYEQD9HNLyb0/394yfqzmIeyp2nDzcPV4yPjqNUG3ohX6jX00WryXz6s1FXpVhsCo3i+g4RUX+g==}
'@next/swc-win32-arm64-msvc@14.2.21':
resolution: {integrity: sha512-plykgB3vL2hB4Z32W3ktsfqyuyGAPxqwiyrAi2Mr8LlEUhNn9VgkiAl5hODSBpzIfWweX3er1f5uNpGDygfQVQ==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [win32]
'@next/swc-win32-ia32-msvc@14.2.15':
resolution: {integrity: sha512-fyTE8cklgkyR1p03kJa5zXEaZ9El+kDNM5A+66+8evQS5e/6v0Gk28LqA0Jet8gKSOyP+OTm/tJHzMlGdQerdQ==}
'@next/swc-win32-ia32-msvc@14.2.21':
resolution: {integrity: sha512-w5bacz4Vxqrh06BjWgua3Yf7EMDb8iMcVhNrNx8KnJXt8t+Uu0Zg4JHLDL/T7DkTCEEfKXO/Er1fcfWxn2xfPA==}
engines: {node: '>= 10'}
cpu: [ia32]
os: [win32]
'@next/swc-win32-x64-msvc@14.2.15':
resolution: {integrity: sha512-SzqGbsLsP9OwKNUG9nekShTwhj6JSB9ZLMWQ8g1gG6hdE5gQLncbnbymrwy2yVmH9nikSLYRYxYMFu78Ggp7/g==}
'@next/swc-win32-x64-msvc@14.2.21':
resolution: {integrity: sha512-sT6+llIkzpsexGYZq8cjjthRyRGe5cJVhqh12FmlbxHqna6zsDDK8UNaV7g41T6atFHCJUPeLb3uyAwrBwy0NA==}
engines: {node: '>= 10'}
cpu: [x64]
os: [win32]
@@ -5721,8 +5722,8 @@ packages:
builtins@5.1.0:
resolution: {integrity: sha512-SW9lzGTLvWTP1AY8xeAMZimqDrIaSdLQUcVr9DMef51niJ022Ri87SwRRKYm4A6iHfkPaiVUu/Duw2Wc4J7kKg==}
bullmq@5.12.10:
resolution: {integrity: sha512-lXH8Caj+FvYHiBS0QBEpQOq57RcVuEPziBC5cBlWguCVNfn1UMSri22bRrynDKuof8o9XB43ctmYASUpoa0DeQ==}
bullmq@5.34.10:
resolution: {integrity: sha512-ia6EzpQm1ZPq6GUBSLyfvzJrhdBTd1f3Gn2g9pFtLX4hBOob6QHmcmBzGgPlSCyr/i2Qfe4OdjS21bRd02srbw==}
bundle-name@4.1.0:
resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==}
@@ -5775,9 +5776,6 @@ packages:
caniuse-lite@1.0.30001599:
resolution: {integrity: sha512-LRAQHZ4yT1+f9LemSMeqdMpMxZcc4RMWdj4tiFe3G8tNkWK+E58g+/tzotb5cU6TbcVJLr4fySiAW7XmxQvZQA==}
caniuse-lite@1.0.30001667:
resolution: {integrity: sha512-7LTwJjcRkzKFmtqGsibMeuXmvFDfZq/nzIjnmgCGzKKRVzjD72selLDK1oPF/Oxzmt4fNcPvTDvGqSDG4tCALw==}
caniuse-lite@1.0.30001680:
resolution: {integrity: sha512-rPQy70G6AGUMnbwS1z6Xg+RkHYPAi18ihs47GH0jcxIG7wArmPgY3XbS2sRdBbxJljp3thdT8BIqv9ccCypiPA==}
@@ -6178,6 +6176,9 @@ packages:
csstype@3.1.3:
resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
csv-parse@5.6.0:
resolution: {integrity: sha512-l3nz3euub2QMg5ouu5U09Ew9Wf6/wQ8I++ch1loQ0ljmzhmfZYrH9fflS22i/PQEvsPvxCwxgz5q7UB8K1JO4Q==}
cytoscape-cose-bilkent@4.1.0:
resolution: {integrity: sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ==}
peerDependencies:
@@ -8204,8 +8205,8 @@ packages:
node-notifier:
optional: true
jiti@1.21.0:
resolution: {integrity: sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q==}
jiti@1.21.7:
resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==}
hasBin: true
jju@1.4.0:
@@ -8495,8 +8496,8 @@ packages:
resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==}
engines: {node: '>=10'}
lilconfig@3.1.1:
resolution: {integrity: sha512-O18pf7nyvHTckunPWCV1XUNXU1piu01y2b7ATJ0ppkUkk8ocqVWBrYjJBCwHDjD/ZWcfyrA0P4gKhzWGi5EINQ==}
lilconfig@3.1.3:
resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==}
engines: {node: '>=14'}
limiter@1.1.5:
@@ -8951,8 +8952,8 @@ packages:
resolution: {integrity: sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA==}
hasBin: true
msgpackr@1.10.1:
resolution: {integrity: sha512-r5VRLv9qouXuLiIBrLpl2d5ZvPt8svdQTl5/vMvE4nzDMyEX4sgW5yWhuBBj5UmgwOTWj8CIdSXn5sAfsHAWIQ==}
msgpackr@1.11.2:
resolution: {integrity: sha512-F9UngXRlPyWCDEASDpTf6c9uNhGPTqnTeLVt7bN+bU1eajoR/8V9ys2BRaV5C/e5ihE6sJ9uPIKaYt6bFuO32g==}
msw@2.6.5:
resolution: {integrity: sha512-PnlnTpUlOrj441kYQzzFhzMzMCGFT6a2jKUBG7zSpLkYS5oh8Arrbc0dL8/rNAtxaoBy0EVs2mFqj2qdmWK7lQ==}
@@ -8979,13 +8980,8 @@ packages:
mz@2.7.0:
resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==}
nanoid@3.3.6:
resolution: {integrity: sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==}
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
hasBin: true
nanoid@3.3.7:
resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==}
nanoid@3.3.8:
resolution: {integrity: sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==}
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
hasBin: true
@@ -9038,8 +9034,8 @@ packages:
react: ^16.8 || ^17 || ^18
react-dom: ^16.8 || ^17 || ^18
next@14.2.15:
resolution: {integrity: sha512-h9ctmOokpoDphRvMGnwOJAedT6zKhwqyZML9mDtspgf4Rh3Pn7UTYKqePNoDvhsWBAO5GoPNYshnAUGIazVGmw==}
next@14.2.21:
resolution: {integrity: sha512-rZmLwucLHr3/zfDMYbJXbw0ZeoBpirxkXuvsJbk7UPorvPYZhP7vq7aHbKnU7dQNCYIimRrbB2pp3xmf+wsYUg==}
engines: {node: '>=18.17.0'}
hasBin: true
peerDependencies:
@@ -9521,9 +9517,6 @@ packages:
picocolors@1.0.0:
resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==}
picocolors@1.1.0:
resolution: {integrity: sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==}
picocolors@1.1.1:
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
@@ -9598,14 +9591,14 @@ packages:
ts-node:
optional: true
postcss-nested@6.0.1:
resolution: {integrity: sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ==}
postcss-nested@6.2.0:
resolution: {integrity: sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==}
engines: {node: '>=12.0'}
peerDependencies:
postcss: ^8.2.14
postcss-selector-parser@6.0.16:
resolution: {integrity: sha512-A0RVJrX+IUkVZbW3ClroRWurercFhieevHB38sr2+l9eUClMqome3LmEmnhlNy+5Mr2EYN6B2Kaw9wYdd+VHiw==}
postcss-selector-parser@6.1.2:
resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==}
engines: {node: '>=4'}
postcss-value-parser@4.2.0:
@@ -9615,10 +9608,6 @@ packages:
resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==}
engines: {node: ^10 || ^12 || >=14}
postcss@8.4.47:
resolution: {integrity: sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==}
engines: {node: ^10 || ^12 || >=14}
postcss@8.4.49:
resolution: {integrity: sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==}
engines: {node: ^10 || ^12 || >=14}
@@ -10740,8 +10729,8 @@ packages:
peerDependencies:
tailwindcss: '>=3.0.0 || insiders'
tailwindcss@3.4.13:
resolution: {integrity: sha512-KqjHOJKogOUt5Bs752ykCeiwvi0fKVkr5oqsFNt/8px/tA8scFPIlkygsf6jXrfCqGHz7VflA6+yytWuM+XhFw==}
tailwindcss@3.4.17:
resolution: {integrity: sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==}
engines: {node: '>=14.0.0'}
hasBin: true
@@ -11578,7 +11567,7 @@ snapshots:
dependencies:
'@ai-sdk/provider': 0.0.24
eventsource-parser: 1.1.2
nanoid: 3.3.6
nanoid: 3.3.8
secure-json-parse: 2.7.0
optionalDependencies:
zod: 3.23.8
@@ -11668,14 +11657,14 @@ snapshots:
'@types/node': 18.19.25
tiktoken: 1.0.15
'@appsignal/opentelemetry-instrumentation-bullmq@0.7.3(bullmq@5.12.10)':
'@appsignal/opentelemetry-instrumentation-bullmq@0.7.3(bullmq@5.34.10)':
dependencies:
'@opentelemetry/api': 1.9.0
'@opentelemetry/instrumentation': 0.52.1(@opentelemetry/api@1.9.0)
'@opentelemetry/semantic-conventions': 1.27.0
flat: 5.0.2
optionalDependencies:
bullmq: 5.12.10
bullmq: 5.34.10
transitivePeerDependencies:
- supports-color
@@ -12869,7 +12858,7 @@ snapshots:
'@babel/code-frame@7.24.2':
dependencies:
'@babel/highlight': 7.24.5
picocolors: 1.1.0
picocolors: 1.1.1
'@babel/code-frame@7.26.2':
dependencies:
@@ -12997,7 +12986,7 @@ snapshots:
'@babel/helper-validator-identifier': 7.24.5
chalk: 2.4.2
js-tokens: 4.0.0
picocolors: 1.1.0
picocolors: 1.1.1
'@babel/parser@7.24.1':
dependencies:
@@ -13708,9 +13697,9 @@ snapshots:
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
'@headlessui/tailwindcss@0.2.1(tailwindcss@3.4.13(ts-node@10.9.2(@types/node@20.10.5)(typescript@5.4.5)))':
'@headlessui/tailwindcss@0.2.1(tailwindcss@3.4.17(ts-node@10.9.2(@types/node@20.10.5)(typescript@5.4.5)))':
dependencies:
tailwindcss: 3.4.13(ts-node@10.9.2(@types/node@20.10.5)(typescript@5.4.5))
tailwindcss: 3.4.17(ts-node@10.9.2(@types/node@20.10.5)(typescript@5.4.5))
'@heroicons/react@2.1.5(react@18.2.0)':
dependencies:
@@ -14370,42 +14359,42 @@ snapshots:
transitivePeerDependencies:
- '@types/react'
'@next-auth/prisma-adapter@1.0.7(@prisma/client@5.22.0(prisma@5.22.0))(next-auth@4.24.11(next@14.2.15(@babel/core@7.24.3)(@opentelemetry/api@1.9.0)(@playwright/test@1.47.2)(babel-plugin-macros@3.1.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(nodemailer@6.9.15)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))':
'@next-auth/prisma-adapter@1.0.7(@prisma/client@5.22.0(prisma@5.22.0))(next-auth@4.24.11(next@14.2.21(@babel/core@7.24.3)(@opentelemetry/api@1.9.0)(@playwright/test@1.47.2)(babel-plugin-macros@3.1.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(nodemailer@6.9.15)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))':
dependencies:
'@prisma/client': 5.22.0(prisma@5.22.0)
next-auth: 4.24.11(next@14.2.15(@babel/core@7.24.3)(@opentelemetry/api@1.9.0)(@playwright/test@1.47.2)(babel-plugin-macros@3.1.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(nodemailer@6.9.15)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
next-auth: 4.24.11(next@14.2.21(@babel/core@7.24.3)(@opentelemetry/api@1.9.0)(@playwright/test@1.47.2)(babel-plugin-macros@3.1.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(nodemailer@6.9.15)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
'@next/env@14.2.15': {}
'@next/env@14.2.21': {}
'@next/eslint-plugin-next@14.2.15':
dependencies:
glob: 10.3.10
'@next/swc-darwin-arm64@14.2.15':
'@next/swc-darwin-arm64@14.2.21':
optional: true
'@next/swc-darwin-x64@14.2.15':
'@next/swc-darwin-x64@14.2.21':
optional: true
'@next/swc-linux-arm64-gnu@14.2.15':
'@next/swc-linux-arm64-gnu@14.2.21':
optional: true
'@next/swc-linux-arm64-musl@14.2.15':
'@next/swc-linux-arm64-musl@14.2.21':
optional: true
'@next/swc-linux-x64-gnu@14.2.15':
'@next/swc-linux-x64-gnu@14.2.21':
optional: true
'@next/swc-linux-x64-musl@14.2.15':
'@next/swc-linux-x64-musl@14.2.21':
optional: true
'@next/swc-win32-arm64-msvc@14.2.15':
'@next/swc-win32-arm64-msvc@14.2.21':
optional: true
'@next/swc-win32-ia32-msvc@14.2.15':
'@next/swc-win32-ia32-msvc@14.2.21':
optional: true
'@next/swc-win32-x64-msvc@14.2.15':
'@next/swc-win32-x64-msvc@14.2.21':
optional: true
'@nicolo-ribaudo/eslint-scope-5-internals@5.1.1-v1':
@@ -16254,7 +16243,7 @@ snapshots:
'@sentry/types': 8.39.0
'@sentry/utils': 8.39.0
'@sentry/nextjs@8.39.0(@opentelemetry/core@1.26.0(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.53.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.26.0(@opentelemetry/api@1.9.0))(next@14.2.15(@babel/core@7.24.3)(@opentelemetry/api@1.9.0)(@playwright/test@1.47.2)(babel-plugin-macros@3.1.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react@18.2.0)(webpack@5.95.0)':
'@sentry/nextjs@8.39.0(@opentelemetry/core@1.26.0(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.53.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.26.0(@opentelemetry/api@1.9.0))(next@14.2.21(@babel/core@7.24.3)(@opentelemetry/api@1.9.0)(@playwright/test@1.47.2)(babel-plugin-macros@3.1.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react@18.2.0)(webpack@5.95.0)':
dependencies:
'@opentelemetry/api': 1.9.0
'@opentelemetry/instrumentation-http': 0.53.0(@opentelemetry/api@1.9.0)
@@ -16270,7 +16259,7 @@ snapshots:
'@sentry/vercel-edge': 8.39.0
'@sentry/webpack-plugin': 2.22.6(webpack@5.95.0)
chalk: 3.0.0
next: 14.2.15(@babel/core@7.24.3)(@opentelemetry/api@1.9.0)(@playwright/test@1.47.2)(babel-plugin-macros@3.1.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
next: 14.2.21(@babel/core@7.24.3)(@opentelemetry/api@1.9.0)(@playwright/test@1.47.2)(babel-plugin-macros@3.1.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
resolve: 1.22.8
rollup: 3.29.5
stacktrace-parser: 0.1.10
@@ -16760,14 +16749,14 @@ snapshots:
optionalDependencies:
typescript: 5.4.5
'@tailwindcss/container-queries@0.1.1(tailwindcss@3.4.13(ts-node@10.9.2(@types/node@20.10.5)(typescript@5.4.5)))':
'@tailwindcss/container-queries@0.1.1(tailwindcss@3.4.17(ts-node@10.9.2(@types/node@20.10.5)(typescript@5.4.5)))':
dependencies:
tailwindcss: 3.4.13(ts-node@10.9.2(@types/node@20.10.5)(typescript@5.4.5))
tailwindcss: 3.4.17(ts-node@10.9.2(@types/node@20.10.5)(typescript@5.4.5))
'@tailwindcss/forms@0.5.7(tailwindcss@3.4.13(ts-node@10.9.2(@types/node@20.10.5)(typescript@5.4.5)))':
'@tailwindcss/forms@0.5.7(tailwindcss@3.4.17(ts-node@10.9.2(@types/node@20.10.5)(typescript@5.4.5)))':
dependencies:
mini-svg-data-uri: 1.4.4
tailwindcss: 3.4.13(ts-node@10.9.2(@types/node@20.10.5)(typescript@5.4.5))
tailwindcss: 3.4.17(ts-node@10.9.2(@types/node@20.10.5)(typescript@5.4.5))
'@tanstack/query-core@4.36.1': {}
@@ -16836,11 +16825,11 @@ snapshots:
'@tootallnate/quickjs-emscripten@0.23.0': {}
'@tremor/react@3.16.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(tailwindcss@3.4.13(ts-node@10.9.2(@types/node@20.10.5)(typescript@5.4.5)))':
'@tremor/react@3.16.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(tailwindcss@3.4.17(ts-node@10.9.2(@types/node@20.10.5)(typescript@5.4.5)))':
dependencies:
'@floating-ui/react': 0.19.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
'@headlessui/react': 1.7.19(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
'@headlessui/tailwindcss': 0.2.1(tailwindcss@3.4.13(ts-node@10.9.2(@types/node@20.10.5)(typescript@5.4.5)))
'@headlessui/tailwindcss': 0.2.1(tailwindcss@3.4.17(ts-node@10.9.2(@types/node@20.10.5)(typescript@5.4.5)))
date-fns: 2.30.0
react: 18.2.0
react-day-picker: 8.10.1(date-fns@2.30.0)(react@18.2.0)
@@ -16855,13 +16844,13 @@ snapshots:
dependencies:
'@trpc/server': 10.45.2
'@trpc/next@10.45.2(@tanstack/react-query@4.36.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@trpc/client@10.45.2(@trpc/server@10.45.2))(@trpc/react-query@10.45.2(@tanstack/react-query@4.36.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@trpc/client@10.45.2(@trpc/server@10.45.2))(@trpc/server@10.45.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@trpc/server@10.45.2)(next@14.2.15(@babel/core@7.24.3)(@opentelemetry/api@1.9.0)(@playwright/test@1.47.2)(babel-plugin-macros@3.1.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
'@trpc/next@10.45.2(@tanstack/react-query@4.36.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@trpc/client@10.45.2(@trpc/server@10.45.2))(@trpc/react-query@10.45.2(@tanstack/react-query@4.36.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@trpc/client@10.45.2(@trpc/server@10.45.2))(@trpc/server@10.45.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@trpc/server@10.45.2)(next@14.2.21(@babel/core@7.24.3)(@opentelemetry/api@1.9.0)(@playwright/test@1.47.2)(babel-plugin-macros@3.1.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
dependencies:
'@tanstack/react-query': 4.36.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
'@trpc/client': 10.45.2(@trpc/server@10.45.2)
'@trpc/react-query': 10.45.2(@tanstack/react-query@4.36.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@trpc/client@10.45.2(@trpc/server@10.45.2))(@trpc/server@10.45.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
'@trpc/server': 10.45.2
next: 14.2.15(@babel/core@7.24.3)(@opentelemetry/api@1.9.0)(@playwright/test@1.47.2)(babel-plugin-macros@3.1.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
next: 14.2.21(@babel/core@7.24.3)(@opentelemetry/api@1.9.0)(@playwright/test@1.47.2)(babel-plugin-macros@3.1.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
@@ -17772,7 +17761,7 @@ snapshots:
eventsource-parser: 1.1.2
json-schema: 0.4.0
jsondiffpatch: 0.6.0
nanoid: 3.3.6
nanoid: 3.3.8
secure-json-parse: 2.7.0
zod-to-json-schema: 3.23.2(zod@3.23.8)
optionalDependencies:
@@ -17985,14 +17974,14 @@ snapshots:
asynckit@0.4.0: {}
autoprefixer@10.4.19(postcss@8.4.47):
autoprefixer@10.4.19(postcss@8.4.49):
dependencies:
browserslist: 4.23.0
caniuse-lite: 1.0.30001599
fraction.js: 4.3.7
normalize-range: 0.1.2
picocolors: 1.0.0
postcss: 8.4.47
postcss: 8.4.49
postcss-value-parser: 4.2.0
available-typed-arrays@1.0.7:
@@ -18145,14 +18134,14 @@ snapshots:
browserslist@4.23.0:
dependencies:
caniuse-lite: 1.0.30001667
caniuse-lite: 1.0.30001680
electron-to-chromium: 1.4.710
node-releases: 2.0.14
update-browserslist-db: 1.0.13(browserslist@4.23.0)
browserslist@4.24.0:
dependencies:
caniuse-lite: 1.0.30001667
caniuse-lite: 1.0.30001680
electron-to-chromium: 1.5.33
node-releases: 2.0.18
update-browserslist-db: 1.1.1(browserslist@4.24.0)
@@ -18190,11 +18179,11 @@ snapshots:
dependencies:
semver: 7.6.3
bullmq@5.12.10:
bullmq@5.34.10:
dependencies:
cron-parser: 4.9.0
ioredis: 5.4.1
msgpackr: 1.10.1
msgpackr: 1.11.2
node-abort-controller: 3.1.1
semver: 7.6.3
tslib: 2.6.3
@@ -18246,8 +18235,6 @@ snapshots:
caniuse-lite@1.0.30001599: {}
caniuse-lite@1.0.30001667: {}
caniuse-lite@1.0.30001680: {}
ccount@2.0.1: {}
@@ -18678,6 +18665,8 @@ snapshots:
csstype@3.1.3: {}
csv-parse@5.6.0: {}
cytoscape-cose-bilkent@4.1.0(cytoscape@3.30.2):
dependencies:
cose-base: 1.0.3
@@ -21303,7 +21292,7 @@ snapshots:
- ts-node
optional: true
jiti@1.21.0: {}
jiti@1.21.7: {}
jju@1.4.0: {}
@@ -21582,7 +21571,7 @@ snapshots:
lilconfig@2.1.0: {}
lilconfig@3.1.1: {}
lilconfig@3.1.3: {}
limiter@1.1.5: {}
@@ -22237,7 +22226,7 @@ snapshots:
'@msgpackr-extract/msgpackr-extract-win32-x64': 3.0.3
optional: true
msgpackr@1.10.1:
msgpackr@1.11.2:
optionalDependencies:
msgpackr-extract: 3.0.3
@@ -22330,9 +22319,7 @@ snapshots:
object-assign: 4.1.1
thenify-all: 1.6.0
nanoid@3.3.6: {}
nanoid@3.3.7: {}
nanoid@3.3.8: {}
natural-compare@1.4.0: {}
@@ -22348,13 +22335,13 @@ snapshots:
dependencies:
type-fest: 2.19.0
next-auth@4.24.11(next@14.2.15(@babel/core@7.24.3)(@opentelemetry/api@1.9.0)(@playwright/test@1.47.2)(babel-plugin-macros@3.1.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(nodemailer@6.9.15)(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
next-auth@4.24.11(next@14.2.21(@babel/core@7.24.3)(@opentelemetry/api@1.9.0)(@playwright/test@1.47.2)(babel-plugin-macros@3.1.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(nodemailer@6.9.15)(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
dependencies:
'@babel/runtime': 7.26.0
'@panva/hkdf': 1.1.1
cookie: 0.7.2
jose: 4.15.5
next: 14.2.15(@babel/core@7.24.3)(@opentelemetry/api@1.9.0)(@playwright/test@1.47.2)(babel-plugin-macros@3.1.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
next: 14.2.21(@babel/core@7.24.3)(@opentelemetry/api@1.9.0)(@playwright/test@1.47.2)(babel-plugin-macros@3.1.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
oauth: 0.9.15
openid-client: 5.6.5
preact: 10.19.7
@@ -22365,9 +22352,9 @@ snapshots:
optionalDependencies:
nodemailer: 6.9.15
next-query-params@5.0.1(next@14.2.15(@babel/core@7.24.3)(@opentelemetry/api@1.9.0)(@playwright/test@1.47.2)(babel-plugin-macros@3.1.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react@18.2.0)(use-query-params@2.2.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0)):
next-query-params@5.0.1(next@14.2.21(@babel/core@7.24.3)(@opentelemetry/api@1.9.0)(@playwright/test@1.47.2)(babel-plugin-macros@3.1.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react@18.2.0)(use-query-params@2.2.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0)):
dependencies:
next: 14.2.15(@babel/core@7.24.3)(@opentelemetry/api@1.9.0)(@playwright/test@1.47.2)(babel-plugin-macros@3.1.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
next: 14.2.21(@babel/core@7.24.3)(@opentelemetry/api@1.9.0)(@playwright/test@1.47.2)(babel-plugin-macros@3.1.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
react: 18.2.0
tslib: 2.6.3
use-query-params: 2.2.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
@@ -22377,27 +22364,27 @@ snapshots:
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
next@14.2.15(@babel/core@7.24.3)(@opentelemetry/api@1.9.0)(@playwright/test@1.47.2)(babel-plugin-macros@3.1.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
next@14.2.21(@babel/core@7.24.3)(@opentelemetry/api@1.9.0)(@playwright/test@1.47.2)(babel-plugin-macros@3.1.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
dependencies:
'@next/env': 14.2.15
'@next/env': 14.2.21
'@swc/helpers': 0.5.5
busboy: 1.6.0
caniuse-lite: 1.0.30001667
caniuse-lite: 1.0.30001680
graceful-fs: 4.2.11
postcss: 8.4.31
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
styled-jsx: 5.1.1(@babel/core@7.24.3)(babel-plugin-macros@3.1.0)(react@18.2.0)
optionalDependencies:
'@next/swc-darwin-arm64': 14.2.15
'@next/swc-darwin-x64': 14.2.15
'@next/swc-linux-arm64-gnu': 14.2.15
'@next/swc-linux-arm64-musl': 14.2.15
'@next/swc-linux-x64-gnu': 14.2.15
'@next/swc-linux-x64-musl': 14.2.15
'@next/swc-win32-arm64-msvc': 14.2.15
'@next/swc-win32-ia32-msvc': 14.2.15
'@next/swc-win32-x64-msvc': 14.2.15
'@next/swc-darwin-arm64': 14.2.21
'@next/swc-darwin-x64': 14.2.21
'@next/swc-linux-arm64-gnu': 14.2.21
'@next/swc-linux-arm64-musl': 14.2.21
'@next/swc-linux-x64-gnu': 14.2.21
'@next/swc-linux-x64-musl': 14.2.21
'@next/swc-win32-arm64-msvc': 14.2.21
'@next/swc-win32-ia32-msvc': 14.2.21
'@next/swc-win32-x64-msvc': 14.2.21
'@opentelemetry/api': 1.9.0
'@playwright/test': 1.47.2
transitivePeerDependencies:
@@ -22899,8 +22886,6 @@ snapshots:
picocolors@1.0.0: {}
picocolors@1.1.0: {}
picocolors@1.1.1: {}
picomatch@2.3.1: {}
@@ -22940,32 +22925,32 @@ snapshots:
possible-typed-array-names@1.0.0: {}
postcss-import@15.1.0(postcss@8.4.47):
postcss-import@15.1.0(postcss@8.4.49):
dependencies:
postcss: 8.4.47
postcss: 8.4.49
postcss-value-parser: 4.2.0
read-cache: 1.0.0
resolve: 1.22.8
postcss-js@4.0.1(postcss@8.4.47):
postcss-js@4.0.1(postcss@8.4.49):
dependencies:
camelcase-css: 2.0.1
postcss: 8.4.47
postcss: 8.4.49
postcss-load-config@4.0.2(postcss@8.4.47)(ts-node@10.9.2(@types/node@20.10.5)(typescript@5.4.5)):
postcss-load-config@4.0.2(postcss@8.4.49)(ts-node@10.9.2(@types/node@20.10.5)(typescript@5.4.5)):
dependencies:
lilconfig: 3.1.1
lilconfig: 3.1.3
yaml: 2.5.1
optionalDependencies:
postcss: 8.4.47
postcss: 8.4.49
ts-node: 10.9.2(@types/node@20.10.5)(typescript@5.4.5)
postcss-nested@6.0.1(postcss@8.4.47):
postcss-nested@6.2.0(postcss@8.4.49):
dependencies:
postcss: 8.4.47
postcss-selector-parser: 6.0.16
postcss: 8.4.49
postcss-selector-parser: 6.1.2
postcss-selector-parser@6.0.16:
postcss-selector-parser@6.1.2:
dependencies:
cssesc: 3.0.0
util-deprecate: 1.0.2
@@ -22974,19 +22959,13 @@ snapshots:
postcss@8.4.31:
dependencies:
nanoid: 3.3.7
picocolors: 1.1.0
source-map-js: 1.2.1
postcss@8.4.47:
dependencies:
nanoid: 3.3.7
picocolors: 1.1.0
nanoid: 3.3.8
picocolors: 1.1.1
source-map-js: 1.2.1
postcss@8.4.49:
dependencies:
nanoid: 3.3.7
nanoid: 3.3.8
picocolors: 1.1.1
source-map-js: 1.2.1
@@ -24222,11 +24201,11 @@ snapshots:
tailwind-merge@2.5.2: {}
tailwindcss-animate@1.0.7(tailwindcss@3.4.13(ts-node@10.9.2(@types/node@20.10.5)(typescript@5.4.5))):
tailwindcss-animate@1.0.7(tailwindcss@3.4.17(ts-node@10.9.2(@types/node@20.10.5)(typescript@5.4.5))):
dependencies:
tailwindcss: 3.4.13(ts-node@10.9.2(@types/node@20.10.5)(typescript@5.4.5))
tailwindcss: 3.4.17(ts-node@10.9.2(@types/node@20.10.5)(typescript@5.4.5))
tailwindcss@3.4.13(ts-node@10.9.2(@types/node@20.10.5)(typescript@5.4.5)):
tailwindcss@3.4.17(ts-node@10.9.2(@types/node@20.10.5)(typescript@5.4.5)):
dependencies:
'@alloc/quick-lru': 5.2.0
arg: 5.0.2
@@ -24236,18 +24215,18 @@ snapshots:
fast-glob: 3.3.2
glob-parent: 6.0.2
is-glob: 4.0.3
jiti: 1.21.0
lilconfig: 2.1.0
jiti: 1.21.7
lilconfig: 3.1.3
micromatch: 4.0.8
normalize-path: 3.0.0
object-hash: 3.0.0
picocolors: 1.1.0
postcss: 8.4.47
postcss-import: 15.1.0(postcss@8.4.47)
postcss-js: 4.0.1(postcss@8.4.47)
postcss-load-config: 4.0.2(postcss@8.4.47)(ts-node@10.9.2(@types/node@20.10.5)(typescript@5.4.5))
postcss-nested: 6.0.1(postcss@8.4.47)
postcss-selector-parser: 6.0.16
picocolors: 1.1.1
postcss: 8.4.49
postcss-import: 15.1.0(postcss@8.4.49)
postcss-js: 4.0.1(postcss@8.4.49)
postcss-load-config: 4.0.2(postcss@8.4.49)(ts-node@10.9.2(@types/node@20.10.5)(typescript@5.4.5))
postcss-nested: 6.2.0(postcss@8.4.49)
postcss-selector-parser: 6.1.2
resolve: 1.22.8
sucrase: 3.35.0
transitivePeerDependencies:
@@ -24643,19 +24622,19 @@ snapshots:
dependencies:
browserslist: 4.23.0
escalade: 3.2.0
picocolors: 1.1.0
picocolors: 1.1.1
update-browserslist-db@1.1.1(browserslist@4.24.0):
dependencies:
browserslist: 4.24.0
escalade: 3.2.0
picocolors: 1.1.0
picocolors: 1.1.1
update-browserslist-db@1.1.1(browserslist@4.24.2):
dependencies:
browserslist: 4.24.2
escalade: 3.2.0
picocolors: 1.1.0
picocolors: 1.1.1
update-notifier@7.0.0:
dependencies:
@@ -24825,7 +24804,7 @@ snapshots:
vite@5.2.14(@types/node@20.10.5)(terser@5.36.0):
dependencies:
esbuild: 0.20.2
postcss: 8.4.47
postcss: 8.4.49
rollup: 4.13.0
optionalDependencies:
'@types/node': 20.10.5
@@ -24836,7 +24815,7 @@ snapshots:
vite@5.2.14(@types/node@20.11.29)(terser@5.36.0):
dependencies:
esbuild: 0.20.2
postcss: 8.4.47
postcss: 8.4.49
rollup: 4.13.0
optionalDependencies:
'@types/node': 20.11.29
@@ -24846,7 +24825,7 @@ snapshots:
vite@5.2.14(@types/node@20.14.8)(terser@5.36.0):
dependencies:
esbuild: 0.20.2
postcss: 8.4.47
postcss: 8.4.49
rollup: 4.13.0
optionalDependencies:
'@types/node': 20.14.8
+3
View File
@@ -95,6 +95,9 @@ ARG BUILDPLATFORM
WORKDIR /app
ARG NEXT_PUBLIC_BUILD_ID
ENV BUILD_ID=$NEXT_PUBLIC_BUILD_ID
ENV NODE_ENV production
# Uncomment the following line in case you want to disable telemetry during runtime.
ENV NEXT_TELEMETRY_DISABLED 1
+3 -3
View File
@@ -12,8 +12,8 @@ import { env } from "./src/env.mjs";
*/
const cspHeader = `
default-src 'self' https://*.langfuse.com https://*.langfuse.dev https://*.posthog.com https://*.sentry.io wss://*.crisp.chat https://*.crisp.chat;
script-src 'self' 'unsafe-eval' 'unsafe-inline' https://*.langfuse.com https://*.langfuse.dev https://client.crisp.chat https://settings.crisp.chat https://challenges.cloudflare.com https://*.sentry.io https://static.cloudflareinsights.com https://*.stripe.com;
style-src 'self' 'unsafe-inline' https://client.crisp.chat;
script-src 'self' 'unsafe-eval' 'unsafe-inline' https://*.langfuse.com https://*.langfuse.dev https://client.crisp.chat https://settings.crisp.chat https://challenges.cloudflare.com https://*.sentry.io https://static.cloudflareinsights.com https://*.stripe.com https://uptime.betterstack.com;
style-src 'self' 'unsafe-inline' https://client.crisp.chat https://uptime.betterstack.com;
img-src 'self' https: blob: data: http://localhost:* https://client.crisp.chat https://image.crisp.chat https://storage.crisp.chat;
font-src 'self' https://client.crisp.chat;
frame-src 'self' https://challenges.cloudflare.com https://*.stripe.com https://game.crisp.chat;
@@ -22,7 +22,7 @@ const cspHeader = `
base-uri 'self';
form-action 'self';
frame-ancestors 'none';
connect-src 'self' https://*.langfuse.com https://*.langfuse.dev https://client.crisp.chat https://storage.crisp.chat wss://client.relay.crisp.chat wss://stream.relay.crisp.chat https://*.ingest.us.sentry.io;
connect-src 'self' https://*.langfuse.com https://*.langfuse.dev https://client.crisp.chat https://storage.crisp.chat wss://client.relay.crisp.chat wss://stream.relay.crisp.chat https://*.ingest.us.sentry.io https://uptime.betterstack.com;
media-src 'self' https: http://localhost:* https://client.crisp.chat;
${env.LANGFUSE_CSP_ENFORCE_HTTPS === "true" ? "upgrade-insecure-requests; block-all-mixed-content;" : ""}
${env.SENTRY_CSP_REPORT_URI ? `report-uri ${env.SENTRY_CSP_REPORT_URI}; report-to csp-endpoint;` : ""}
+6 -6
View File
@@ -1,6 +1,6 @@
{
"name": "web",
"version": "3.2.0",
"version": "3.9.0",
"private": true,
"license": "MIT",
"engines": {
@@ -99,7 +99,7 @@
"@uiw/react-codemirror": "^4.21.25",
"ai": "^3.4.9",
"bcryptjs": "^2.4.3",
"bullmq": "^5.12.10",
"bullmq": "^5.34.10",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"cmdk": "^1.0.0",
@@ -117,7 +117,7 @@
"langchain": "^0.3.6",
"lodash": "^4.17.21",
"lucide-react": "^0.462.0",
"next": "^14.2.15",
"next": "^14.2.21",
"next-auth": "^4.24.11",
"next-query-params": "^5.0.1",
"next-themes": "^0.3.0",
@@ -175,10 +175,9 @@
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
"node-mocks-http": "^1.14.1",
"postcss": "^8.4.47",
"prettier": "^3.3.3",
"prettier-plugin-tailwindcss": "^0.6.6",
"tailwindcss": "^3.4.13",
"tailwindcss": "^3.4.17",
"ts-node": "^10.9.2",
"tsconfig-paths": "^4.2.0",
"tsx": "^4.19.1",
@@ -193,7 +192,8 @@
},
"pnpm": {
"overrides": {
"jsonpath-plus": "10.0.7"
"jsonpath-plus": "10.0.7",
"nanoid": "^3.3.8"
}
}
}
+6 -3
View File
@@ -723,6 +723,9 @@ paths:
Notes:
- Introduction to data model:
https://langfuse.com/docs/tracing-data-model
- Batch sizes are limited to 3.5 MB in total. You need to adjust the
number of events per batch accordingly.
@@ -3183,11 +3186,12 @@ components:
to exact match, use `(?i)^modelname$`
startDate:
type: string
format: date
format: date-time
nullable: true
description: Apply only to generations which are newer than this ISO date.
unit:
$ref: '#/components/schemas/ModelUsageUnit'
nullable: true
description: Unit used by this model.
inputPrice:
type: number
@@ -3223,7 +3227,6 @@ components:
- id
- modelName
- matchPattern
- unit
- isLangfuseManaged
ModelUsageUnit:
title: ModelUsageUnit
@@ -4276,6 +4279,7 @@ components:
description: Apply only to generations which are newer than this ISO date.
unit:
$ref: '#/components/schemas/ModelUsageUnit'
nullable: true
description: Unit used by this model.
inputPrice:
type: number
@@ -4308,7 +4312,6 @@ components:
required:
- modelName
- matchPattern
- unit
Observations:
title: Observations
type: object
+1 -1
View File
@@ -558,7 +558,7 @@
"_type": "endpoint",
"name": "Batch",
"request": {
"description": "Batched ingestion for Langfuse Tracing. If you want to use tracing via the API, such as to build your own Langfuse client implementation, this is the only API route you need to implement.\n\nNotes:\n\n- Batch sizes are limited to 3.5 MB in total. You need to adjust the number of events per batch accordingly.\n- The API does not return a 4xx status code for input errors. Instead, it responds with a 207 status code, which includes a list of the encountered errors.",
"description": "Batched ingestion for Langfuse Tracing. If you want to use tracing via the API, such as to build your own Langfuse client implementation, this is the only API route you need to implement.\n\nNotes:\n\n- Introduction to data model: https://langfuse.com/docs/tracing-data-model\n- Batch sizes are limited to 3.5 MB in total. You need to adjust the number of events per batch accordingly.\n- The API does not return a 4xx status code for input errors. Instead, it responds with a 207 status code, which includes a list of the encountered errors.",
"url": {
"raw": "{{baseUrl}}/api/public/ingestion",
"host": [
+1 -1
View File
@@ -123,7 +123,7 @@ test("Unauthenticated user should not be redirected to non-relative URLs after l
test("Unauthenticated user should be redirected to relative URL after login", async ({
page,
}) => {
const relativeUrl = "/support";
const relativeUrl = "/setup";
await page.goto(
`/auth/sign-in?targetPath=${encodeURIComponent(relativeUrl)}`,
);
@@ -244,7 +244,7 @@ describe("Fetch datasets for UI presentation", () => {
expect(JSON.stringify(secondRun.scores)).toEqual(JSON.stringify({}));
});
it.only("should test that dataset runs can link to the same traces", async () => {
it("should test that dataset runs can link to the same traces", async () => {
const datasetId = v4();
await prisma.dataset.create({
@@ -215,6 +215,102 @@ describe("/api/public/datasets and /api/public/dataset-items API Endpoints", ()
expect(getDataset.body.items[0].id).toEqual("active-item-id");
});
it("should correctly update dataset items", async () => {
const datasetItemId = v4();
const datasetName = v4();
await prisma.dataset.create({
data: {
name: datasetName,
projectId: projectId,
},
});
await makeZodVerifiedAPICall(
PostDatasetItemsV1Response,
"POST",
"/api/public/dataset-items",
{
datasetName: datasetName,
id: datasetItemId,
input: { key: "value" },
expectedOutput: { key: "value" },
metadata: null,
sourceTraceId: null,
sourceObservationId: null,
status: null,
},
auth,
);
await makeZodVerifiedAPICall(
PostDatasetItemsV1Response,
"POST",
"/api/public/dataset-items",
{
datasetName: datasetName,
id: datasetItemId,
input: { john: "doe" },
expectedOutput: { john: "doe" },
metadata: null,
sourceTraceId: null,
sourceObservationId: null,
status: null,
},
auth,
);
const databaseDatasetItem = await prisma.datasetItem.findFirst({
where: {
id: datasetItemId,
},
});
expect(databaseDatasetItem).toMatchObject({
input: { john: "doe" },
expectedOutput: { john: "doe" },
});
});
it("should return 404 when trying to update dataset item that exists in different dataset of the same project", async () => {
const datasetItemId = v4();
const dataset = await prisma.dataset.create({
data: {
name: "dataset-name-1",
projectId: projectId,
},
});
await prisma.dataset.create({
data: {
name: "dataset-name-2",
projectId: projectId,
},
});
await prisma.datasetItem.create({
data: {
id: datasetItemId,
datasetId: dataset.id,
projectId: projectId,
},
});
const response = await makeAPICall(
"POST",
"/api/public/dataset-items",
{
datasetName: "dataset-name-2",
id: datasetItemId,
input: { key: "new-value" },
expectedOutput: { key: "new-value" },
},
auth,
);
expect(response.status).toBe(404);
});
it("GET datasets (v1 & v2)", async () => {
// v1 post
await makeZodVerifiedAPICall(
@@ -0,0 +1,219 @@
import { randomUUID } from "crypto";
import { makeAPICall } from "@/src/__tests__/test-utils";
import waitForExpect from "wait-for-expect";
import {
getObservationById,
getScoreById,
getTraceById,
} from "@langfuse/shared/src/server";
import { v4 } from "uuid";
const projectId = "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a";
describe("/api/public/ingestion API Endpoint", () => {
it.each([
[
"plain",
{
id: randomUUID(),
type: "trace-create",
timestamp: new Date().toISOString(),
body: {
id: randomUUID(),
timestamp: new Date().toISOString(),
},
},
],
[
"metadata+io",
{
id: randomUUID(),
type: "trace-create",
timestamp: new Date().toISOString(),
body: {
id: randomUUID(),
timestamp: new Date().toISOString(),
metadata: { hello: "world" },
input: "input",
output: "output",
},
},
],
[
"complex-io",
{
id: randomUUID(),
type: "trace-create",
timestamp: new Date().toISOString(),
body: {
id: randomUUID(),
timestamp: new Date().toISOString(),
metadata: { hello: "world" },
input: ["hello", { world: "world" }, [1, 2, 3]],
},
},
],
])(
"should create traces via the ingestion API (%s)",
async (_name: string, entity: any) => {
const response = await makeAPICall("POST", "/api/public/ingestion", {
batch: [entity],
});
expect(response.status).toBe(207);
await waitForExpect(async () => {
const trace = await getTraceById(entity.body.id, projectId);
expect(trace).toBeDefined();
expect(trace!.id).toBe(entity.body.id);
expect(trace!.projectId).toBe(projectId);
expect(trace!.metadata).toEqual(entity.body?.metadata ?? {});
expect(trace!.input).toEqual(entity.body?.input ?? null);
expect(trace!.output).toEqual(entity.body?.output ?? null);
});
},
);
it.each([
[
"generation",
"GENERATION",
{
id: randomUUID(),
type: "generation-create",
timestamp: new Date().toISOString(),
body: {
id: randomUUID(),
traceId: randomUUID(),
parentObservationId: randomUUID(),
startTime: new Date().toISOString(),
model: "gpt-4",
input: { text: "input" },
output: { text: "output" },
},
},
],
[
"span",
"SPAN",
{
id: randomUUID(),
type: "span-create",
timestamp: new Date().toISOString(),
body: {
id: randomUUID(),
traceId: randomUUID(),
startTime: new Date().toISOString(),
},
},
],
[
"span-complex-io",
"SPAN",
{
id: randomUUID(),
type: "span-create",
timestamp: new Date().toISOString(),
body: {
id: randomUUID(),
traceId: randomUUID(),
startTime: new Date().toISOString(),
input: ["hello", { world: "world" }, [1, 2, 3]],
output: ["hello", { world: [2, 3, "test"] }, [1, 2, 3]],
},
},
],
])(
"should create observations via the ingestion API (%s)",
async (_name: string, type: string, entity: any) => {
const response = await makeAPICall("POST", "/api/public/ingestion", {
batch: [entity],
});
expect(response.status).toBe(207);
await waitForExpect(async () => {
const observation = await getObservationById(
entity.body.id,
projectId,
true,
);
expect(observation).toBeDefined();
expect(observation!.id).toBe(entity.body.id);
expect(observation!.projectId).toBe(projectId);
expect(observation!.metadata).toEqual(entity.body?.metadata ?? {});
expect(observation!.input).toEqual(entity.body?.input ?? null);
expect(observation!.output).toEqual(entity.body?.output ?? null);
expect(observation!.type).toBe(type);
});
},
);
it.each([
[
"plain",
{
id: randomUUID(),
type: "score-create",
timestamp: new Date().toISOString(),
body: {
id: randomUUID(),
name: "score-name",
traceId: randomUUID(),
value: 100.5,
observationId: randomUUID(),
},
},
],
])(
"should create scores via the ingestion API (%s)",
async (_name: string, entity: any) => {
const response = await makeAPICall("POST", "/api/public/ingestion", {
batch: [entity],
});
expect(response.status).toBe(207);
await waitForExpect(async () => {
const score = await getScoreById(projectId, entity.body.id);
expect(score).toBeDefined();
expect(score!.id).toBe(entity.body.id);
expect(score!.projectId).toBe(projectId);
expect(score!.value).toEqual(100.5);
});
},
);
it("should fail for long trace name", async () => {
const traceId = v4();
const baseString =
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. ";
const repeatCount = Math.ceil(1500 / baseString.length);
const name = baseString.repeat(repeatCount);
const response = await makeAPICall("POST", "/api/public/ingestion", {
batch: [
{
id: v4(),
type: "trace-create",
timestamp: new Date().toISOString(),
body: {
id: traceId,
name,
userId: "user-1",
metadata: { key: "value" },
release: "1.0.0",
version: "2.0.0",
},
},
],
});
expect(response.status).toBe(207);
expect("errors" in response.body).toBe(true);
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
expect(response.body.errors.length).toBe(1);
expect(response.body.errors[0].message).toBe("Invalid request data");
});
});
@@ -204,6 +204,7 @@ describe("/api/public/observations API Endpoint", () => {
trace_id: traceId,
name: "generation-name",
start_time: new Date("2021-01-01T00:00:00.000Z").getTime(),
event_ts: new Date("2021-01-01T00:00:00.000Z").getTime(),
end_time: new Date("2021-01-01T00:00:00.000Z").getTime(),
project_id: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
type: "GENERATION",
@@ -214,6 +215,7 @@ describe("/api/public/observations API Endpoint", () => {
trace_id: traceId,
name: "generation-name",
start_time: new Date("2021-02-01T00:00:00.000Z").getTime(),
event_ts: new Date("2021-02-01T00:00:00.000Z").getTime(),
end_time: new Date("2021-02-01T00:00:00.000Z").getTime(),
project_id: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
type: "SPAN",
@@ -223,6 +225,7 @@ describe("/api/public/observations API Endpoint", () => {
trace_id: traceId,
name: "generation-name",
start_time: new Date("2021-03-01T00:00:00.000Z").getTime(),
event_ts: new Date("2021-03-01T00:00:00.000Z").getTime(),
project_id: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
type: "EVENT",
});
@@ -231,6 +234,7 @@ describe("/api/public/observations API Endpoint", () => {
trace_id: traceId,
name: "generation-name",
start_time: new Date("2021-04-01T00:00:00.000Z").getTime(),
event_ts: new Date("2021-04-01T00:00:00.000Z").getTime(),
end_time: new Date("2021-04-01T00:00:00.000Z").getTime(),
project_id: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
type: "GENERATION",
@@ -154,8 +154,9 @@ describe("Clickhouse Observations Repository Test", () => {
expect(firstObservation.promptId).toEqual(observation.prompt_id);
expect(firstObservation.endTime).toEqual(new Date(observation.end_time));
expect(firstObservation.timeToFirstToken).toEqual(
new Date(observation.completion_start_time).getTime() -
new Date(observation.start_time).getTime(),
(new Date(observation.completion_start_time).getTime() -
new Date(observation.start_time).getTime()) /
1000,
);
expect(firstObservation.timeToFirstToken).toBeGreaterThan(0);
expect(firstObservation.calculatedTotalCost).toEqual(
@@ -35,6 +35,14 @@ describe("Clickhouse Traces Repository Test", () => {
release: null,
version: null,
user_id: null,
input: JSON.stringify({
this: {
is: {
a: ["complex", "object"],
},
},
}),
output: "regular string",
created_at: Date.now(),
updated_at: Date.now(),
event_ts: Date.now(),
@@ -63,8 +71,8 @@ describe("Clickhouse Traces Repository Test", () => {
expect(result.userId).toEqual(trace.user_id);
expect(result.sessionId).toEqual(trace.session_id);
expect(result.public).toEqual(trace.public);
expect(result.input).toEqual(null);
expect(result.output).toEqual(null);
expect(result.input).toEqual(JSON.parse(trace.input));
expect(result.output).toEqual("regular string");
expect(result.metadata).toEqual(trace.metadata);
expect(result.createdAt).toEqual(new Date(trace.created_at));
expect(result.updatedAt).toEqual(new Date(trace.updated_at));
@@ -0,0 +1,325 @@
import { v4 } from "uuid";
import { prisma } from "@langfuse/shared/src/db";
import {
createObservation,
createObservationsCh,
createOrgProjectAndApiKey,
createTracesCh,
getSessionsWithMetrics,
} from "@langfuse/shared/src/server";
import { createTrace, getSessionsTable } from "@langfuse/shared/src/server";
describe("trpc.sessions", () => {
describe("GET sessions.all", () => {
it("should GET all session", async () => {
const { projectId } = await createOrgProjectAndApiKey();
const sessionId = v4();
await prisma.traceSession.create({
data: {
id: sessionId,
projectId: projectId,
},
});
const traces = [
createTrace({ session_id: sessionId, project_id: projectId }),
createTrace({ session_id: sessionId, project_id: projectId }),
];
await createTracesCh(traces);
const uiSessions = await getSessionsTable({
projectId: projectId,
filter: [],
orderBy: null,
limit: 10000,
page: 0,
});
expect(uiSessions.length).toBe(1);
expect(uiSessions[0].session_id).toBe(sessionId);
expect(uiSessions[0].trace_count).toBe(2);
expect(uiSessions[0].trace_tags).toEqual(["doe", "john"]);
});
});
it("should GET all session filtered by trace attribute only", async () => {
const { projectId } = await createOrgProjectAndApiKey();
const sessionId = v4();
await prisma.traceSession.create({
data: {
id: sessionId,
projectId: projectId,
},
});
const traces = [
createTrace({
session_id: sessionId,
project_id: projectId,
user_id: "user1",
}),
createTrace({
session_id: sessionId,
project_id: projectId,
user_id: undefined,
}),
];
await createTracesCh(traces);
const uiSessions = await getSessionsTable({
projectId: projectId,
filter: [
{
column: "userIds",
type: "stringOptions",
operator: "any of",
value: ["user1"],
},
],
orderBy: null,
limit: 10000,
page: 0,
});
expect(uiSessions.length).toBe(1);
expect(uiSessions[0].session_id).toBe(sessionId);
expect(uiSessions[0].trace_count).toBe(2);
expect(uiSessions[0].trace_tags).toEqual(["doe", "john"]);
expect(uiSessions[0].user_ids).toEqual(["user1"]);
});
it("should GET sessions ordered by total cost", async () => {
const { projectId } = await createOrgProjectAndApiKey();
const sessionId1 = v4();
const sessionId2 = v4();
await prisma.traceSession.createMany({
data: [
{
id: sessionId1,
projectId: projectId,
},
{
id: sessionId2,
projectId: projectId,
},
],
});
const traces = [
createTrace({
session_id: sessionId1,
project_id: projectId,
}),
createTrace({
session_id: sessionId2,
project_id: projectId,
}),
];
const observations = [
createObservation({
trace_id: traces[0].id,
project_id: projectId,
cost_details: {
input: 0.1,
output: 0.2,
total: 0.3,
},
total_cost: 0.3,
}),
createObservation({
trace_id: traces[1].id,
project_id: projectId,
cost_details: {
input: 0.3,
output: 0.4,
total: 0.7,
},
total_cost: 0.7,
}),
];
await createTracesCh(traces);
await createObservationsCh(observations);
const uiSessions = await getSessionsTable({
projectId: projectId,
filter: [],
orderBy: {
column: "totalCost",
order: "DESC" as const,
},
limit: 10000,
page: 0,
});
expect(uiSessions.length).toBe(2);
expect(uiSessions[0].session_id).toBe(sessionId2);
expect(uiSessions[1].session_id).toBe(sessionId1);
});
it("should GET sessions ordered by duration", async () => {
const { projectId } = await createOrgProjectAndApiKey();
const sessionId1 = v4();
const sessionId2 = v4();
await prisma.traceSession.createMany({
data: [
{
id: sessionId1,
projectId: projectId,
},
{
id: sessionId2,
projectId: projectId,
},
],
});
const traces = [
createTrace({
session_id: sessionId1,
project_id: projectId,
}),
createTrace({
session_id: sessionId2,
project_id: projectId,
}),
];
const observations = [
createObservation({
trace_id: traces[0].id,
project_id: projectId,
start_time: new Date("2024-01-01T00:00:00Z").getTime(),
end_time: new Date("2024-01-01T00:00:10Z").getTime(), // 10 second duration
}),
createObservation({
trace_id: traces[1].id,
project_id: projectId,
start_time: new Date("2024-01-01T00:00:00Z").getTime(),
end_time: new Date("2024-01-01T00:00:20Z").getTime(), // 20 second duration
}),
];
await createTracesCh(traces);
await createObservationsCh(observations);
const uiSessions = await getSessionsTable({
projectId: projectId,
filter: [],
orderBy: {
column: "sessionDuration",
order: "DESC" as const,
},
limit: 10000,
page: 0,
});
expect(uiSessions.length).toBe(2);
expect(uiSessions[0].session_id).toBe(sessionId2);
expect(uiSessions[1].session_id).toBe(sessionId1);
});
it("should GET metrics for a list of sessions", async () => {
const { projectId } = await createOrgProjectAndApiKey();
const sessionId1 = v4();
const sessionId2 = v4();
await prisma.traceSession.createMany({
data: [
{
id: sessionId1,
projectId: projectId,
},
{
id: sessionId2,
projectId: projectId,
},
],
});
const traces = [
createTrace({
session_id: sessionId1,
project_id: projectId,
user_id: "user1",
}),
createTrace({
session_id: sessionId1,
project_id: projectId,
user_id: "user2",
}),
createTrace({
session_id: sessionId2,
project_id: projectId,
user_id: "user3",
}),
];
const observations = traces.flatMap((trace) => [
createObservation({
trace_id: trace.id,
project_id: projectId,
}),
createObservation({
trace_id: trace.id,
project_id: projectId,
}),
]);
await createObservationsCh(observations);
await createTracesCh(traces);
const sessions = await getSessionsWithMetrics({
projectId: projectId,
filter: [
{
column: "id",
type: "stringOptions",
operator: "any of",
value: [sessionId1, sessionId2],
},
],
});
expect(sessions.length).toBe(2);
// Session 1 checks
const session1 = sessions.find((s) => s.session_id === sessionId1);
expect(session1).toBeDefined();
expect(session1?.trace_count).toBe(2);
expect(session1?.user_ids).toEqual(
expect.arrayContaining(["user1", "user2"]),
);
expect(session1?.trace_tags).toEqual(["doe", "john"]);
expect(session1?.total_observations).toEqual(4);
expect(Number(session1?.session_input_cost)).toBeGreaterThan(0);
expect(Number(session1?.session_output_cost)).toBeGreaterThan(0);
expect(Number(session1?.session_total_cost)).toBeGreaterThan(0);
expect(Number(session1?.session_input_usage)).toBeGreaterThan(0);
expect(Number(session1?.session_output_usage)).toBeGreaterThan(0);
expect(Number(session1?.session_total_usage)).toBeGreaterThan(0);
// Session 2 checks
const session2 = sessions.find((s) => s.session_id === sessionId2);
expect(session2).toBeDefined();
expect(session2?.trace_count).toBe(1);
expect(session2?.user_ids).toEqual(["user3"]);
expect(session2?.trace_tags).toEqual(["doe", "john"]);
expect(session2?.total_observations).toEqual(2);
expect(Number(session2?.session_input_cost)).toBeGreaterThan(0);
expect(Number(session2?.session_output_cost)).toBeGreaterThan(0);
expect(Number(session2?.session_total_cost)).toBeGreaterThan(0);
expect(Number(session2?.session_input_usage)).toBeGreaterThan(0);
expect(Number(session2?.session_output_usage)).toBeGreaterThan(0);
expect(Number(session2?.session_total_usage)).toBeGreaterThan(0);
});
});
@@ -52,7 +52,7 @@ describe("getUserMetrics function", () => {
await createObservationsInClickhouse([observation1, observation2]);
const userMetrics = await getUserMetrics(projectId, [userId]);
const userMetrics = await getUserMetrics(projectId, [userId], []);
expect(userMetrics.length).toBe(1);
expect(userMetrics[0]).toMatchObject({
-297
View File
@@ -1,297 +0,0 @@
/** @jest-environment node */
import { prisma } from "@langfuse/shared/src/db";
import {
makeZodVerifiedAPICall,
pruneDatabase,
} from "@/src/__tests__/test-utils";
import { v4 as uuidv4 } from "uuid";
import { PostTracesV1Response } from "@/src/features/public-api/types/traces";
import { PostEventsV1Response } from "@/src/features/public-api/types/events";
describe("/api/public/events API Endpoint", () => {
beforeEach(async () => await pruneDatabase());
it("should create event after trace", async () => {
await pruneDatabase();
const traceId = uuidv4();
await makeZodVerifiedAPICall(
PostTracesV1Response,
"POST",
"/api/public/traces",
{
id: traceId,
name: "trace-name",
userId: "user-1",
projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
metadata: { key: "value" },
release: "1.0.0",
version: "2.0.0",
},
);
const dbTrace = await prisma.trace.findMany({
where: {
id: traceId,
},
});
expect(dbTrace.length).toBeGreaterThan(0);
expect(dbTrace[0]?.id).toBe(traceId);
const eventId = uuidv4();
const createEvent = await makeZodVerifiedAPICall(
PostEventsV1Response,
"POST",
"/api/public/events",
{
id: eventId,
traceId: traceId,
name: "event-name",
startTime: "2021-01-01T00:00:00.000Z",
input: { input: "value" },
output: { output: "value" },
metadata: { meta: "value" },
version: "2.0.0",
},
);
expect(createEvent.status).toBe(200);
const dbEvent = await prisma.observation.findUnique({
where: {
id: eventId,
},
});
expect(dbEvent?.id).toBe(eventId);
expect(dbEvent?.traceId).toBe(traceId);
expect(dbEvent?.name).toBe("event-name");
expect(dbEvent?.startTime).toEqual(new Date("2021-01-01T00:00:00.000Z"));
expect(dbEvent?.input).toEqual({ input: "value" });
expect(dbEvent?.output).toEqual({ output: "value" });
expect(dbEvent?.metadata).toEqual({ meta: "value" });
expect(dbEvent?.version).toBe("2.0.0");
});
it("should create event before trace", async () => {
await pruneDatabase();
const traceId = uuidv4();
const eventId = uuidv4();
const createEvent = await makeZodVerifiedAPICall(
PostEventsV1Response,
"POST",
"/api/public/events",
{
id: eventId,
traceId: traceId,
name: "event-name",
startTime: "2021-01-01T00:00:00.000Z",
input: { input: "value" },
output: { output: "value" },
metadata: { meta: "value" },
version: "2.0.0",
},
);
expect(createEvent.status).toBe(200);
const dbEvent = await prisma.observation.findUnique({
where: {
id: eventId,
},
});
expect(dbEvent?.id).toBe(eventId);
expect(dbEvent?.traceId).toBe(traceId);
expect(dbEvent?.name).toBe("event-name");
expect(dbEvent?.startTime).toEqual(new Date("2021-01-01T00:00:00.000Z"));
expect(dbEvent?.input).toEqual({ input: "value" });
expect(dbEvent?.output).toEqual({ output: "value" });
expect(dbEvent?.metadata).toEqual({ meta: "value" });
expect(dbEvent?.version).toBe("2.0.0");
await makeZodVerifiedAPICall(
PostTracesV1Response,
"POST",
"/api/public/traces",
{
id: traceId,
name: "trace-name",
userId: "user-1",
projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
metadata: { key: "value" },
release: "1.0.0",
version: "2.0.0",
},
);
const dbTrace = await prisma.trace.findMany({
where: {
id: traceId,
},
});
expect(dbTrace.length).toBeGreaterThan(0);
expect(dbTrace[0]?.id).toBe(traceId);
});
it("should create trace and ignore externalId and create event afterwards", async () => {
await pruneDatabase();
const traceId = uuidv4();
const response = await makeZodVerifiedAPICall(
PostTracesV1Response,
"POST",
"/api/public/traces",
{
externalId: traceId,
name: "trace-name",
userId: "user-1",
projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
metadata: { key: "value" },
release: "1.0.0",
version: "2.0.0",
},
);
expect(response.status).toBe(200);
const dbTrace = await prisma.trace.findMany({
where: {
name: "trace-name",
},
});
expect(dbTrace.length).toBeGreaterThan(0);
expect(dbTrace[0]?.externalId).toBeNull();
expect(dbTrace[0]?.id).not.toBe(traceId);
const eventId = uuidv4();
const createEvent = await makeZodVerifiedAPICall(
PostEventsV1Response,
"POST",
"/api/public/events",
{
id: eventId,
traceIdType: "EXTERNAL",
traceId: dbTrace[0]?.id,
name: "event-name",
startTime: "2021-01-01T00:00:00.000Z",
input: { input: "value" },
output: { output: "value" },
metadata: { meta: "value" },
version: "2.0.0",
},
);
expect(createEvent.status).toBe(200);
const dbEvent = await prisma.observation.findUnique({
where: {
id: eventId,
},
});
expect(dbEvent?.id).toBe(eventId);
expect(dbEvent?.traceId).toBe(dbTrace[0]?.id);
expect(dbEvent?.name).toBe("event-name");
expect(dbEvent?.startTime).toEqual(new Date("2021-01-01T00:00:00.000Z"));
expect(dbEvent?.input).toEqual({ input: "value" });
expect(dbEvent?.output).toEqual({ output: "value" });
expect(dbEvent?.metadata).toEqual({ meta: "value" });
expect(dbEvent?.version).toBe("2.0.0");
});
it("should create trace when creating span without existing trace", async () => {
const eventName = uuidv4();
const spanId = uuidv4();
const createEvent = await makeZodVerifiedAPICall(
PostEventsV1Response,
"POST",
"/api/public/events",
{
id: spanId,
name: eventName,
startTime: "2021-01-01T00:00:00.000Z",
endTime: "2021-01-01T00:00:00.000Z",
input: { input: "value" },
output: { output: "value" },
metadata: { meta: "value" },
version: "2.0.0",
},
);
const dbTrace = await prisma.trace.findMany({
where: {
name: eventName,
},
});
expect(dbTrace.length).toBe(1);
expect(dbTrace[0]?.name).toBe(eventName);
expect(createEvent.status).toBe(200);
const dbEvent = await prisma.observation.findUnique({
where: {
id: spanId,
},
});
expect(dbEvent?.id).toBe(spanId);
expect(dbEvent?.traceId).toBe(dbTrace[0]?.id);
expect(dbEvent?.name).toBe(eventName);
expect(dbEvent?.startTime).toEqual(new Date("2021-01-01T00:00:00.000Z"));
expect(dbEvent?.input).toEqual({ input: "value" });
expect(dbEvent?.output).toEqual({ output: "value" });
expect(dbEvent?.metadata).toEqual({ meta: "value" });
expect(dbEvent?.version).toBe("2.0.0");
});
it("should create event when creating generation without existing trace without traceId", async () => {
const generationName = uuidv4();
const spanId = uuidv4();
const createSpan = await makeZodVerifiedAPICall(
PostEventsV1Response,
"POST",
"/api/public/events",
{
id: spanId,
name: generationName,
startTime: "2021-01-01T00:00:00.000Z",
input: { key: "value" },
metadata: { key: "value" },
version: "2.0.0",
},
);
const dbEvent = await prisma.observation.findFirstOrThrow({
where: {
name: generationName,
},
});
const dbTrace = await prisma.trace.findMany({
where: {
id: dbEvent.traceId!,
},
});
expect(dbTrace.length).toBe(1);
expect(dbTrace[0]?.name).toBe(generationName);
expect(createSpan.status).toBe(200);
expect(dbEvent.id).toBe(spanId);
expect(dbEvent.traceId).toBe(dbTrace[0]?.id);
expect(dbEvent.name).toBe(generationName);
expect(dbEvent.startTime).toEqual(new Date("2021-01-01T00:00:00.000Z"));
expect(dbEvent.input).toEqual({ key: "value" });
expect(dbEvent.metadata).toEqual({ key: "value" });
expect(dbEvent.version).toBe("2.0.0");
});
});
-643
View File
@@ -1,643 +0,0 @@
/** @jest-environment node */
import { v4 as uuidv4 } from "uuid";
import {
makeAPICall,
makeZodVerifiedAPICall,
pruneDatabase,
} from "@/src/__tests__/test-utils";
import { ModelUsageUnit } from "@langfuse/shared";
import { prisma } from "@langfuse/shared/src/db";
import { PostTracesV1Response } from "@/src/features/public-api/types/traces";
import {
PatchGenerationsV1Response,
PostGenerationsV1Body,
PostGenerationsV1Response,
} from "@/src/features/public-api/types/generations";
describe("/api/public/generations API Endpoint", () => {
beforeEach(async () => await pruneDatabase());
[
{
usage: {
input: 100,
output: 200,
total: 100,
unit: ModelUsageUnit.Characters,
},
expectedUnit: ModelUsageUnit.Characters,
expectedPromptTokens: 100,
expectedCompletionTokens: 200,
expectedTotalTokens: 100,
},
{
usage: {
total: 100,
unit: ModelUsageUnit.Characters,
},
expectedUnit: ModelUsageUnit.Characters,
expectedPromptTokens: 0,
expectedCompletionTokens: 0,
expectedTotalTokens: 100,
},
{
usage: {
total: 100,
},
expectedUnit: null,
expectedPromptTokens: 0,
expectedCompletionTokens: 0,
expectedTotalTokens: 100,
},
{
usage: {
promptTokens: 100,
completionTokens: 200,
totalTokens: 100,
},
expectedPromptTokens: 100,
expectedCompletionTokens: 200,
expectedTotalTokens: 100,
expectedUnit: ModelUsageUnit.Tokens,
},
{
usage: {
totalTokens: 100,
},
expectedPromptTokens: 0,
expectedCompletionTokens: 0,
expectedTotalTokens: 100,
expectedUnit: ModelUsageUnit.Tokens,
},
{
usage: undefined,
expectedPromptTokens: 0,
expectedCompletionTokens: 0,
expectedTotalTokens: 0,
expectedUnit: null,
},
{
usage: null,
expectedPromptTokens: 0,
expectedCompletionTokens: 0,
expectedTotalTokens: 0,
expectedUnit: null,
},
{
usage: {},
expectedPromptTokens: 0,
expectedCompletionTokens: 0,
expectedTotalTokens: 0,
expectedUnit: null,
},
].forEach((testConfig) => {
it(`should create generation after trace 1 ${JSON.stringify(
testConfig,
)}`, async () => {
await pruneDatabase();
const traceId = uuidv4();
await makeZodVerifiedAPICall(
PostTracesV1Response,
"POST",
"/api/public/traces",
{
id: traceId,
name: "trace-name",
userId: "user-1",
projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
metadata: { key: "value" },
release: "1.0.0",
version: "2.0.0",
},
);
const dbTrace = await prisma.trace.findMany({
where: {
id: traceId,
},
});
expect(dbTrace.length).toBeGreaterThan(0);
expect(dbTrace[0]?.id).toBe(traceId);
const generationId = uuidv4();
const createGeneration = await makeZodVerifiedAPICall(
PostGenerationsV1Response,
"POST",
"/api/public/generations",
{
id: generationId,
traceId: traceId,
name: "generation-name",
startTime: "2021-01-01T00:00:00.000Z",
endTime: "2021-01-01T00:00:00.000Z",
model: "model-name",
modelParameters: { key: "value" },
prompt: { key: "value" },
metadata: { key: "value" },
version: "2.0.0",
usage: testConfig.usage,
},
);
expect(createGeneration.status).toBe(200);
const dbGeneration = await prisma.observation.findUnique({
where: {
id: generationId,
},
});
expect(dbGeneration?.id).toBe(generationId);
expect(dbGeneration?.traceId).toBe(traceId);
expect(dbGeneration?.name).toBe("generation-name");
expect(dbGeneration?.startTime).toEqual(
new Date("2021-01-01T00:00:00.000Z"),
);
expect(dbGeneration?.endTime).toEqual(
new Date("2021-01-01T00:00:00.000Z"),
);
expect(dbGeneration?.model).toBe("model-name");
expect(dbGeneration?.modelParameters).toEqual({ key: "value" });
expect(dbGeneration?.input).toEqual({ key: "value" });
expect(dbGeneration?.metadata).toEqual({ key: "value" });
expect(dbGeneration?.version).toBe("2.0.0");
expect(dbGeneration?.unit).toBe(testConfig.expectedUnit);
expect(dbGeneration?.promptTokens).toBe(testConfig.expectedPromptTokens);
expect(dbGeneration?.completionTokens).toBe(
testConfig.expectedCompletionTokens,
);
expect(dbGeneration?.totalTokens).toBe(testConfig.expectedTotalTokens);
});
});
it("should create generation before trace", async () => {
await pruneDatabase();
const traceId = uuidv4();
const generationId = uuidv4();
const createGeneration = await makeZodVerifiedAPICall(
PostGenerationsV1Body,
"POST",
"/api/public/generations",
{
id: generationId,
traceId: traceId,
name: "generation-name",
startTime: "2021-01-01T00:00:00.000Z",
endTime: "2021-01-01T00:00:00.000Z",
model: "model-name",
modelParameters: { key: "value" },
prompt: { key: "value" },
metadata: { key: "value" },
version: "2.0.0",
},
);
expect(createGeneration.status).toBe(200);
const dbGeneration = await prisma.observation.findUnique({
where: {
id: generationId,
},
});
expect(dbGeneration?.id).toBe(generationId);
expect(dbGeneration?.traceId).toBe(traceId);
expect(dbGeneration?.name).toBe("generation-name");
expect(dbGeneration?.startTime).toEqual(
new Date("2021-01-01T00:00:00.000Z"),
);
expect(dbGeneration?.endTime).toEqual(new Date("2021-01-01T00:00:00.000Z"));
expect(dbGeneration?.model).toBe("model-name");
expect(dbGeneration?.modelParameters).toEqual({ key: "value" });
expect(dbGeneration?.input).toEqual({ key: "value" });
expect(dbGeneration?.metadata).toEqual({ key: "value" });
expect(dbGeneration?.version).toBe("2.0.0");
await makeZodVerifiedAPICall(
PostTracesV1Response,
"POST",
"/api/public/traces",
{
id: traceId,
name: "trace-name",
userId: "user-1",
projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
metadata: { key: "value" },
release: "1.0.0",
version: "2.0.0",
},
);
const dbTrace = await prisma.trace.findMany({
where: {
id: traceId,
},
});
expect(dbTrace.length).toBeGreaterThan(0);
expect(dbTrace[0]?.id).toBe(traceId);
});
it("should create generation after trace ignoring externalId", async () => {
const traceId = uuidv4();
const response = await makeZodVerifiedAPICall(
PostTracesV1Response,
"POST",
"/api/public/traces",
{
externalId: traceId,
name: "trace-name",
userId: "user-1",
projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
metadata: { key: "value" },
release: "1.0.0",
version: "2.0.0",
},
);
expect(response.status).toBe(200);
const dbTrace = await prisma.trace.findMany({
where: {
name: "trace-name",
},
});
expect(dbTrace.length).toBeGreaterThan(0);
expect(dbTrace[0]?.externalId).toBeNull();
expect(dbTrace[0]?.id).not.toBe(traceId);
const generationId = uuidv4();
const createGeneration = await makeZodVerifiedAPICall(
PostGenerationsV1Body,
"POST",
"/api/public/generations",
{
id: generationId,
traceIdType: "EXTERNAL",
traceId: traceId,
name: "generation-name",
startTime: "2021-01-01T00:00:00.000Z",
endTime: "2021-01-01T00:00:00.000Z",
model: "model-name",
modelParameters: { key: "value" },
prompt: { key: "value" },
metadata: { key: "value" },
version: "2.0.0",
},
);
expect(createGeneration.status).toBe(200);
const dbGeneration = await prisma.observation.findUnique({
where: {
id: generationId,
},
});
expect(dbGeneration?.id).toBe(generationId);
expect(dbGeneration?.traceId).toBe(traceId);
expect(dbGeneration?.name).toBe("generation-name");
expect(dbGeneration?.startTime).toEqual(
new Date("2021-01-01T00:00:00.000Z"),
);
expect(dbGeneration?.endTime).toEqual(new Date("2021-01-01T00:00:00.000Z"));
expect(dbGeneration?.model).toBe("model-name");
expect(dbGeneration?.modelParameters).toEqual({ key: "value" });
expect(dbGeneration?.input).toEqual({ key: "value" });
expect(dbGeneration?.metadata).toEqual({ key: "value" });
expect(dbGeneration?.version).toBe("2.0.0");
});
it("should create trace when creating generation without existing trace", async () => {
const generationName = uuidv4();
const generationId = uuidv4();
const createGeneration = await makeZodVerifiedAPICall(
PostGenerationsV1Body,
"POST",
"/api/public/generations",
{
id: generationId,
name: generationName,
startTime: "2021-01-01T00:00:00.000Z",
endTime: "2021-01-01T00:00:00.000Z",
model: "model-name",
modelParameters: { key: "value" },
prompt: { key: "value" },
metadata: { key: "value" },
version: "2.0.0",
},
);
const dbTrace = await prisma.trace.findMany({
where: {
name: generationName,
},
});
expect(dbTrace.length).toBe(1);
expect(dbTrace[0]?.name).toBe(generationName);
expect(createGeneration.status).toBe(200);
const dbGeneration = await prisma.observation.findUnique({
where: {
id: generationId,
},
});
expect(dbGeneration?.id).toBe(generationId);
expect(dbGeneration?.traceId).toBe(dbTrace[0]?.id);
expect(dbGeneration?.name).toBe(generationName);
expect(dbGeneration?.startTime).toEqual(
new Date("2021-01-01T00:00:00.000Z"),
);
expect(dbGeneration?.endTime).toEqual(new Date("2021-01-01T00:00:00.000Z"));
expect(dbGeneration?.model).toBe("model-name");
expect(dbGeneration?.modelParameters).toEqual({ key: "value" });
expect(dbGeneration?.input).toEqual({ key: "value" });
expect(dbGeneration?.metadata).toEqual({ key: "value" });
expect(dbGeneration?.version).toBe("2.0.0");
});
it("should create nested generations", async () => {
const generationName = uuidv4();
const generationId = uuidv4();
const createGeneration = await makeZodVerifiedAPICall(
PostGenerationsV1Body,
"POST",
"/api/public/generations",
{
id: generationId,
name: generationName,
startTime: "2021-01-01T00:00:00.000Z",
endTime: "2021-01-01T00:00:00.000Z",
model: "model-name",
modelParameters: { key: "value" },
prompt: { key: "value" },
metadata: { key: "value" },
version: "2.0.0",
},
);
expect(createGeneration.status).toBe(200);
const dbGeneration = await prisma.observation.findUnique({
where: {
id: generationId,
},
});
expect(dbGeneration?.id).toBe(generationId);
const generationId2 = uuidv4();
const generationName2 = uuidv4();
const createGeneration2 = await makeZodVerifiedAPICall(
PostGenerationsV1Body,
"POST",
"/api/public/generations",
{
id: generationId2,
name: generationName2,
startTime: "2021-01-01T00:00:00.000Z",
endTime: "2021-01-01T00:00:00.000Z",
model: "model-name",
modelParameters: { key: "value" },
prompt: { key: "value" },
metadata: { key: "value" },
version: "2.0.0",
parentObservationId: generationId,
},
);
expect(createGeneration2.status).toBe(200);
const dbGeneration2 = await prisma.observation.findUnique({
where: {
id: generationId2,
},
});
expect(dbGeneration2?.id).toBe(generationId2);
expect(dbGeneration2?.parentObservationId).toBe(generationId);
});
it("should not create trace when creating generation without existing trace with externalId", async () => {
const generationName = uuidv4();
const generationId = uuidv4();
const externalTraceId = uuidv4();
const createGeneration = await makeZodVerifiedAPICall(
PostGenerationsV1Body,
"POST",
"/api/public/generations",
{
id: generationId,
traceIdType: "EXTERNAL",
traceId: externalTraceId,
name: generationName,
startTime: "2021-01-01T00:00:00.000Z",
endTime: "2021-01-01T00:00:00.000Z",
model: "model-name",
modelParameters: { key: "value" },
prompt: { key: "value" },
metadata: { key: "value" },
version: "2.0.0",
},
);
expect(createGeneration.status).toBe(200);
const dbGeneration = await prisma.observation.findFirstOrThrow({
where: {
name: generationName,
},
});
expect(dbGeneration.id).toBe(generationId);
expect(dbGeneration.traceId).toBe(externalTraceId);
const dbTraces = await prisma.trace.findMany();
expect(dbTraces.length).toBe(0);
});
it("should create trace when creating generation without existing trace without traceId", async () => {
const generationName = uuidv4();
const generationId = uuidv4();
const createGeneration = await makeZodVerifiedAPICall(
PostGenerationsV1Body,
"POST",
"/api/public/generations",
{
id: generationId,
name: generationName,
startTime: "2021-01-01T00:00:00.000Z",
endTime: "2021-01-01T00:00:00.000Z",
model: "model-name",
modelParameters: { key: "value" },
prompt: { key: "value" },
metadata: { key: "value" },
version: "2.0.0",
},
);
const dbGeneration = await prisma.observation.findFirstOrThrow({
where: {
name: generationName,
},
});
const dbTrace = await prisma.trace.findMany({
where: {
id: dbGeneration.traceId!,
},
});
expect(dbTrace.length).toBe(1);
expect(dbTrace[0]?.name).toBe(generationName);
expect(createGeneration.status).toBe(200);
expect(dbGeneration.id).toBe(generationId);
expect(dbGeneration.traceId).toBe(dbTrace[0]?.id);
expect(dbGeneration.name).toBe(generationName);
expect(dbGeneration.startTime).toEqual(
new Date("2021-01-01T00:00:00.000Z"),
);
expect(dbGeneration.endTime).toEqual(new Date("2021-01-01T00:00:00.000Z"));
expect(dbGeneration.model).toBe("model-name");
expect(dbGeneration.modelParameters).toEqual({ key: "value" });
expect(dbGeneration.input).toEqual({ key: "value" });
expect(dbGeneration.metadata).toEqual({ key: "value" });
expect(dbGeneration.version).toBe("2.0.0");
});
it("should update generation", async () => {
const generationName = uuidv4();
const generationId = uuidv4();
const createGeneration = await makeZodVerifiedAPICall(
PostGenerationsV1Body,
"POST",
"/api/public/generations",
{
id: generationId,
name: generationName,
startTime: "2021-01-01T00:00:00.000Z",
endTime: "2021-01-01T00:00:00.000Z",
model: "model-name",
modelParameters: { key: "value" },
prompt: { key: "value" },
metadata: { key: "value" },
version: "2.0.0",
},
);
expect(createGeneration.status).toBe(200);
const updateGeneration = await makeZodVerifiedAPICall(
PatchGenerationsV1Response,
"PATCH",
"/api/public/generations",
{
generationId: generationId,
completion: "this is a great gpt response",
},
);
expect(updateGeneration.status).toBe(200);
const dbGeneration = await prisma.observation.findUnique({
where: {
id: generationId,
},
});
expect(dbGeneration?.id).toBe(generationId);
expect(dbGeneration?.name).toBe(generationName);
expect(dbGeneration?.startTime).toEqual(
new Date("2021-01-01T00:00:00.000Z"),
);
expect(dbGeneration?.endTime).toEqual(new Date("2021-01-01T00:00:00.000Z"));
expect(dbGeneration?.model).toBe("model-name");
expect(dbGeneration?.modelParameters).toEqual({ key: "value" });
expect(dbGeneration?.input).toEqual({ key: "value" });
expect(dbGeneration?.output).toEqual("this is a great gpt response");
expect(dbGeneration?.metadata).toEqual({ key: "value" });
expect(dbGeneration?.version).toBe("2.0.0");
});
it("should accept objects as i/o", async () => {
const generationName = uuidv4();
const generationId = uuidv4();
const createGeneration = await makeZodVerifiedAPICall(
PostGenerationsV1Body,
"POST",
"/api/public/generations",
{
id: generationId,
name: generationName,
prompt: { key: "value" },
completion: [
{
foo: "bar",
},
],
metadata: [
{
tags: ["example tag", "second tag"],
},
],
},
);
expect(createGeneration.status).toBe(200);
const dbGeneration = await prisma.observation.findUnique({
where: {
id: generationId,
},
});
expect(dbGeneration?.id).toBe(generationId);
expect(dbGeneration?.name).toBe(generationName);
expect(dbGeneration?.input).toEqual({ key: "value" });
expect(dbGeneration?.output).toEqual([
{
foo: "bar",
},
]);
expect(dbGeneration?.metadata).toEqual([
{
tags: ["example tag", "second tag"],
},
]);
});
it("should not succeed update if generation does not exist", async () => {
const generationId = uuidv4();
const updateGeneration = await makeAPICall(
"PATCH",
"/api/public/generations",
{
generationId: generationId,
completion: "this is a great gpt response",
},
);
expect(updateGeneration.status).toBe(404);
const dbGeneration = await prisma.observation.findUnique({
where: {
id: generationId,
},
});
expect(dbGeneration).toBeNull();
});
});
File diff suppressed because it is too large Load Diff
-198
View File
@@ -1,198 +0,0 @@
import {
makeZodVerifiedAPICall,
pruneDatabase,
} from "@/src/__tests__/test-utils";
import { PostGenerationsV1Response } from "@/src/features/public-api/types/generations";
import { GetMetricsDailyV1Response } from "@/src/features/public-api/types/metrics";
import { PostTracesV1Response } from "@/src/features/public-api/types/traces";
import { v4 as uuidv4 } from "uuid";
describe("/api/public/metrics/daily API Endpoint", () => {
beforeEach(async () => await pruneDatabase());
afterEach(async () => await pruneDatabase());
it("should handle daily metrics correctly", async () => {
// Create traces with observations on different days
const traceId1 = uuidv4();
const traceId2 = uuidv4();
await makeZodVerifiedAPICall(
PostTracesV1Response,
"POST",
"/api/public/traces",
{
id: traceId1,
timestamp: "2021-01-01T00:00:00.000Z",
name: "trace-day-1",
userId: "user-daily-metrics",
projectId: "project-daily-metrics",
},
);
await makeZodVerifiedAPICall(
PostTracesV1Response,
"POST",
"/api/public/traces",
{
id: traceId2,
timestamp: "2021-01-02T00:00:00.000Z",
name: "trace-day-2",
userId: "user-daily-metrics",
projectId: "project-daily-metrics",
},
);
// Simulate observations with usage metrics on different days
await makeZodVerifiedAPICall(
PostGenerationsV1Response,
"POST",
"/api/public/generations",
{
traceId: traceId1,
model: "modelA",
usage: { input: 100, output: 200, total: 300 },
startTime: "2021-01-01T00:00:00.000Z",
endTime: "2021-01-01T00:01:00.000Z",
},
);
await makeZodVerifiedAPICall(
PostGenerationsV1Response,
"POST",
"/api/public/generations",
{
traceId: traceId2,
model: "modelB",
usage: { input: 333 },
startTime: "2021-01-02T00:00:00.000Z",
endTime: "2021-01-02T00:02:00.000Z",
},
);
await makeZodVerifiedAPICall(
PostGenerationsV1Response,
"POST",
"/api/public/generations",
{
traceId: traceId2,
model: "modelC",
usage: { input: 666, output: 777, totalCost: 1024.22 },
startTime: "2021-01-02T00:00:00.000Z",
endTime: "2021-01-02T00:04:00.000Z",
},
);
await makeZodVerifiedAPICall(
PostGenerationsV1Response,
"POST",
"/api/public/generations",
{
traceId: traceId2,
usage: { output: 300 },
startTime: "2021-01-02T00:00:00.000Z",
endTime: "2021-01-02T00:04:00.000Z",
},
);
// Retrieve the daily metrics
const dailyMetricsResponse = await makeZodVerifiedAPICall(
GetMetricsDailyV1Response,
"GET",
`/api/public/metrics/daily`,
);
const dailyMetricsData = dailyMetricsResponse.body.data;
// Check if the daily metrics are calculated correctly
expect(dailyMetricsData).toHaveLength(2); // Two days of data
if (!dailyMetricsData[0])
throw new Error("dailyMetricsData[0] is undefined");
expect(dailyMetricsData[0].date).toBe("2021-01-02"); // Latest date first
expect(dailyMetricsData[0].countTraces).toBe(1);
expect(dailyMetricsData[0].totalCost).toEqual(1024.22);
expect(dailyMetricsData[0].usage).toEqual([
{
model: "modelB",
inputUsage: 333,
outputUsage: 0,
totalUsage: 333,
countObservations: 1,
countTraces: 1,
totalCost: 0,
},
{
model: "modelC",
inputUsage: 666,
outputUsage: 777,
totalUsage: 1443,
countObservations: 1,
countTraces: 1,
totalCost: 1024.22,
},
{
model: null,
countObservations: 1,
countTraces: 1,
inputUsage: 0,
outputUsage: 300,
totalCost: 0,
totalUsage: 300,
},
]);
if (!dailyMetricsData[1])
throw new Error("dailyMetricsData[1] is undefined");
expect(dailyMetricsData[1].date).toBe("2021-01-01");
expect(dailyMetricsData[1].countTraces).toBe(1);
expect(dailyMetricsData[1].totalCost).toEqual(0);
expect(dailyMetricsData[1].usage).toEqual([
{
model: "modelA",
inputUsage: 100,
outputUsage: 200,
totalUsage: 300,
countObservations: 1,
countTraces: 1,
totalCost: 0,
},
]);
});
it("should handle daily metrics correctly when there is no data", async () => {
// Retrieve the daily metrics
const dailyMetricsResponse = await makeZodVerifiedAPICall(
GetMetricsDailyV1Response,
"GET",
`/api/public/metrics/daily`,
);
const dailyMetricsData = dailyMetricsResponse.body.data;
// Check if the daily metrics are calculated correctly
expect(dailyMetricsData).toHaveLength(0); // No data
});
it("should handle daily metrics correctly when there is just a trace", async () => {
const traceId1 = uuidv4();
await makeZodVerifiedAPICall(
PostTracesV1Response,
"POST",
"/api/public/traces",
{
id: traceId1,
timestamp: "2021-01-01T00:00:00.000Z",
name: "trace-day-1",
userId: "user-daily-metrics",
projectId: "project-daily-metrics",
},
);
// Retrieve the daily metrics
const dailyMetricsResponse = await makeZodVerifiedAPICall(
GetMetricsDailyV1Response,
"GET",
`/api/public/metrics/daily`,
);
const dailyMetricsData = dailyMetricsResponse.body.data;
// Check if the daily metrics are calculated correctly
expect(dailyMetricsData).toHaveLength(1);
expect(dailyMetricsData[0].date).toBe("2021-01-01");
expect(dailyMetricsData[0].countTraces).toBe(1);
expect(dailyMetricsData[0].totalCost).toEqual(0);
expect(dailyMetricsData[0].usage).toEqual([]);
});
});
+7 -6
View File
@@ -9,6 +9,7 @@ import {
PromptType,
type LegacyValidatedPrompt,
} from "@/src/features/prompts/server/utils/validation";
import { getObservationById } from "@langfuse/shared/src/server";
describe("/api/public/prompts API Endpoint", () => {
beforeEach(async () => await pruneDatabase());
@@ -411,6 +412,7 @@ describe("/api/public/prompts API Endpoint", () => {
const generationId = v4();
const promptId = uuidv4();
const projectId = "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a";
await prisma.prompt.create({
data: {
@@ -420,7 +422,7 @@ describe("/api/public/prompts API Endpoint", () => {
labels: ["production"],
version: 1,
project: {
connect: { id: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a" },
connect: { id: projectId },
},
createdBy: "user-1",
},
@@ -459,11 +461,10 @@ describe("/api/public/prompts API Endpoint", () => {
expect(response.status).toBe(207);
const dbGeneration = await prisma.observation.findUnique({
where: {
id: generationId,
},
});
// Delay to allow for async processing
await new Promise((resolve) => setTimeout(resolve, 500));
const dbGeneration = await getObservationById(generationId, projectId);
expect(dbGeneration?.id).toBe(generationId);
expect(dbGeneration?.promptId).toBe(promptId);
+5 -5
View File
@@ -13,6 +13,7 @@ import {
import { nanoid } from "ai";
import { type PromptsMetaResponse } from "@/src/features/prompts/server/actions/getPromptsMeta";
import { getObservationById } from "@langfuse/shared/src/server";
const projectId = "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a";
const baseURI = "/api/public/v2/prompts";
@@ -395,11 +396,10 @@ describe("/api/public/v2/prompts API Endpoint", () => {
expect(response.status).toBe(207);
const dbGeneration = await prisma.observation.findUnique({
where: {
id: generationId,
},
});
// Delay to allow for async processing
await new Promise((resolve) => setTimeout(resolve, 500));
const dbGeneration = await getObservationById(generationId, projectId);
expect(dbGeneration?.id).toBe(generationId);
expect(dbGeneration?.promptId).toBe(promptId);
-791
View File
@@ -1,791 +0,0 @@
/** @jest-environment node */
import { prisma } from "@langfuse/shared/src/db";
import {
makeAPICall,
makeZodVerifiedAPICall,
pruneDatabase,
} from "@/src/__tests__/test-utils";
import { v4 as uuidv4 } from "uuid";
import { DeleteScoreResponse, GetScoreResponse } from "@langfuse/shared";
import { PostTracesV1Response } from "@/src/features/public-api/types/traces";
const traceId = "de98afa2-89dc-47e9-9924-33f1490fdaf4";
describe("/api/public/scores API Endpoint", () => {
let should_prune_db = true;
beforeEach(async () => {
if (should_prune_db) await pruneDatabase();
await makeZodVerifiedAPICall(
PostTracesV1Response,
"POST",
"/api/public/traces",
{
id: traceId,
name: "trace-name",
userId: "user-1",
projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
metadata: { key: "value" },
release: "1.0.0",
version: "2.0.0",
},
);
}, 10000);
afterEach(async () => {
if (should_prune_db) await pruneDatabase();
});
it("should create score for a trace", async () => {
const dbTrace = await prisma.trace.findMany({
where: {
id: traceId,
},
});
expect(dbTrace.length).toBeGreaterThan(0);
expect(dbTrace[0]?.id).toBe(traceId);
const scoreId = uuidv4();
const createScore = await makeAPICall("POST", "/api/public/scores", {
id: scoreId,
name: "score-name",
value: 100.5,
traceId: traceId,
comment: "comment",
});
expect(createScore.status).toBe(200);
const fetchedScore = await makeZodVerifiedAPICall(
GetScoreResponse,
"GET",
`/api/public/scores/${scoreId}`,
);
expect(fetchedScore.body?.id).toBe(scoreId);
expect(fetchedScore.body?.traceId).toBe(traceId);
expect(fetchedScore.body?.name).toBe("score-name");
expect(fetchedScore.body?.value).toBe(100.5);
expect(fetchedScore.body?.observationId).toBeNull();
expect(fetchedScore.body?.comment).toBe("comment");
expect(fetchedScore.body?.source).toBe("API");
expect(fetchedScore.body?.projectId).toBe(
"7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
);
});
it("should create score for a trace with int", async () => {
const dbTrace = await prisma.trace.findMany({
where: {
id: traceId,
},
});
expect(dbTrace.length).toBeGreaterThan(0);
expect(dbTrace[0]?.id).toBe(traceId);
const scoreId = uuidv4();
const createScore = await makeAPICall("POST", "/api/public/scores", {
id: scoreId,
name: "score-name",
value: 100,
traceId: traceId,
});
expect(createScore.status).toBe(200);
const fetchedScore = await makeZodVerifiedAPICall(
GetScoreResponse,
"GET",
`/api/public/scores/${scoreId}`,
);
expect(fetchedScore.body?.id).toBe(scoreId);
expect(fetchedScore.body?.traceId).toBe(traceId);
expect(fetchedScore.body?.name).toBe("score-name");
expect(fetchedScore.body?.value).toBe(100);
expect(fetchedScore.body?.observationId).toBeNull();
});
it("should create score for a generation", async () => {
await pruneDatabase();
const generationId = uuidv4();
await makeZodVerifiedAPICall(
PostTracesV1Response,
"POST",
"/api/public/generations",
{
id: generationId,
name: "generation-name",
startTime: "2021-01-01T00:00:00.000Z",
endTime: "2021-01-01T00:00:00.000Z",
model: "model-name",
modelParameters: { key: "value" },
prompt: { key: "value" },
metadata: { key: "value" },
version: "2.0.0",
},
);
const dbGeneration = await prisma.observation.findMany({
where: {
id: generationId,
},
});
expect(dbGeneration.length).toBeGreaterThan(0);
expect(dbGeneration[0]?.id).toBe(generationId);
const scoreId = uuidv4();
const scoreData = {
id: scoreId,
name: "score-name",
value: 100,
traceId: dbGeneration[0]!.traceId!,
observationId: dbGeneration[0]!.id,
};
const createScore = await makeAPICall(
"POST",
"/api/public/scores",
scoreData,
);
expect(createScore.status).toBe(200);
const fetchedScore = await makeZodVerifiedAPICall(
GetScoreResponse,
"GET",
`/api/public/scores/${scoreId}`,
);
expect(fetchedScore.body).toMatchObject(scoreData);
});
it("should create numeric score if value is integer and no data type is passed", async () => {
const dbTrace = await prisma.trace.findMany({
where: {
id: traceId,
},
});
expect(dbTrace.length).toBeGreaterThan(0);
expect(dbTrace[0]?.id).toBe(traceId);
const scoreId = uuidv4();
const scoreData = {
id: scoreId,
name: "accuracy",
value: 1,
traceId,
};
const createScore = await makeAPICall(
"POST",
"/api/public/scores",
scoreData,
);
expect(createScore.status).toBe(200);
const fetchedScore = await makeZodVerifiedAPICall(
GetScoreResponse,
"GET",
`/api/public/scores/${scoreId}`,
);
expect(fetchedScore.body).toMatchObject({
...scoreData,
dataType: "NUMERIC",
});
});
it("should create categorical score if value is string and no data type is passed", async () => {
const dbTrace = await prisma.trace.findMany({
where: {
id: traceId,
},
});
expect(dbTrace.length).toBeGreaterThan(0);
expect(dbTrace[0]?.id).toBe(traceId);
const scoreId = uuidv4();
const scoreData = {
id: scoreId,
name: "accuracy",
value: "Good",
traceId,
};
const createScore = await makeAPICall(
"POST",
"/api/public/scores",
scoreData,
);
expect(createScore.status).toBe(200);
const fetchedScore = await makeZodVerifiedAPICall(
GetScoreResponse,
"GET",
`/api/public/scores/${scoreId}`,
);
expect(fetchedScore.body).toMatchObject({
...scoreData,
value: null,
stringValue: "Good",
dataType: "CATEGORICAL",
});
});
it("should create boolean score if boolean data type is passed", async () => {
const dbTrace = await prisma.trace.findMany({
where: {
id: traceId,
},
});
expect(dbTrace.length).toBeGreaterThan(0);
expect(dbTrace[0]?.id).toBe(traceId);
const scoreId = uuidv4();
const scoreData = {
id: scoreId,
name: "score-name",
value: 1,
dataType: "BOOLEAN",
traceId,
};
const createScore = await makeAPICall(
"POST",
"/api/public/scores",
scoreData,
);
expect(createScore.status).toBe(200);
const fetchedScore = await makeZodVerifiedAPICall(
GetScoreResponse,
"GET",
`/api/public/scores/${scoreId}`,
);
expect(fetchedScore.body).toMatchObject({
...scoreData,
stringValue: "True",
});
});
it("should infer boolean data type from boolean score config", async () => {
const dbTrace = await prisma.trace.findMany({
where: {
id: traceId,
},
});
expect(dbTrace.length).toBeGreaterThan(0);
expect(dbTrace[0]?.id).toBe(traceId);
await makeAPICall("POST", "/api/public/score-configs", {
name: "accuracy",
dataType: "BOOLEAN",
});
const dbScoreConfig = await prisma.scoreConfig.findMany({
where: {
name: "accuracy",
},
});
expect(dbScoreConfig.length).toBeGreaterThan(0);
expect(dbScoreConfig[0]?.name).toBe("accuracy");
const scoreId = uuidv4();
const scoreData = {
id: scoreId,
name: "accuracy",
value: 1,
configId: dbScoreConfig[0].id,
traceId,
};
const createScore = await makeAPICall(
"POST",
"/api/public/scores",
scoreData,
);
expect(createScore.status).toBe(200);
const fetchedScore = await makeZodVerifiedAPICall(
GetScoreResponse,
"GET",
`/api/public/scores/${scoreId}`,
);
expect(fetchedScore.body).toMatchObject({
...scoreData,
stringValue: "True",
dataType: "BOOLEAN",
});
});
it("should NOT create categorical score if numeric data type is passed", async () => {
const dbTrace = await prisma.trace.findMany({
where: {
id: traceId,
},
});
expect(dbTrace.length).toBeGreaterThan(0);
expect(dbTrace[0]?.id).toBe(traceId);
const scoreId = uuidv4();
const scoreData = {
id: scoreId,
name: "accuracy",
value: 1,
dataType: "CATEGORICAL",
traceId,
};
const createScore = await makeAPICall(
"POST",
"/api/public/scores",
scoreData,
);
expect(createScore.status).toBe(400);
expect(createScore.body).toMatchObject({
message: "Invalid request data",
error: [
{
code: "invalid_type",
expected: "string",
message: "Expected string, received number",
path: ["value"],
received: "number",
},
],
});
});
it("should NOT create numeric score if categorical data type is passed incl numeric config", async () => {
const dbTrace = await prisma.trace.findMany({
where: {
id: traceId,
},
});
expect(dbTrace.length).toBeGreaterThan(0);
expect(dbTrace[0]?.id).toBe(traceId);
await makeAPICall("POST", "/api/public/score-configs", {
name: "accuracy",
dataType: "NUMERIC",
});
const dbScoreConfig = await prisma.scoreConfig.findMany({
where: {
name: "accuracy",
},
});
expect(dbScoreConfig.length).toBeGreaterThan(0);
expect(dbScoreConfig[0]?.name).toBe("accuracy");
const scoreId = uuidv4();
const scoreData = {
id: scoreId,
name: "accuracy",
value: "Good",
configId: dbScoreConfig[0].id,
dataType: "CATEGORICAL",
traceId,
};
const createScore = await makeAPICall(
"POST",
"/api/public/scores",
scoreData,
);
expect(createScore.status).toBe(400);
expect(createScore.body).toMatchObject({
message:
"Data type mismatch based on config: expected NUMERIC, got CATEGORICAL",
});
});
it("should NOT create numeric score if config and passed data type mismatch", async () => {
const dbTrace = await prisma.trace.findMany({
where: {
id: traceId,
},
});
expect(dbTrace.length).toBeGreaterThan(0);
expect(dbTrace[0]?.id).toBe(traceId);
await makeAPICall("POST", "/api/public/score-configs", {
name: "accuracy",
dataType: "CATEGORICAL",
categories: [
{ label: "One", value: 1 },
{ label: "Zero", value: 0 },
],
});
const dbScoreConfig = await prisma.scoreConfig.findMany({
where: {
name: "accuracy",
},
});
expect(dbScoreConfig.length).toBeGreaterThan(0);
expect(dbScoreConfig[0]?.name).toBe("accuracy");
const scoreId = uuidv4();
const scoreData = {
id: scoreId,
name: "accuracy",
value: 1,
configId: dbScoreConfig[0].id,
dataType: "NUMERIC",
traceId,
};
const createScore = await makeAPICall(
"POST",
"/api/public/scores",
scoreData,
);
expect(createScore.status).toBe(400);
expect(createScore.body).toMatchObject({
message:
"Data type mismatch based on config: expected CATEGORICAL, got NUMERIC",
});
});
it("should NOT create boolean score if string value is passed", async () => {
const dbTrace = await prisma.trace.findMany({
where: {
id: traceId,
},
});
expect(dbTrace.length).toBeGreaterThan(0);
expect(dbTrace[0]?.id).toBe(traceId);
await makeAPICall("POST", "/api/public/score-configs", {
name: "accuracy",
dataType: "BOOLEAN",
});
const dbScoreConfig = await prisma.scoreConfig.findMany({
where: {
name: "accuracy",
},
});
expect(dbScoreConfig.length).toBeGreaterThan(0);
expect(dbScoreConfig[0]?.name).toBe("accuracy");
const scoreId = uuidv4();
const scoreData = {
id: scoreId,
name: "accuracy",
value: "True",
configId: dbScoreConfig[0].id,
dataType: "BOOLEAN",
traceId,
};
const createScore = await makeAPICall(
"POST",
"/api/public/scores",
scoreData,
);
expect(createScore.status).toBe(400);
expect(createScore.body).toMatchObject({
message: "Invalid request data",
error: [
{
code: "invalid_type",
expected: "number",
message: "Expected number, received string",
path: ["value"],
received: "string",
},
],
});
});
it("should NOT create boolean score with value other than 1 | 0", async () => {
const dbTrace = await prisma.trace.findMany({
where: {
id: traceId,
},
});
expect(dbTrace.length).toBeGreaterThan(0);
expect(dbTrace[0]?.id).toBe(traceId);
await makeAPICall("POST", "/api/public/score-configs", {
name: "accuracy",
dataType: "BOOLEAN",
});
const dbScoreConfig = await prisma.scoreConfig.findMany({
where: {
name: "accuracy",
},
});
expect(dbScoreConfig.length).toBeGreaterThan(0);
expect(dbScoreConfig[0]?.name).toBe("accuracy");
const scoreId = uuidv4();
const scoreData = {
id: scoreId,
name: "accuracy",
value: 0.5,
configId: dbScoreConfig[0].id,
dataType: "BOOLEAN",
traceId,
};
const createScore = await makeAPICall(
"POST",
"/api/public/scores",
scoreData,
);
expect(createScore.status).toBe(400);
expect(createScore.body).toMatchObject({
message: "Invalid request data",
error: [
{
code: "custom",
message:
"Value must be a number equal to either 0 or 1 for data type BOOLEAN",
path: ["value"],
},
],
});
});
it("should NOT create categorical score with value not defined in config.categories", async () => {
const dbTrace = await prisma.trace.findMany({
where: {
id: traceId,
},
});
expect(dbTrace.length).toBeGreaterThan(0);
expect(dbTrace[0]?.id).toBe(traceId);
await makeAPICall("POST", "/api/public/score-configs", {
name: "accuracy",
dataType: "CATEGORICAL",
categories: [
{ label: "One", value: 1 },
{ label: "Zero", value: 0 },
],
});
const dbScoreConfig = await prisma.scoreConfig.findMany({
where: {
name: "accuracy",
},
});
expect(dbScoreConfig.length).toBeGreaterThan(0);
expect(dbScoreConfig[0]?.name).toBe("accuracy");
const scoreId = uuidv4();
const scoreData = {
id: scoreId,
name: "accuracy",
value: 1,
configId: dbScoreConfig[0].id,
dataType: "CATEGORICAL",
traceId,
};
const createScore = await makeAPICall(
"POST",
"/api/public/scores",
scoreData,
);
expect(createScore.status).toBe(400);
expect(createScore.body).toMatchObject({
message: "Invalid request data",
error: [
{
code: "invalid_type",
expected: "string",
message: "Expected string, received number",
path: ["value"],
received: "number",
},
],
});
});
it("should NOT create numeric score outside of defined range", async () => {
const dbTrace = await prisma.trace.findMany({
where: {
id: traceId,
},
});
expect(dbTrace.length).toBeGreaterThan(0);
expect(dbTrace[0]?.id).toBe(traceId);
await makeAPICall("POST", "/api/public/score-configs", {
name: "accuracy",
dataType: "NUMERIC",
maxValue: 0,
});
const dbScoreConfig = await prisma.scoreConfig.findMany({
where: {
name: "accuracy",
},
});
expect(dbScoreConfig.length).toBeGreaterThan(0);
expect(dbScoreConfig[0]?.name).toBe("accuracy");
const scoreId = uuidv4();
const scoreData = {
id: scoreId,
name: "accuracy",
value: 0.5,
configId: dbScoreConfig[0].id,
dataType: "NUMERIC",
traceId,
};
const createScore = await makeAPICall(
"POST",
"/api/public/scores",
scoreData,
);
expect(createScore.status).toBe(400);
expect(createScore.body).toMatchObject({
message:
"Ingested score body not valid against provided config: - Value exceeds maximum value of 0 defined in config",
});
});
it("should upsert a score", async () => {
const dbTrace = await prisma.trace.findMany({
where: {
id: traceId,
},
});
expect(dbTrace.length).toBeGreaterThan(0);
expect(dbTrace[0]?.id).toBe(traceId);
const generationId = uuidv4();
await makeZodVerifiedAPICall(
PostTracesV1Response,
"POST",
"/api/public/generations",
{
id: generationId,
name: "generation-name",
traceId,
startTime: "2021-01-01T00:00:00.000Z",
endTime: "2021-01-01T00:00:00.000Z",
model: "model-name",
modelParameters: { key: "value" },
prompt: { key: "value" },
metadata: { key: "value" },
version: "2.0.0",
},
);
const dbGeneration = await prisma.observation.findMany({
where: {
id: generationId,
},
});
expect(dbGeneration.length).toBeGreaterThan(0);
expect(dbGeneration[0]?.id).toBe(generationId);
const scoreId = uuidv4();
const createScore = await makeAPICall("POST", "/api/public/scores", {
id: scoreId,
name: "score-name",
value: 100,
traceId: traceId,
comment: "comment",
});
expect(createScore.status).toBe(200);
const upsertScore = await makeAPICall("POST", "/api/public/scores", {
id: scoreId,
traceId: traceId,
name: "score-name-updated",
value: 200,
comment: "comment-updated",
observationId: dbGeneration[0]!.id,
});
expect(upsertScore.status).toBe(200);
const fetchedScore = await makeZodVerifiedAPICall(
GetScoreResponse,
"GET",
`/api/public/scores/${scoreId}`,
);
expect(fetchedScore.body?.id).toBe(scoreId);
expect(fetchedScore.body?.traceId).toBe(traceId);
expect(fetchedScore.body?.name).toBe("score-name-updated");
expect(fetchedScore.body?.value).toBe(200);
expect(fetchedScore.body?.comment).toBe("comment-updated");
expect(fetchedScore.body?.observationId).toBe(dbGeneration[0]!.id);
});
it("should delete a score", async () => {
await pruneDatabase();
const traceId = uuidv4();
await makeZodVerifiedAPICall(
PostTracesV1Response,
"POST",
"/api/public/traces",
{
id: traceId,
},
);
const scoreId = uuidv4();
const createScore = await makeAPICall("POST", "/api/public/scores", {
id: scoreId,
name: "score-name",
value: 100.5,
traceId: traceId,
comment: "comment",
});
expect(createScore.status).toBe(200);
const fetchedScore = await makeZodVerifiedAPICall(
GetScoreResponse,
"GET",
`/api/public/scores/${scoreId}`,
);
expect(fetchedScore.body.id).toBe(scoreId);
const deleteScore = await makeZodVerifiedAPICall(
DeleteScoreResponse,
"DELETE",
`/api/public/scores/${scoreId}`,
);
expect(deleteScore.status).toBe(200);
const deletedScore = await prisma.score.findUnique({
where: {
id: scoreId,
},
});
expect(deletedScore).toBeNull();
});
});
@@ -1,145 +0,0 @@
/** @jest-environment node */
import { pruneDatabase } from "@/src/__tests__/test-utils";
import { appRouter } from "@/src/server/api/root";
import { createInnerTRPCContext } from "@/src/server/api/trpc";
import { prisma } from "@langfuse/shared/src/db";
import type { Session } from "next-auth";
describe("Traces TRPC Router", () => {
beforeEach(async () => await pruneDatabase());
afterEach(async () => await pruneDatabase());
const session: Session = {
expires: "1",
user: {
id: "clgb17vnp000008jjere5g15i",
name: "John Doe",
organizations: [
{
id: "seed-org-id",
role: "OWNER",
plan: "cloud:hobby",
cloudConfig: undefined,
projects: [
{
id: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
role: "ADMIN",
},
],
},
],
featureFlags: {
templateFlag: true,
},
admin: true,
},
environment: {} as any,
};
const ctx = createInnerTRPCContext({ session });
const caller = appRouter.createCaller({ ...ctx, prisma });
test("traces.all RPC returns an array of traces", async () => {
const trace = {
name: "trace-name",
userId: "user-1",
release: "1.0.0",
version: "2.0.0",
};
await prisma.trace.create({
data: { ...trace, projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a" },
});
const traces = await caller.traces.all({
page: 0,
limit: 10,
// projectId from `seed.ts`
projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
filter: null,
searchQuery: "",
orderBy: null,
});
expect(traces).toBeDefined();
expect(traces).toMatchObject({ traces: [trace] });
});
test("traces.all RPC must not return input, output, metadata", async () => {
const trace = {
name: "trace-name",
userId: "user-1",
input: { a: 1 },
output: { b: 2 },
metadata: { c: 3 },
release: "1.0.0",
version: "2.0.0",
};
await prisma.trace.create({
data: { ...trace, projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a" },
});
const traces = await caller.traces.all({
page: 0,
limit: 10,
// projectId from `seed.ts`
projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
filter: null,
searchQuery: "",
orderBy: null,
});
expect(traces.traces).toBeDefined();
expect(traces.traces).toHaveLength(1);
const returnedTrace = traces.traces[0];
expect(returnedTrace).not.toHaveProperty("input");
expect(returnedTrace).not.toHaveProperty("output");
expect(returnedTrace).not.toHaveProperty("metadata");
});
test("traces.all RPC orders traces by userId", async () => {
const traceTmpl = {
name: "trace-name",
userId: "user-1",
release: "1.0.0",
version: "2.0.0",
};
const trace1 = traceTmpl;
const trace2 = { ...traceTmpl, userId: "user-2" };
await prisma.trace.create({
data: { ...traceTmpl, projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a" },
});
await prisma.trace.create({
data: {
...trace2,
projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
},
});
const tracesASC = await caller.traces.all({
page: 0,
limit: 10,
// projectId from `seed.ts`
projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
filter: null,
searchQuery: "",
orderBy: {
column: "userId",
order: "ASC",
},
});
expect(tracesASC).toMatchObject({ traces: [trace1, trace2] });
const tracesDESC = await caller.traces.all({
page: 0,
limit: 10,
// projectId from `seed.ts`
projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
filter: null,
searchQuery: "",
orderBy: {
column: "userId",
order: "DESC",
},
});
expect(tracesDESC).toMatchObject({ traces: [trace2, trace1] });
});
});
+3
View File
@@ -25,6 +25,9 @@ describe("Create and get sessions", () => {
},
);
// Delay to allow for async processing
await new Promise((resolve) => setTimeout(resolve, 500));
const dbSession = await prisma.traceSession.findFirst({
where: {
id: "session-id",
-376
View File
@@ -1,376 +0,0 @@
/** @jest-environment node */
import { prisma } from "@langfuse/shared/src/db";
import {
makeAPICall,
makeZodVerifiedAPICall,
pruneDatabase,
} from "@/src/__tests__/test-utils";
import { v4 as uuidv4 } from "uuid";
import { PostTracesV1Response } from "@/src/features/public-api/types/traces";
import {
PatchSpansV1Response,
PostSpansV1Response,
} from "@/src/features/public-api/types/spans";
describe("/api/public/spans API Endpoint", () => {
beforeEach(async () => await pruneDatabase());
afterEach(async () => await pruneDatabase());
it("should create span after trace", async () => {
const traceId = uuidv4();
await makeZodVerifiedAPICall(
PostTracesV1Response,
"POST",
"/api/public/traces",
{
id: traceId,
name: "trace-name",
userId: "user-1",
projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
metadata: { key: "value" },
release: "1.0.0",
version: "2.0.0",
},
);
const dbTrace = await prisma.trace.findMany({
where: {
id: traceId,
},
});
expect(dbTrace.length).toBeGreaterThan(0);
expect(dbTrace[0]?.id).toBe(traceId);
const spanId = uuidv4();
const createSpan = await makeZodVerifiedAPICall(
PostSpansV1Response,
"POST",
"/api/public/spans",
{
id: spanId,
traceId: traceId,
name: "span-name",
startTime: "2021-01-01T00:00:00.000Z",
endTime: "2021-01-01T00:00:00.000Z",
input: { input: "value" },
metadata: { meta: "value" },
version: "2.0.0",
},
);
expect(createSpan.status).toBe(200);
const dbSpan = await prisma.observation.findUnique({
where: {
id: spanId,
},
});
expect(dbSpan?.id).toBe(spanId);
expect(dbSpan?.traceId).toBe(traceId);
expect(dbSpan?.name).toBe("span-name");
expect(dbSpan?.startTime).toEqual(new Date("2021-01-01T00:00:00.000Z"));
expect(dbSpan?.endTime).toEqual(new Date("2021-01-01T00:00:00.000Z"));
expect(dbSpan?.input).toEqual({ input: "value" });
expect(dbSpan?.metadata).toEqual({ meta: "value" });
expect(dbSpan?.version).toBe("2.0.0");
});
it("should create span before trace", async () => {
const traceId = uuidv4();
const spanId = uuidv4();
const createSpan = await makeZodVerifiedAPICall(
PostSpansV1Response,
"POST",
"/api/public/spans",
{
id: spanId,
traceId: traceId,
name: "span-name",
startTime: "2021-01-01T00:00:00.000Z",
endTime: "2021-01-01T00:00:00.000Z",
input: { input: "value" },
metadata: { meta: "value" },
version: "2.0.0",
},
);
expect(createSpan.status).toBe(200);
const dbSpan = await prisma.observation.findUnique({
where: {
id: spanId,
},
});
expect(dbSpan?.id).toBe(spanId);
expect(dbSpan?.traceId).toBe(traceId);
expect(dbSpan?.name).toBe("span-name");
expect(dbSpan?.startTime).toEqual(new Date("2021-01-01T00:00:00.000Z"));
expect(dbSpan?.endTime).toEqual(new Date("2021-01-01T00:00:00.000Z"));
expect(dbSpan?.input).toEqual({ input: "value" });
expect(dbSpan?.metadata).toEqual({ meta: "value" });
expect(dbSpan?.version).toBe("2.0.0");
await makeZodVerifiedAPICall(
PostTracesV1Response,
"POST",
"/api/public/traces",
{
id: traceId,
name: "trace-name",
userId: "user-1",
projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
metadata: { key: "value" },
release: "1.0.0",
version: "2.0.0",
},
);
const dbTrace = await prisma.trace.findMany({
where: {
id: traceId,
},
});
expect(dbTrace.length).toBeGreaterThan(0);
expect(dbTrace[0]?.id).toBe(traceId);
});
it("should create span after trace ignoring externalId", async () => {
const traceId = uuidv4();
const spanId = uuidv4();
const response = await makeZodVerifiedAPICall(
PostTracesV1Response,
"POST",
"/api/public/traces",
{
externalId: uuidv4(),
id: traceId,
name: "trace-name",
userId: "user-1",
projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
},
);
expect(response.status).toBe(200);
const createSpan = await makeZodVerifiedAPICall(
PostSpansV1Response,
"POST",
"/api/public/spans",
{
id: spanId,
name: "spanName",
projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
traceId: traceId,
externalTraceIdType: "EXTERNAL",
},
);
expect(createSpan.status).toBe(200);
const dbSpan = await prisma.observation.findFirstOrThrow({
where: {
name: "spanName",
},
});
expect(dbSpan.id).toBe(spanId);
expect(dbSpan.traceId).toBe(traceId);
});
it("should create trace when creating span without existing trace", async () => {
const spanName = uuidv4();
const spanId = uuidv4();
const createSpan = await makeZodVerifiedAPICall(
PostSpansV1Response,
"POST",
"/api/public/spans",
{
id: spanId,
name: spanName,
startTime: "2021-01-01T00:00:00.000Z",
endTime: "2021-01-01T00:00:00.000Z",
input: { input: "value" },
metadata: { meta: "value" },
version: "2.0.0",
},
);
const dbTrace = await prisma.trace.findMany({
where: {
name: spanName,
},
});
expect(dbTrace.length).toBe(1);
expect(dbTrace[0]?.name).toBe(spanName);
expect(createSpan.status).toBe(200);
const dbSpan = await prisma.observation.findUnique({
where: {
id: spanId,
},
});
expect(dbSpan?.id).toBe(spanId);
expect(dbSpan?.traceId).toBe(dbTrace[0]?.id);
expect(dbSpan?.name).toBe(spanName);
expect(dbSpan?.startTime).toEqual(new Date("2021-01-01T00:00:00.000Z"));
expect(dbSpan?.endTime).toEqual(new Date("2021-01-01T00:00:00.000Z"));
expect(dbSpan?.input).toEqual({ input: "value" });
expect(dbSpan?.metadata).toEqual({ meta: "value" });
expect(dbSpan?.version).toBe("2.0.0");
});
it("should create trace when creating span without existing trace with externalId", async () => {
const spanName = uuidv4();
const spanId = uuidv4();
const traceId = uuidv4();
const createSpan = await makeZodVerifiedAPICall(
PostSpansV1Response,
"POST",
"/api/public/spans",
{
id: spanId,
traceIdType: "EXTERNAL",
traceId: traceId,
name: spanName,
startTime: "2021-01-01T00:00:00.000Z",
endTime: "2021-01-01T00:00:00.000Z",
input: { input: "value" },
metadata: { meta: "value" },
version: "2.0.0",
},
);
expect(createSpan.status).toBe(200);
const dbSpan = await prisma.observation.findMany({
where: {
name: spanName,
},
});
expect(dbSpan.length).toBe(1);
expect(dbSpan[0]?.name).toBe(spanName);
expect(dbSpan[0]?.id).toBe(spanId);
expect(dbSpan[0]?.traceId).toBe(traceId);
const dbTraces = await prisma.trace.findMany();
expect(dbTraces.length).toBe(0);
});
it("should create trace when creating span without existing trace without traceId", async () => {
const generationName = uuidv4();
const spanId = uuidv4();
const createSpan = await makeZodVerifiedAPICall(
PostSpansV1Response,
"POST",
"/api/public/spans",
{
id: spanId,
name: generationName,
startTime: "2021-01-01T00:00:00.000Z",
endTime: "2021-01-01T00:00:00.000Z",
input: { key: "value" },
metadata: { key: "value" },
version: "2.0.0",
},
);
const dbSpan = await prisma.observation.findFirstOrThrow({
where: {
name: generationName,
},
});
const dbTrace = await prisma.trace.findMany({
where: {
id: dbSpan.traceId!,
},
});
expect(dbTrace.length).toBe(1);
expect(dbTrace[0]?.name).toBe(generationName);
expect(createSpan.status).toBe(200);
expect(dbSpan.id).toBe(spanId);
expect(dbSpan.traceId).toBe(dbTrace[0]?.id);
expect(dbSpan.name).toBe(generationName);
expect(dbSpan.startTime).toEqual(new Date("2021-01-01T00:00:00.000Z"));
expect(dbSpan.endTime).toEqual(new Date("2021-01-01T00:00:00.000Z"));
expect(dbSpan.input).toEqual({ key: "value" });
expect(dbSpan.metadata).toEqual({ key: "value" });
expect(dbSpan.version).toBe("2.0.0");
});
it("should update span", async () => {
const spanName = uuidv4();
const spanId = uuidv4();
const createSpan = await makeZodVerifiedAPICall(
PostSpansV1Response,
"POST",
"/api/public/spans",
{
id: spanId,
name: spanName,
startTime: "2021-01-01T00:00:00.000Z",
endTime: "2021-01-01T00:00:00.000Z",
input: { input: "value" },
metadata: { meta: "value" },
version: "2.0.0",
},
);
expect(createSpan.status).toBe(200);
const updatedSpan = await makeZodVerifiedAPICall(
PatchSpansV1Response,
"PATCH",
"/api/public/spans",
{
spanId: spanId,
output: { key: "this is a great gpt output" },
},
);
expect(updatedSpan.status).toBe(200);
const dbSpan = await prisma.observation.findUnique({
where: {
id: spanId,
},
});
expect(dbSpan?.id).toBe(spanId);
expect(dbSpan?.name).toBe(spanName);
expect(dbSpan?.startTime).toEqual(new Date("2021-01-01T00:00:00.000Z"));
expect(dbSpan?.endTime).toEqual(new Date("2021-01-:00:00.000Z"));
expect(dbSpan?.input).toEqual({ input: "value" });
expect(dbSpan?.output).toEqual({ key: "this is a great gpt output" });
expect(dbSpan?.metadata).toEqual({ meta: "value" });
expect(dbSpan?.version).toBe("2.0.0");
});
it("should not upsert span if span does not exist", async () => {
const spanId = uuidv4();
const updatedSpan = await makeAPICall("PATCH", "/api/public/spans", {
spanId: spanId,
output: { key: "this is a great gpt output" },
});
expect(updatedSpan.status).toBe(404);
const dbSpan = await prisma.observation.findUnique({
where: {
id: spanId,
},
});
expect(dbSpan).toBeNull();
});
});
File diff suppressed because it is too large Load Diff
-358
View File
@@ -1,358 +0,0 @@
/** @jest-environment node */
import { v4 as uuidv4 } from "uuid";
import {
makeZodVerifiedAPICall,
pruneDatabase,
} from "@/src/__tests__/test-utils";
import { PostGenerationsV1Response } from "@/src/features/public-api/types/generations";
import {
GetTracesV1Response,
GetTraceV1Response,
PostTracesV1Response,
} from "@/src/features/public-api/types/traces";
import { PostScoresResponse } from "@langfuse/shared";
import { prisma } from "@langfuse/shared/src/db";
describe("/api/public/traces API Endpoint", () => {
beforeEach(async () => await pruneDatabase());
afterEach(async () => await pruneDatabase());
it("should upsert second trace", async () => {
await pruneDatabase();
await makeZodVerifiedAPICall(
PostTracesV1Response,
"POST",
"/api/public/traces",
{
id: "trace-id",
name: "trace-name",
userId: "user-1",
metadata: { key: "value" },
release: "1.0.0",
version: "2.0.0",
public: true,
},
);
const dbTrace1 = await prisma.trace.findFirst({
where: {
id: "trace-id",
},
});
expect(dbTrace1).not.toBeNull();
expect(dbTrace1).toMatchObject({
name: "trace-name",
release: "1.0.0",
externalId: null,
version: "2.0.0",
public: true,
userId: "user-1",
});
await makeZodVerifiedAPICall(
PostTracesV1Response,
"POST",
"/api/public/traces",
{
id: "trace-id",
metadata: { key: "value" },
timestamp: "2021-01-01T00:00:00.000Z",
release: "1.0.0",
version: "5.0.0",
public: false,
},
);
const dbTrace2 = await prisma.trace.findFirst({
where: {
name: "trace-name",
},
});
expect(dbTrace2).not.toBeNull();
expect(dbTrace2).toMatchObject({
name: "trace-name",
release: "1.0.0",
externalId: null,
version: "5.0.0",
public: false,
userId: "user-1",
timestamp: new Date("2021-01-01T00:00:00.000Z"),
});
});
it("should use tags correctly on POST and GET", async () => {
await pruneDatabase();
await makeZodVerifiedAPICall(
PostTracesV1Response,
"POST",
"/api/public/traces",
{
id: "trace-1",
tags: ["tag-1", "tag-2", "tag-3"],
},
);
await makeZodVerifiedAPICall(
PostTracesV1Response,
"POST",
"/api/public/traces",
{
id: "trace-2",
tags: ["tag-1"],
},
);
await makeZodVerifiedAPICall(
PostTracesV1Response,
"POST",
"/api/public/traces",
{
id: "trace-3",
tags: ["tag-2", "tag-3"],
},
);
// multiple tags
const traces = await makeZodVerifiedAPICall(
GetTracesV1Response,
"GET",
"/api/public/traces?tags=tag-2&tags=tag-3",
);
const traceIds = traces.body.data.map((t) => t.id);
// check for equality ok as ordered by timestamp
expect(traceIds).toEqual(["trace-3", "trace-1"]);
// single tag
const traces2 = await makeZodVerifiedAPICall(
GetTracesV1Response,
"GET",
"/api/public/traces?tags=tag-1",
);
const traceIds2 = traces2.body.data.map((t) => t.id);
// check for equality ok as ordered by timestamp
expect(traceIds2).toEqual(["trace-2", "trace-1"]);
// wrong tag
const traces3 = await makeZodVerifiedAPICall(
GetTracesV1Response,
"GET",
"/api/public/traces?tags=tag-10",
);
const traceIds3 = traces3.body.data.map((t) => t.id);
// check for equality ok as ordered by timestamp
expect(traceIds3).toEqual([]);
// no tag
const traces4 = await makeZodVerifiedAPICall(
GetTracesV1Response,
"GET",
"/api/public/traces?tags=",
);
const traceIds4 = traces4.body.data.map((t) => t.id);
// check for equality ok as ordered by timestamp
expect(traceIds4).toEqual(["trace-3", "trace-2", "trace-1"]);
});
it("should handle metrics correctly on GET traces and GET trace", async () => {
await pruneDatabase();
// Create a trace with some observations that have costs and latencies
const traceId = uuidv4();
await makeZodVerifiedAPICall(
PostTracesV1Response,
"POST",
"/api/public/traces",
{
id: traceId,
name: "trace-with-costs",
userId: "user-costs",
projectId: "project-costs",
metadata: { key: "value" },
release: "1.0.0",
version: "2.0.0",
},
);
console.log(traceId);
// Simulate observations with costs and latencies
const generationId = uuidv4();
await makeZodVerifiedAPICall(
PostGenerationsV1Response,
"POST",
"/api/public/generations",
{
traceId: traceId,
id: generationId,
name: "Generation1",
usage: { totalCost: 10.5 },
startTime: "2021-01-01T00:00:00.000Z",
endTime: "2021-01-01T00:10:00.000Z",
},
);
await makeZodVerifiedAPICall(
PostGenerationsV1Response,
"POST",
"/api/public/generations",
{
traceId: traceId,
usage: { totalCost: 5.25 },
startTime: "2021-01-01T00:10:00.000Z",
endTime: "2021-01-01T00:20:00.000Z",
},
);
// Simulate scores on the trace
const scoreId1 = uuidv4();
await makeZodVerifiedAPICall(
PostScoresResponse,
"POST",
"/api/public/scores",
{
id: scoreId1,
name: "score-1",
value: 75.0,
traceId: traceId,
comment: "First score",
},
);
const scoreId2 = uuidv4();
await makeZodVerifiedAPICall(
PostScoresResponse,
"POST",
"/api/public/scores",
{
id: scoreId2,
name: "score-2",
value: 85.5,
traceId: traceId,
comment: "Second score",
},
);
const scoreId3 = uuidv4();
await makeZodVerifiedAPICall(
PostScoresResponse,
"POST",
"/api/public/scores",
{
id: scoreId3,
name: "score-3",
value: 95.0,
traceId: traceId,
comment: "Third score",
},
);
// GET traces
// Retrieve the trace with totalCost and latency
const traces = await makeZodVerifiedAPICall(
GetTracesV1Response,
"GET",
`/api/public/traces`,
);
const traceData = traces.body.data[0];
if (!traceData) throw new Error("traceData is undefined");
// Check if the totalCost and latency are calculated correctly
expect(traceData.totalCost).toBeCloseTo(15.75); // Sum of costs
expect(traceData.latency).toBeCloseTo(1200); // Difference in seconds between min startTime and max endTime
expect(traceData.id).toBe(traceId);
expect(traceData.htmlPath).toContain(`/traces/${traceId}`);
expect(traceData.htmlPath).toContain(`/project/`); // do not know the projectId
// GET trace
// Retrieve the trace with total
const trace = await makeZodVerifiedAPICall(
GetTraceV1Response,
"GET",
`/api/public/traces/${traceId}`,
);
console.log(trace.body);
expect(trace.body.totalCost).toBeCloseTo(15.75);
expect(trace.body.id).toBe(traceId);
expect(trace.body.id).toBe(traceId);
expect(trace.body.htmlPath).toContain(`/traces/${traceId}`);
expect(trace.body.htmlPath).toContain(`/project/`); // do not know the projectId
expect(trace.body.scores).toHaveLength(3);
expect(trace.body.scores[0].id).toBe(scoreId3);
expect(trace.body.scores[0].name).toBe("score-3");
expect(trace.body.observations).toHaveLength(2);
expect(trace.body.observations[0].id).toBe(generationId);
expect(trace.body.observations[0].name).toBe("Generation1");
});
it("should filter traces by session ID", async () => {
const sessionId = "test-session-id";
const anotherSessionId = "another-session-id";
// Create traces with different session IDs
await makeZodVerifiedAPICall(
PostTracesV1Response,
"POST",
"/api/public/traces",
{
id: "trace-1",
name: "test-trace-1",
sessionId,
userId: "user-1",
projectId: "project-1",
metadata: { key: "value" },
release: "1.0.0",
version: "1.0.0",
},
);
await makeZodVerifiedAPICall(
PostTracesV1Response,
"POST",
"/api/public/traces",
{
id: "trace-2",
name: "test-trace-2",
sessionId: anotherSessionId,
userId: "user-2",
projectId: "project-1",
metadata: { key: "value" },
release: "1.0.0",
version: "1.0.0",
},
);
// Filter by session ID
const tracesBySessionId = await makeZodVerifiedAPICall(
GetTracesV1Response,
"GET",
`/api/public/traces?sessionId=${sessionId}`,
);
expect(tracesBySessionId.status).toBe(200);
expect(tracesBySessionId.body.data).toHaveLength(1);
expect(tracesBySessionId.body.data[0].id).toBe("trace-1");
// Filter by another session ID
const tracesByAnotherSessionId = await makeZodVerifiedAPICall(
GetTracesV1Response,
"GET",
`/api/public/traces?sessionId=${anotherSessionId}`,
);
expect(tracesByAnotherSessionId.status).toBe(200);
expect(tracesByAnotherSessionId.body.data).toHaveLength(1);
expect(tracesByAnotherSessionId.body.data[0].id).toBe("trace-2");
// Filter by non-existent session ID
const tracesByNonExistentSessionId = await makeZodVerifiedAPICall(
GetTracesV1Response,
"GET",
`/api/public/traces?sessionId=non-existent-session-id`,
);
expect(tracesByNonExistentSessionId.status).toBe(200);
expect(tracesByNonExistentSessionId.body.data).toHaveLength(0);
});
});
+30 -1
View File
@@ -1,4 +1,4 @@
import { paginationZod } from "@langfuse/shared";
import { paginationZod, parseJsonPrioritised } from "@langfuse/shared";
import { ZodError } from "zod";
// Create test cases
@@ -24,3 +24,32 @@ describe("Pagination Zod Schema", () => {
expect(() => paginationZod.limit.parse("abc")).toThrowError(ZodError);
});
});
describe("parseJsonPrioritised", () => {
it.each([
["test", "test"], // Raw string
['{"hello": "world"}', { hello: "world" }], // Simple object
["[1, 2, 3]", [1, 2, 3]], // Array
['{"nested": {"key": "value"}}', { nested: { key: "value" } }], // Nested object
["[]", []], // Empty array
["{}", {}], // Empty object
['"simple string"', "simple string"], // Quoted string
["42", 42], // Number
["true", true], // Boolean true
["false", false], // Boolean false
["null", null], // Null
["", ""], // Empty string
["invalid{json}", "invalid{json}"], // Invalid JSON string
['{"hello": "world"', '{"hello": "world"'], // Invalid JSON string
['[null, 123, "abc"]', [null, 123, "abc"]], // Mixed array
[
'{"array": [1, 2], "nested": {"key": "value"}}',
{ array: [1, 2], nested: { key: "value" } },
], // Complex object
])(
"should parse input correctly (%s, %s)",
(input: string, expectedOutput: any) => {
expect(parseJsonPrioritised(input)).toEqual(expectedOutput);
},
);
});
@@ -37,12 +37,17 @@ export const BatchExportTableButton: React.FC<BatchExportTableButtonProps> = (
showSuccessToast({
title: "Export queued",
description: "You will receive an email when the export is ready.",
duration: 10000,
link: {
href: `/project/${props.projectId}/settings/exports`,
text: "View exports",
},
});
},
});
const hasAccess = useHasProjectAccess({
projectId: props.projectId,
scope: "batchExport:create",
scope: "batchExports:create",
});
const handleExport = async (format: BatchExportFileFormat) => {
+6 -9
View File
@@ -3,6 +3,7 @@ import {
Select,
SelectContent,
SelectItem,
SelectSeparator,
SelectTrigger,
SelectValue,
} from "@/src/components/ui/select";
@@ -33,15 +34,12 @@ export type ModelParamsContext = {
modelParamsDescription?: string;
};
export const ModelParameters: React.FC<
ModelParamsContext & { evalModelsOnly: boolean }
> = ({
export const ModelParameters: React.FC<ModelParamsContext> = ({
modelParams,
availableProviders,
availableModels,
updateModelParamValue,
setModelParamEnabled,
evalModelsOnly,
formDisabled = false,
modelParamsDescription,
}) => {
@@ -54,11 +52,8 @@ export const ModelParameters: React.FC<
<p className="font-semibold">Model</p>
{availableProviders.length === 0 ? (
<>
<p className="text-xs">
No LLM API key set in project.{" "}
{evalModelsOnly && "For evals, only OpenAI models are supported."}
</p>
<CreateLLMApiKeyDialog evalModelsOnly={evalModelsOnly} />
<p className="text-xs">No LLM API key set in project. </p>
<CreateLLMApiKeyDialog />
</>
) : (
<div className="space-y-4">
@@ -186,6 +181,8 @@ const ModelParamsSelect = ({
{option}
</SelectItem>
))}
<SelectSeparator />
<CreateLLMApiKeyDialog />
</SelectContent>
</Select>
{modelParamsDescription ? (
-109
View File
@@ -1,109 +0,0 @@
import { Button } from "@/src/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/src/components/ui/card";
import { chatAvailable, openChat } from "@/src/features/support-chat/chat";
import { Book, Github, Mail, MessageSquare, Slack } from "lucide-react";
import Link from "next/link";
import { SiDiscord } from "react-icons/si";
const supportChannels = [
{
icon: Book,
title: "Documentation",
description: "Find answers in the documentation.",
href: "https://docs.langfuse.com",
buttonText: "Visit Docs",
primary: true,
},
{
icon: Github,
title: "GitHub Issues",
description:
"Create an issue on Github to report bugs or request new features.",
href: "https://github.com/langfuse/langfuse/issues/new/choose",
buttonText: "Create Issue",
primary: true,
},
{
icon: Github,
title: "GitHub Support",
description: "Create a support ticket via GitHub discussions.",
href: "https://github.com/orgs/langfuse/discussions/categories/support",
buttonText: "Submit Question",
primary: true,
},
{
icon: Mail,
title: "Email",
description: "Send email to our shared inbox: help@langfuse.com",
href: "mailto:help@langfuse.com",
buttonText: "Send Email",
available: !chatAvailable,
},
{
icon: MessageSquare,
title: "Chat",
description: "Get support directly from the team.",
onClick: () => openChat(),
available: chatAvailable,
buttonText: "Launch Chat",
},
{
icon: SiDiscord,
title: "Discord",
description:
"Get support from community. Follow announcements to stay up to date with new features.",
href: "https://langfuse.com/discord",
buttonText: "Join Discord",
},
{
icon: Slack,
title: "Slack Connect",
description: "Get a dedicated support channel for you and your team.",
href: "mailto:help@langfuse.com?subject=Slack%20Connect%20Request&body=I'd%20like%20to%20request%20a%20dedicated%20Slack%20Connect%20channel%20for%20me%20and%20my%20team.%0D%0A%0D%0AUsers%20(emails)%20to%20include%20besides%20mine%3A%0D%0A%0D%0A",
buttonText: "Request via Email",
},
];
export const SupportChannels = () => (
<div className="mt-10 grid grid-cols-1 gap-5 md:grid-cols-2 lg:grid-cols-3">
{supportChannels
.filter((channel) => channel.available === undefined || channel.available)
.map((channel) => (
<Card key={channel.title} className="flex flex-col">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<channel.icon size={20} />
{channel.title}
</CardTitle>
</CardHeader>
<CardContent className="flex-1">
<CardDescription>{channel.description}</CardDescription>
</CardContent>
<CardFooter>
{channel.href ? (
<Button
asChild
variant={channel.primary ? "default" : "secondary"}
>
<Link href={channel.href}>{channel.buttonText}</Link>
</Button>
) : (
<Button
onClick={channel.onClick}
variant={channel.primary ? "default" : "secondary"}
>
{channel.buttonText}
</Button>
)}
</CardFooter>
</Card>
))}
</div>
);
+1 -1
View File
@@ -15,7 +15,7 @@ import { useUiCustomization } from "@/src/ee/features/ui-customization/useUiCust
import { hasOrganizationAccess } from "@/src/features/rbac/utils/checkOrganizationAccess";
import { ClickhouseAdminToggle } from "@/src/components/layouts/ClickhouseAdminToggle";
import { SidebarInset, SidebarProvider } from "@/src/components/ui/sidebar";
import { AppSidebar } from "@/src/components/app-sidebar";
import { AppSidebar } from "@/src/components/nav/app-sidebar";
const signOutUser = async () => {
localStorage.clear();
+10 -12
View File
@@ -8,7 +8,6 @@ import {
type LucideIcon,
Settings,
UsersIcon,
LibraryBig,
TerminalIcon,
Lightbulb,
Grid2X2,
@@ -20,9 +19,11 @@ import { type Entitlement } from "@/src/features/entitlements/constants/entitlem
import { type UiCustomizationOption } from "@/src/ee/features/ui-customization/useUiCustomization";
import { type User } from "next-auth";
import { type OrganizationScope } from "@/src/features/rbac/constants/organizationAccessRights";
import { SupportMenuDropdown } from "@/src/components/nav/support-menu-dropdown";
export type Route = {
title: string;
menuNode?: ReactNode;
featureFlag?: Flag;
label?: string | ReactNode;
projectRbacScopes?: ProjectScope[]; // array treated as OR
@@ -40,6 +41,12 @@ export type Route = {
};
export const ROUTES: Route[] = [
{
title: "Organizations",
pathname: "/",
icon: Grid2X2,
show: ({ organization }) => organization === undefined,
},
{
title: "Projects",
pathname: "/organization/[organizationId]",
@@ -81,7 +88,6 @@ export const ROUTES: Route[] = [
title: "Evaluation",
icon: Lightbulb,
pathname: `/project/[projectId]/annotation-queues`,
label: "Beta",
entitlements: ["annotation-queues", "model-based-evaluations"],
projectRbacScopes: ["annotationQueues:read", "evalJob:read"],
items: [
@@ -151,19 +157,11 @@ export const ROUTES: Route[] = [
icon: Settings,
bottom: true,
},
{
title: "Docs",
pathname: "https://langfuse.com/docs",
icon: LibraryBig,
bottom: true,
newTab: true,
customizableHref: "documentationHref",
},
{
title: "Support",
pathname: "/support",
icon: LifeBuoy,
bottom: true,
customizableHref: "supportHref",
pathname: "", // Empty pathname since this is a dropdown
menuNode: <SupportMenuDropdown />,
},
];
+5 -6
View File
@@ -27,19 +27,18 @@ export const StatusBadge = ({
let dotPingColor = "bg-muted-foreground";
let showDot = isLive;
if (statusCategories.active.includes(type)) {
if (statusCategories.active.includes(type.toLowerCase())) {
badgeColor = "bg-light-green text-dark-green";
dotColor = "animate-ping bg-dark-green";
dotPingColor = "bg-dark-green";
} else if (statusCategories.pending.includes(type)) {
} else if (statusCategories.pending.includes(type.toLowerCase())) {
badgeColor = "bg-light-yellow text-dark-yellow";
dotColor = "animate-ping bg-dark-yellow";
dotPingColor = "bg-dark-yellow";
} else if (statusCategories.error.includes(type)) {
} else if (statusCategories.error.includes(type.toLowerCase())) {
badgeColor = "bg-light-red text-dark-red";
dotColor = "animate-ping bg-dark-red";
dotPingColor = "bg-dark-red";
} else if (statusCategories.completed.includes(type)) {
showDot = false;
} else if (statusCategories.completed.includes(type.toLowerCase())) {
badgeColor = "bg-light-green text-dark-green";
showDot = false;
}
@@ -1,8 +1,11 @@
"use client";
import * as React from "react";
import { NavMain, type NavMainItem } from "@/src/components/nav-main";
import { NavUser, type UserNavigationProps } from "@/src/components/nav-user";
import { NavMain, type NavMainItem } from "@/src/components/nav/nav-main";
import {
NavUser,
type UserNavigationProps,
} from "@/src/components/nav/nav-user";
import {
Sidebar,
SidebarContent,
@@ -15,7 +18,7 @@ import { useRouter } from "next/router";
import Link from "next/link";
import { Alert, AlertDescription } from "@/src/components/ui/alert";
import { LangfuseLogo } from "@/src/components/LangfuseLogo";
import { SidebarNotifications } from "@/src/components/sidebar-notifications";
import { SidebarNotifications } from "@/src/components/nav/sidebar-notifications";
import { UsageTracker } from "@/src/ee/features/billing/components/UsageTracker";
type AppSidebarProps = {
@@ -45,7 +48,7 @@ export function AppSidebar({
<UsageTracker />
<SidebarNotifications />
</div>
<NavMain items={secondaryNavItems} showFeedbackButton />
<NavMain items={secondaryNavItems} />
</SidebarContent>
<SidebarFooter>
<NavUser {...userNavProps} />
@@ -1,6 +1,6 @@
"use client";
import { ChevronRight, MessageSquarePlus, type LucideIcon } from "lucide-react";
import { ChevronRight, type LucideIcon } from "lucide-react";
import {
Collapsible,
@@ -20,18 +20,20 @@ import {
import Link from "next/link";
import { type ReactNode } from "react";
import { cn } from "@/src/utils/tailwind";
import { FeedbackButtonWrapper } from "@/src/features/feedback/component/FeedbackButton";
export type NavMainItem = {
title: string;
menuNode?: ReactNode;
url: string;
icon?: LucideIcon;
isActive?: boolean;
label?: string | ReactNode;
newTab?: boolean;
items?: {
title: string;
url: string;
isActive?: boolean;
newTab?: boolean;
}[];
};
@@ -57,31 +59,11 @@ function NavItemContent({ item }: { item: NavMainItem }) {
);
}
export function NavMain({
items,
showFeedbackButton,
}: {
items: NavMainItem[];
showFeedbackButton?: boolean;
}) {
export function NavMain({ items }: { items: NavMainItem[] }) {
const { open, setOpen } = useSidebar();
return (
<SidebarGroup>
<SidebarMenu>
{showFeedbackButton && (
<FeedbackButtonWrapper
title="Provide feedback"
description="What do you think about this project? What can be improved?"
type="feedback"
>
<SidebarMenuItem>
<SidebarMenuButton tooltip="Feedback">
<MessageSquarePlus className="h-5 w-5" aria-hidden="true" />
Feedback
</SidebarMenuButton>
</SidebarMenuItem>
</FeedbackButtonWrapper>
)}
{items.map((item) =>
item.items && item.items.length > 0 ? (
<Collapsible
@@ -115,7 +97,10 @@ export function NavMain({
asChild
isActive={subItem.isActive}
>
<Link href={subItem.url}>
<Link
href={subItem.url}
target={subItem.newTab ? "_blank" : undefined}
>
<span>{subItem.title}</span>
</Link>
</SidebarMenuSubButton>
@@ -127,15 +112,20 @@ export function NavMain({
</Collapsible>
) : (
<SidebarMenuItem key={item.title}>
<SidebarMenuButton
asChild
tooltip={item.title}
isActive={item.isActive}
>
<Link href={item.url}>
<NavItemContent item={item} />
</Link>
</SidebarMenuButton>
{item.menuNode || (
<SidebarMenuButton
asChild
tooltip={item.title}
isActive={item.isActive}
>
<Link
href={item.url}
target={item.newTab ? "blank" : undefined}
>
<NavItemContent item={item} />
</Link>
</SidebarMenuButton>
)}
</SidebarMenuItem>
),
)}
@@ -8,7 +8,7 @@ import {
} from "@/src/components/ui/card";
import { useEffect, useState } from "react";
import { X } from "lucide-react";
import useLocalStorage from "./useLocalStorage";
import useLocalStorage from "../useLocalStorage";
import Link from "next/link";
import { usePostHogClientCapture } from "@/src/features/posthog-analytics/usePostHogClientCapture";
@@ -23,42 +23,6 @@ type SidebarNotification = {
};
const notifications: SidebarNotification[] = [
{
id: "lw2-5",
title: "Launch Week 2 Day 5",
description: "Introducing Prompt Experiments to test prompts on datasets",
link: "https://langfuse.com/changelog/2024-11-22-prompt-experimentation",
linkTitle: "Changelog",
},
{
id: "lw2-4",
title: "Launch Week 2 Day 4",
description: "All new docs for datasets, experiments, and evaluations",
link: "https://langfuse.com/changelog/2024-11-21-all-new-datasets-and-evals-documentation",
linkTitle: "Changelog",
},
{
id: "lw2-3",
title: "Launch Week 2 Day 3",
description:
"Full multi-modal support including images, audio, and attachments",
link: "https://langfuse.com/changelog/2024-11-20-full-multi-modal-images-audio-attachments",
linkTitle: "Changelog",
},
{
id: "lw2-2",
title: "Launch Week 2 Day 2",
description: "LLM-as-a-Judge Evaluators for Dataset Experiments",
link: "https://langfuse.com/changelog/2024-11-19-llm-as-a-judge-for-datasets",
linkTitle: "Changelog",
},
{
id: "lw2-1",
title: "Launch Week 2 Day 1",
description: "New side-by-side comparison view for dataset experiment runs",
link: "https://langfuse.com/changelog/2024-11-18-dataset-runs-comparison-view",
linkTitle: "Changelog",
},
{
id: "github-star",
title: "Star Langfuse",
@@ -0,0 +1,182 @@
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/src/components/ui/dropdown-menu";
import {
ArrowUpRight,
Bug,
Github,
LibraryBig,
LifeBuoy,
Lightbulb,
MessageCircle,
MessageSquarePlus,
Sparkles,
type LucideIcon,
} from "lucide-react";
import { useMemo, type ReactNode } from "react";
import {
type UiCustomizationOption,
useUiCustomization,
} from "@/src/ee/features/ui-customization/useUiCustomization";
import { SidebarMenuButton, useSidebar } from "@/src/components/ui/sidebar";
import {
chatAvailable,
chatIsVisible,
showChat,
hideChat,
} from "@/src/features/support-chat/chat";
import { Switch } from "@/src/components/ui/switch";
import { SiDiscord } from "react-icons/si";
type SupportMenuItem = {
title: string;
pathname: string;
icon: LucideIcon | React.ElementType;
menuNode?: ReactNode;
customizableHref?: UiCustomizationOption;
};
export const SupportMenuDropdown = () => {
const uiCustomization = useUiCustomization();
const supportMenuItems: (SupportMenuItem | "separator")[] = useMemo(() => {
const items: (SupportMenuItem | "separator")[] = [
{
title: "Ask AI",
pathname: "https://langfuse.com/docs/ask-ai",
icon: Sparkles,
},
];
const chatVisible = chatIsVisible();
if (uiCustomization?.supportHref) {
items.push({
title: "Support",
pathname: uiCustomization.supportHref,
icon: LifeBuoy,
});
} else {
if (chatAvailable) {
items.push({
title: "Chat",
pathname: "#",
menuNode: (
<div className="flex items-center gap-2">
<MessageCircle className="h-4 w-4" />
<span>Chat</span>
<Switch
defaultChecked={chatVisible}
onClick={(e) => e.stopPropagation()}
onCheckedChange={(checked) => {
if (checked) {
showChat();
} else {
hideChat();
}
}}
className="ml-auto"
/>
</div>
),
icon: MessageCircle,
});
}
items.push("separator");
items.push({
title: "GitHub Support",
pathname: "https://langfuse.com/gh-support",
icon: Github,
});
items.push({
title: "Discord",
pathname: "https://langfuse.com/discord",
icon: SiDiscord,
});
}
items.push("separator");
items.push({
title: "Docs",
pathname: "https://langfuse.com/docs",
icon: LibraryBig,
customizableHref: "documentationHref",
});
items.push("separator");
if (uiCustomization?.feedbackHref) {
items.push({
title: "Feedback",
pathname: uiCustomization.feedbackHref,
icon: MessageSquarePlus,
});
} else {
items.push(
...[
{
title: "Feature Request",
pathname: "https://langfuse.com/ideas",
icon: Lightbulb,
},
{
title: "Report a Bug",
pathname: "https://langfuse.com/issues",
icon: Bug,
},
],
);
}
return items;
}, [uiCustomization]);
const { isMobile } = useSidebar();
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<SidebarMenuButton>
<LifeBuoy className="h-4 w-4" />
Support
</SidebarMenuButton>
</DropdownMenuTrigger>
<DropdownMenuContent
side={isMobile ? "bottom" : "right"}
align="end"
sideOffset={4}
>
{supportMenuItems.map((item, index) => {
if (item === "separator") {
return <DropdownMenuSeparator key={`separator-${index}`} />;
}
const url = item.customizableHref
? (uiCustomization?.[item.customizableHref] ?? item.pathname)
: item.pathname;
return (
<DropdownMenuItem key={item.title}>
{item.menuNode ?? (
<a
href={url}
target={url.startsWith("http") ? "_blank" : undefined}
rel={
url.startsWith("http") ? "noopener noreferrer" : undefined
}
className="flex cursor-pointer items-center"
>
<item.icon className="mr-2 h-4 w-4" />
{item.title}
{url.startsWith("http") && (
<ArrowUpRight className="ml-1 h-3 w-3" />
)}
</a>
)}
</DropdownMenuItem>
);
})}
</DropdownMenuContent>
</DropdownMenu>
);
};
@@ -160,8 +160,12 @@ function ColumnVisibilityDropdownItem<TData, TValue>({
function GroupVisibilityDropdownHeader<TData, TValue>({
column,
groupTotalCount,
groupVisibleCount,
}: {
column: LangfuseColumnDef<TData, TValue>;
groupTotalCount: number;
groupVisibleCount: number;
}) {
const { attributes, isDragging, listeners, setNodeRef, transform } =
useSortable({
@@ -189,6 +193,9 @@ function GroupVisibilityDropdownHeader<TData, TValue>({
? column.header
: column.accessorKey}
</span>
<span className="ml-1.5 text-xs text-muted-foreground">
({groupVisibleCount}/{groupTotalCount})
</span>
</div>
<div className="flex items-center">
<Button
@@ -352,7 +359,11 @@ export function DataTableColumnVisibilityFilter<TData, TValue>({
return (
<DropdownMenuSub key={index}>
{isColumnOrderingEnabled ? (
<GroupVisibilityDropdownHeader column={column} />
<GroupVisibilityDropdownHeader
column={column}
groupTotalCount={groupTotalCount}
groupVisibleCount={groupVisibleCount}
/>
) : (
<DropdownMenuSubTrigger hasCustomIcon>
<Component className="mr-2 h-4 w-4 opacity-50" />
@@ -71,7 +71,8 @@ export function TraceTableMultiSelectAction({
},
});
const hasEntitlement = useHasEntitlement("annotation-queues");
const hasAnnotationEntitlement = useHasEntitlement("annotation-queues");
const hasTraceDeletionEntitlement = useHasEntitlement("trace-deletion");
const hasAddToQueueAccess = useHasProjectAccess({
projectId,
scope: "annotationQueues:CUD",
@@ -97,7 +98,7 @@ export function TraceTableMultiSelectAction({
{
projectId,
},
{ enabled: session.status === "authenticated" && hasEntitlement },
{ enabled: session.status === "authenticated" && hasAnnotationEntitlement },
);
return (
@@ -110,20 +111,22 @@ export function TraceTableMultiSelectAction({
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem
disabled={!hasDeleteAccess}
onClick={() => {
capture("trace:delete_form_open", {
count: selectedTraceIds.length,
source: "table-multi-select",
});
setDeleteDialogOpen(true);
}}
>
<Trash className="mr-2 h-4 w-4" />
<span>Delete</span>
</DropdownMenuItem>
{hasEntitlement && (
{hasTraceDeletionEntitlement && (
<DropdownMenuItem
disabled={!hasDeleteAccess}
onClick={() => {
capture("trace:delete_form_open", {
count: selectedTraceIds.length,
source: "table-multi-select",
});
setDeleteDialogOpen(true);
}}
>
<Trash className="mr-2 h-4 w-4" />
<span>Delete</span>
</DropdownMenuItem>
)}
{hasAnnotationEntitlement && (
<DropdownMenuItem
disabled={!hasAddToQueueAccess}
onClick={() => {
+16 -4
View File
@@ -56,6 +56,7 @@ interface DataTableProps<TData, TValue> {
paginationClassName?: string;
isBorderless?: boolean;
shouldRenderGroupHeaders?: boolean;
onRowClick?: (row: TData) => void;
}
export interface AsyncTableData<T> {
@@ -108,6 +109,7 @@ export function DataTable<TData extends object, TValue>({
paginationClassName,
isBorderless = false,
shouldRenderGroupHeaders = false,
onRowClick,
}: DataTableProps<TData, TValue>) {
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
const rowheighttw = getRowHeightTailwindClass(rowHeight);
@@ -319,6 +321,7 @@ export function DataTable<TData extends object, TValue>({
columns={columns}
data={data}
help={help}
onRowClick={onRowClick}
/>
) : (
<TableBodyComponent
@@ -327,6 +330,7 @@ export function DataTable<TData extends object, TValue>({
columns={columns}
data={data}
help={help}
onRowClick={onRowClick}
/>
)}
</Table>
@@ -368,6 +372,7 @@ interface TableBodyComponentProps<TData> {
columns: LangfuseColumnDef<TData, any>[];
data: AsyncTableData<TData[]>;
help?: { description: string; href: string };
onRowClick?: (row: TData) => void;
}
function TableBodyComponent<TData>({
@@ -376,6 +381,7 @@ function TableBodyComponent<TData>({
columns,
data,
help,
onRowClick,
}: TableBodyComponentProps<TData>) {
return (
<TableBody>
@@ -390,7 +396,13 @@ function TableBodyComponent<TData>({
</TableRow>
) : table.getRowModel().rows.length ? (
table.getRowModel().rows.map((row) => (
<TableRow key={row.id}>
<TableRow
key={row.id}
onClick={() => onRowClick?.(row.original)}
className={
onRowClick ? "cursor-pointer hover:bg-accent" : undefined
}
>
{row.getVisibleCells().map((cell) => (
<TableCell
key={cell.id}
@@ -410,9 +422,9 @@ function TableBodyComponent<TData>({
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={columns.length} className="h-24 text-center">
<div>
<TableRow className="hover:bg-transparent">
<TableCell colSpan={columns.length} className="relative h-24">
<div className="pointer-events-none fixed left-[50%] flex -translate-y-1/2 items-center justify-center">
No results.{" "}
{help && (
<DocPopup description={help.description} href={help.href} />
@@ -42,7 +42,8 @@ import useColumnOrder from "@/src/features/column-visibility/hooks/useColumnOrde
import { BatchExportTableButton } from "@/src/components/BatchExportTableButton";
import { useClickhouse } from "@/src/components/layouts/ClickhouseAdminToggle";
import { BreakdownTooltip } from "@/src/components/trace/BreakdownToolTip";
import { InfoIcon } from "lucide-react";
import { InfoIcon, PlusCircle } from "lucide-react";
import { UpsertModelFormDrawer } from "@/src/features/models/components/UpsertModelFormDrawer";
export type GenerationsTableRow = {
id: string;
@@ -83,6 +84,7 @@ export type GenerationsTableProps = {
projectId: string;
promptName?: string;
promptVersion?: number;
modelId?: string;
omittedFilter?: string[];
};
@@ -90,6 +92,7 @@ export default function GenerationsTable({
projectId,
promptName,
promptVersion,
modelId,
omittedFilter = [],
}: GenerationsTableProps) {
const [searchQuery, setSearchQuery] = useQueryParam(
@@ -143,6 +146,17 @@ export default function GenerationsTable({
]
: [];
const modelIdFilter: FilterState = modelId
? [
{
column: "Model ID",
type: "string",
operator: "=",
value: modelId,
},
]
: [];
const dateRangeFilter: FilterState = dateRange
? [
{
@@ -158,6 +172,7 @@ export default function GenerationsTable({
...dateRangeFilter,
...promptNameFilter,
...promptVersionFilter,
...modelIdFilter,
]);
const getCountPayload = {
@@ -447,7 +462,56 @@ export default function GenerationsTable({
size: 150,
enableHiding: true,
enableSorting: true,
cell: ({ row }) => {
const model = row.getValue("model") as string;
const modelId = row.getValue("modelId") as string | undefined;
if (!model) return null;
return modelId ? (
<TableLink
path={`/project/${projectId}/models/${modelId}`}
value={model}
/>
) : (
<UpsertModelFormDrawer
action="create"
projectId={projectId}
prefilledModelData={{
modelName: model,
prices:
Object.keys(row.original.usageDetails).length > 0
? Object.keys(row.original.usageDetails)
.filter((key) => key != "total")
.reduce(
(acc, key) => {
acc[key] = 0.000001;
return acc;
},
{} as Record<string, number>,
)
: undefined,
}}
className="cursor-pointer"
>
<span className="flex items-center gap-1">
<span>{model}</span>
<PlusCircle className="h-3 w-3" />
</span>
</UpsertModelFormDrawer>
);
},
},
{
accessorKey: "modelId",
id: "modelId",
header: "Model ID",
size: 100,
enableHiding: true,
defaultHidden: true,
},
{
accessorKey: "inputTokens",
id: "inputTokens",
@@ -691,6 +755,7 @@ export default function GenerationsTable({
name: generation.name ?? undefined,
version: generation.version ?? "",
model: generation.model ?? "",
modelId: generation.modelId ?? undefined,
level: generation.level,
statusMessage: generation.statusMessage ?? undefined,
usage: {
+137 -218
View File
@@ -1,39 +1,41 @@
import { useEffect } from "react";
import { DataTable } from "@/src/components/table/data-table";
import { type LangfuseColumnDef } from "@/src/components/table/types";
import { Button } from "@/src/components/ui/button";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/src/components/ui/popover";
import { useState } from "react";
import useColumnVisibility from "@/src/features/column-visibility/hooks/useColumnVisibility";
import { usePostHogClientCapture } from "@/src/features/posthog-analytics/usePostHogClientCapture";
import { useHasProjectAccess } from "@/src/features/rbac/utils/checkProjectAccess";
import { api } from "@/src/utils/api";
import { usdFormatter } from "@/src/utils/numbers";
import { type Prisma, type Model } from "@langfuse/shared/src/db";
import Decimal from "decimal.js";
import { Trash } from "lucide-react";
import { type Prisma } from "@langfuse/shared/src/db";
import { useQueryParams, withDefault, NumberParam } from "use-query-params";
import { cn } from "@/src/utils/tailwind";
import { IOTableCell } from "@/src/components/ui/CodeJsonViewer";
import { useRowHeightLocalStorage } from "@/src/components/table/data-table-row-height-switch";
import { DataTableToolbar } from "@/src/components/table/data-table-toolbar";
import useColumnOrder from "@/src/features/column-visibility/hooks/useColumnOrder";
import { type GetModelResult } from "@/src/features/models/validation";
import { DeleteModelButton } from "@/src/features/models/components/DeleteModelButton";
import { EditModelButton } from "@/src/features/models/components/EditModelButton";
import { CloneModelButton } from "@/src/features/models/components/CloneModelButton";
import { PriceBreakdownTooltip } from "@/src/features/models/components/PriceBreakdownTooltip";
import { UserCircle2Icon } from "lucide-react";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/src/components/ui/tooltip";
import { LangfuseIcon } from "@/src/components/LangfuseLogo";
import { useRouter } from "next/router";
import { PriceUnitSelector } from "@/src/features/models/components/PriceUnitSelector";
import { usePriceUnitMultiplier } from "@/src/features/models/hooks/usePriceUnitMultiplier";
export type ModelTableRow = {
modelId: string;
maintainer: string;
modelName: string;
matchPattern: string;
startDate?: Date;
inputPrice?: Decimal;
outputPrice?: Decimal;
totalPrice?: Decimal;
unit: string;
prices?: Record<string, number>;
tokenizerId?: string;
config?: Prisma.JsonValue;
lastUsed?: Date | null;
serverResponse: GetModelResult;
};
const modelConfigDescriptions = {
@@ -43,39 +45,47 @@ const modelConfigDescriptions = {
"Regex pattern to match `model` parameter of generations to model pricing",
startDate:
"Date to start pricing model. If not set, model is active unless a more recent version exists.",
inputPrice: "Price per 1000 units of input",
outputPrice: "Price per 1000 units of output",
totalPrice:
"Price per 1000 units, for models that don't have input/output specific prices",
unit: "Unit of measurement for generative model, can be TOKENS, CHARACTERS, SECONDS, MILLISECONDS, REQUESTS or IMAGES.",
prices: "Prices per usage type",
tokenizerId:
"Tokenizer used for this model to calculate token counts if none are ingested. Pick from list of supported tokenizers.",
config:
"Some tokenizers require additional configuration (e.g. openai tiktoken). See docs for details.",
maintainer:
"Maintainer of the model. Langfuse managed models can be cloned, user managed models can be edited and deleted. To supersede a Langfuse managed model, set the custom model name to the Langfuse model name.",
lastUsed: "Start time of the latest generation using this model",
} as const;
export default function ModelTable({ projectId }: { projectId: string }) {
const router = useRouter();
const [paginationState, setPaginationState] = useQueryParams({
pageIndex: withDefault(NumberParam, 0),
pageSize: withDefault(NumberParam, 50),
});
const models = api.models.all.useQuery({
page: paginationState.pageIndex,
limit: paginationState.pageSize,
projectId,
});
const models = api.models.getAll.useQuery(
{
page: paginationState.pageIndex,
limit: paginationState.pageSize,
projectId,
},
{
refetchOnWindowFocus: false,
refetchOnMount: true,
refetchOnReconnect: false,
staleTime: 1000 * 60 * 10,
},
);
const totalCount = models.data?.totalCount ?? null;
const { priceUnit } = usePriceUnitMultiplier();
const [rowHeight, setRowHeight] = useRowHeightLocalStorage("models", "m");
const [rowHeight, setRowHeight] = useRowHeightLocalStorage("models", "s");
// Set row height to medium if small as view is not optimized for small row heights
useEffect(() => {
if (rowHeight === "s") {
setRowHeight("m");
}
}, [rowHeight, setRowHeight]);
const columns: LangfuseColumnDef<ModelTableRow>[] = [
{
accessorKey: "maintainer",
id: "maintainer",
enableColumnFilter: true,
header: "Maintainer",
size: 100,
},
{
accessorKey: "modelName",
id: "modelName",
@@ -83,23 +93,40 @@ export default function ModelTable({ projectId }: { projectId: string }) {
headerTooltip: {
description: modelConfigDescriptions.modelName,
},
size: 150,
cell: ({ row }) => {
return (
<span className="font-mono text-xs font-semibold">
{row.original.modelName}
</span>
);
},
size: 120,
},
{
accessorKey: "startDate",
id: "startDate",
header: "Start Date",
accessorKey: "maintainer",
id: "maintainer",
header: "Maintainer",
headerTooltip: {
description: modelConfigDescriptions.startDate,
description: modelConfigDescriptions.maintainer,
},
size: 100,
size: 60,
cell: ({ row }) => {
const value: Date | undefined = row.getValue("startDate");
return value ? (
<span className="text-xs">{value.toISOString().slice(0, 10)} </span>
) : (
<span className="text-xs">-</span>
const isLangfuse = row.original.maintainer === "Langfuse";
return (
<div className="flex justify-center">
<Tooltip>
<TooltipTrigger>
{isLangfuse ? (
<LangfuseIcon size={16} />
) : (
<UserCircle2Icon className="h-4 w-4" />
)}
</TooltipTrigger>
<TooltipContent>
{isLangfuse ? "Langfuse maintained" : "User maintained"}
</TooltipContent>
</Tooltip>
</div>
);
},
},
@@ -115,104 +142,37 @@ export default function ModelTable({ projectId }: { projectId: string }) {
const value: string = row.getValue("matchPattern");
return value ? (
<IOTableCell data={value} singleLine={rowHeight === "s"} />
<span className="font-mono text-xs">{value}</span>
) : null;
},
},
{
accessorKey: "inputPrice",
id: "inputPrice",
accessorKey: "prices",
id: "prices",
header: () => {
return (
<>
Input Price{" "}
<span className="text-xs text-muted-foreground">/ 1k units</span>
</>
<div className="flex items-center gap-2">
<span>Prices {priceUnit}</span>
<PriceUnitSelector />
</div>
);
},
headerTooltip: {
description: modelConfigDescriptions.inputPrice,
},
size: 170,
size: 120,
cell: ({ row }) => {
const value: Decimal | undefined = row.getValue("inputPrice");
const prices: Record<string, number> | undefined =
row.getValue("prices");
return value ? (
<span className="text-xs">
{usdFormatter(value.toNumber() * 1000, 2, 8)}
</span>
) : (
<span className="text-xs">-</span>
);
},
enableHiding: true,
},
{
accessorKey: "outputPrice",
id: "outputPrice",
headerTooltip: {
description: modelConfigDescriptions.outputPrice,
},
header: () => {
return (
<>
Output Price{" "}
<span className="text-xs text-muted-foreground">/ 1k units</span>
</>
);
},
size: 170,
cell: ({ row }) => {
const value: Decimal | undefined = row.getValue("outputPrice");
return value ? (
<span className="text-xs">
{usdFormatter(value.toNumber() * 1000, 2, 8)}
</span>
) : (
<span className="text-xs">-</span>
<PriceBreakdownTooltip
modelName={row.original.modelName}
prices={prices}
priceUnit={priceUnit}
rowHeight={rowHeight}
/>
);
},
enableHiding: true,
},
{
accessorKey: "totalPrice",
id: "totalPrice",
header: () => {
return (
<>
Total Price{" "}
<span className="text-xs text-muted-foreground">/ 1k units</span>
</>
);
},
headerTooltip: {
description: modelConfigDescriptions.totalPrice,
},
size: 170,
cell: ({ row }) => {
const value: Decimal | undefined = row.getValue("totalPrice");
return value ? (
<span className="text-xs">
{usdFormatter(value.toNumber() * 1000, 2, 8)}
</span>
) : (
<span className="text-xs">-</span>
);
},
enableHiding: true,
},
{
accessorKey: "unit",
id: "unit",
header: "Unit",
headerTooltip: {
description: modelConfigDescriptions.unit,
},
enableHiding: true,
size: 110,
},
{
accessorKey: "tokenizerId",
id: "tokenizerId",
@@ -221,7 +181,7 @@ export default function ModelTable({ projectId }: { projectId: string }) {
description: modelConfigDescriptions.tokenizerId,
},
enableHiding: true,
size: 110,
size: 120,
},
{
accessorKey: "config",
@@ -231,7 +191,7 @@ export default function ModelTable({ projectId }: { projectId: string }) {
description: modelConfigDescriptions.config,
},
enableHiding: true,
size: 200,
size: 120,
cell: ({ row }) => {
const value: Prisma.JsonValue | undefined = row.getValue("config");
@@ -240,17 +200,46 @@ export default function ModelTable({ projectId }: { projectId: string }) {
) : null;
},
},
{
accessorKey: "lastUsed",
id: "lastUsed",
header: "Last used",
headerTooltip: {
description: modelConfigDescriptions.lastUsed,
},
enableHiding: true,
size: 120,
cell: ({ row }) => {
const value: Date | null | undefined = row.getValue("lastUsed");
return value?.toLocaleString() ?? "";
},
},
{
accessorKey: "actions",
header: "Actions",
size: 70,
size: 120,
cell: ({ row }) => {
return (
<DeleteModelButton
projectId={projectId}
modelId={row.original.modelId}
isBuiltIn={row.original.maintainer === "Langfuse"}
/>
return row.original.maintainer !== "Langfuse" ? (
<div
className="flex items-center gap-2"
onClick={(e) => e.stopPropagation()}
>
<EditModelButton
projectId={projectId}
modelData={row.original.serverResponse}
/>
<DeleteModelButton
projectId={projectId}
modelData={row.original.serverResponse}
/>
</div>
) : (
<div onClick={(e) => e.stopPropagation()}>
<CloneModelButton
projectId={projectId}
modelData={row.original.serverResponse}
/>
</div>
);
},
},
@@ -264,21 +253,17 @@ export default function ModelTable({ projectId }: { projectId: string }) {
columns,
);
const convertToTableRow = (model: Model): ModelTableRow => {
const convertToTableRow = (model: GetModelResult): ModelTableRow => {
return {
modelId: model.id,
maintainer: model.projectId ? "User" : "Langfuse",
modelName: model.modelName,
matchPattern: model.matchPattern,
startDate: model.startDate ? new Date(model.startDate) : undefined,
inputPrice: model.inputPrice ? new Decimal(model.inputPrice) : undefined,
outputPrice: model.outputPrice
? new Decimal(model.outputPrice)
: undefined,
totalPrice: model.totalPrice ? new Decimal(model.totalPrice) : undefined,
unit: model.unit ?? "",
prices: model.prices,
tokenizerId: model.tokenizerId ?? undefined,
config: model.tokenizerConfig,
lastUsed: model.lastUsed,
serverResponse: model,
};
};
@@ -320,76 +305,10 @@ export default function ModelTable({ projectId }: { projectId: string }) {
columnOrder={columnOrder}
onColumnOrderChange={setColumnOrder}
rowHeight={rowHeight}
onRowClick={(row) => {
router.push(`/project/${projectId}/models/${row.modelId}`);
}}
/>
</>
);
}
const DeleteModelButton = ({
modelId,
projectId,
isBuiltIn,
}: {
modelId: string;
projectId: string;
isBuiltIn?: boolean;
}) => {
const [isOpen, setIsOpen] = useState(false);
const utils = api.useUtils();
const capture = usePostHogClientCapture();
const mut = api.models.delete.useMutation({
onSuccess: () => {
void utils.models.invalidate();
},
});
const hasAccess = useHasProjectAccess({
projectId,
scope: "models:CUD",
});
return (
<Popover open={isOpen} onOpenChange={() => setIsOpen(!isOpen)}>
<PopoverTrigger asChild>
<Button
variant="ghost"
size="xs"
disabled={!hasAccess || isBuiltIn}
title={
isBuiltIn ? "Built-in models cannot be deleted" : "Delete model"
}
className={cn(
isBuiltIn &&
"disabled:pointer-events-auto disabled:cursor-not-allowed",
)}
>
<Trash className="h-4 w-4" />
</Button>
</PopoverTrigger>
<PopoverContent>
<h2 className="text-md mb-3 font-semibold">Please confirm</h2>
<p className="mb-3 text-sm">
This action permanently deletes this model definition.
</p>
<div className="flex justify-end space-x-4">
<Button
type="button"
variant="destructive"
loading={mut.isLoading}
onClick={() => {
capture("models:delete_button_click");
mut.mutateAsync({
projectId,
modelId,
});
setIsOpen(false);
}}
>
Delete Model
</Button>
</div>
</PopoverContent>
</Popover>
);
};
@@ -249,7 +249,7 @@ export function ScoreConfigsTable({ projectId }: { projectId: string }) {
rowHeight={rowHeight}
setRowHeight={setRowHeight}
/>
<Card className="mb-4 flex max-h-[calc(100dvh-30rem)] flex-col overflow-hidden">
<Card className="mb-4 flex max-h-[60dvh] flex-col overflow-hidden">
<DataTable
columns={columns}
data={
@@ -52,6 +52,7 @@ import { BatchExportTableButton } from "@/src/components/BatchExportTableButton"
import { useClickhouse } from "@/src/components/layouts/ClickhouseAdminToggle";
import { BreakdownTooltip } from "@/src/components/trace/BreakdownToolTip";
import { InfoIcon } from "lucide-react";
import { useHasEntitlement } from "@/src/features/entitlements/hooks";
export type TracesTableRow = {
bookmarked: boolean;
@@ -241,6 +242,8 @@ export default function TracesTable({
cellsLoading: !traceMetrics.data,
});
const hasTraceDeletionEntitlement = useHasEntitlement("trace-deletion");
const columns: LangfuseColumnDef<TracesTableRow>[] = [
{
id: "select",
@@ -708,7 +711,9 @@ export default function TracesTable({
isPinned: true,
cell: ({ row }) => {
const traceId: TracesTableRow["id"] = row.getValue("id");
return traceId && typeof traceId === "string" ? (
return traceId &&
typeof traceId === "string" &&
hasTraceDeletionEntitlement ? (
<DeleteButton
itemId={traceId}
projectId={projectId}
@@ -6,6 +6,7 @@ import {
} from "@/src/components/ui/tooltip";
import { useState } from "react";
import Decimal from "decimal.js";
import { getMaxDecimals } from "@/src/features/models/utils";
interface Details {
[key: string]: number | undefined;
@@ -36,15 +37,6 @@ export const BreakdownTooltip = ({
}, {})
: details;
// For costs, calculate the maximum number of decimal places needed
const getMaxDecimals = (value: number | undefined): number => {
if (!value) return 0;
const parts = value.toString().split(".");
// If no decimal point, return 0, else return length of decimal part
return parts.length === 1 ? 0 : parts[1].length;
};
const formatValueWithPadding = (value: number, maxDecimals: number) => {
return !value
? "0"
+3 -1
View File
@@ -257,6 +257,7 @@ export const OpenAiMessageView: React.FC<{
messages: z.infer<typeof ChatMlArraySchema>;
title?: string;
shouldRenderMarkdown?: boolean;
collapseLongHistory?: boolean;
media?: MediaReturnType[];
additionalInput?: Record<string, unknown>;
}> = ({
@@ -264,11 +265,12 @@ export const OpenAiMessageView: React.FC<{
messages,
shouldRenderMarkdown = false,
media,
collapseLongHistory = true,
additionalInput,
}) => {
const COLLAPSE_THRESHOLD = 3;
const [isCollapsed, setCollapsed] = useState(
messages.length > COLLAPSE_THRESHOLD ? true : null,
collapseLongHistory && messages.length > COLLAPSE_THRESHOLD ? true : null,
);
return (

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