Compare commits

...
73 Commits
Author SHA1 Message Date
Marc Klingen a7131dc422 chore: release v2.43.2
CI/CD / lint (push) Waiting to run
CI/CD / test-docker-build (push) Waiting to run
CI/CD / tests-web (20) (push) Waiting to run
CI/CD / tests-worker (20) (push) Waiting to run
CI/CD / e2e-tests (push) Waiting to run
CI/CD / all-ci-passed (push) Blocked by required conditions
CI/CD / push-docker-image (push) Blocked by required conditions
release.yml / release (push) Waiting to run
2024-05-17 11:13:57 +02:00
Marc KlingenandGitHub bb73243a04 fix(ui): use svh for fullscreen pages on mobile (#2076) 2024-05-17 11:12:00 +02:00
Marc Klingen e543c7dc27 feat(auth): capture unexpected credential sign-in errors 2024-05-17 10:42:11 +02:00
Marc KlingenandGitHub a2b6b7a14b feat(cloud): data region select and info modal (#2085) 2024-05-17 10:17:37 +02:00
Johannes GoslarandGitHub e0930fbf46 intercept OpenAI requests in worker testruns (#2020) 2024-05-16 21:12:30 +00:00
Marc KlingenandGitHub 125aa6b2c4 fix(api): align get/v2/prompts meta response with api spec (#2079) 2024-05-16 18:49:20 +00:00
Marc Klingen 3caf70bd2b chore: release v2.43.1
CI/CD / lint (push) Waiting to run
CI/CD / test-docker-build (push) Waiting to run
CI/CD / tests-web (20) (push) Waiting to run
CI/CD / tests-worker (20) (push) Waiting to run
CI/CD / e2e-tests (push) Waiting to run
CI/CD / all-ci-passed (push) Blocked by required conditions
CI/CD / push-docker-image (push) Blocked by required conditions
release.yml / release (push) Waiting to run
2024-05-16 17:19:49 +02:00
Marc Klingen 4139ccec98 fix: hide LlmApiKeyList in settings when not on langfuse cloud as related features are in beta right now and will be released in v3 2024-05-16 17:19:23 +02:00
Marc Klingen 07671c76b4 chore: release v2.43.0
CI/CD / lint (push) Waiting to run
CI/CD / test-docker-build (push) Waiting to run
CI/CD / tests-web (20) (push) Waiting to run
CI/CD / tests-worker (20) (push) Waiting to run
CI/CD / e2e-tests (push) Waiting to run
CI/CD / all-ci-passed (push) Blocked by required conditions
CI/CD / push-docker-image (push) Blocked by required conditions
release.yml / release (push) Waiting to run
2024-05-16 15:46:26 +02:00
Max DeichmannandGitHub 317608421f fix: remove encryption key (#2074) 2024-05-16 15:37:44 +02:00
10d84a4094 perf(scores): remove fk constraint from traceId and observationId on scores, change to projectId (#2012)
---------

Co-authored-by: Marlies Mayerhofer <74332854+marliessophie@users.noreply.github.com>
2024-05-16 12:33:02 +00:00
f623bd9f79 feat: allows disabling model params in playground and evals (#2062)
Co-authored-by: Max Deichmann <m.deichmann@tum.de>
2024-05-16 09:15:04 +01:00
Max Deichmann 08fb5f4000 chore: release v2.42.0
CI/CD / lint (push) Waiting to run
CI/CD / test-docker-build (push) Waiting to run
CI/CD / tests-web (20) (push) Waiting to run
CI/CD / tests-worker (20) (push) Waiting to run
CI/CD / e2e-tests (push) Waiting to run
CI/CD / all-ci-passed (push) Blocked by required conditions
CI/CD / push-docker-image (push) Blocked by required conditions
release.yml / release (push) Waiting to run
2024-05-16 09:53:08 +02:00
marliessophieandGitHub 03d00c405c feat: add sticky headings to table component (#2053)
* feat: add sticky header and footer to table component
2024-05-16 09:45:27 +02:00
Marc Klingen 783f7a205f fix: default to height m for single sessions 2024-05-16 02:50:32 +02:00
Marc KlingenandGitHub 83359e6fc6 perf(ui): add virtualized single session view (#2066) 2024-05-16 02:29:59 +02:00
Marc Klingen edd1484c5a chore: release v2.41.1
CI/CD / lint (push) Waiting to run
CI/CD / test-docker-build (push) Waiting to run
CI/CD / tests-web (20) (push) Waiting to run
CI/CD / tests-worker (20) (push) Waiting to run
CI/CD / e2e-tests (push) Waiting to run
CI/CD / all-ci-passed (push) Blocked by required conditions
CI/CD / push-docker-image (push) Blocked by required conditions
release.yml / release (push) Waiting to run
2024-05-15 03:09:47 +02:00
Marc KlingenandGitHub 410c91b464 chore: add additional tests to dataset item route (#2059) 2024-05-15 03:09:25 +02:00
Marc KlingenandGitHub 8962090404 fix(ui): null values in prompt metrics table (#2058) 2024-05-15 01:02:23 +00:00
Marc Klingen 4f88b78c71 chore: consolidate api types 2024-05-15 02:19:14 +02:00
Marc Klingen 8995b30959 chore: release v2.41.0
CI/CD / lint (push) Waiting to run
CI/CD / test-docker-build (push) Waiting to run
CI/CD / tests-web (20) (push) Waiting to run
CI/CD / tests-worker (20) (push) Waiting to run
CI/CD / e2e-tests (push) Waiting to run
CI/CD / all-ci-passed (push) Blocked by required conditions
CI/CD / push-docker-image (push) Blocked by required conditions
release.yml / release (push) Waiting to run
2024-05-15 02:09:42 +02:00
Marc KlingenandGitHub 321a25cf13 feat(datasets): make status of dataset item editable via API (#2057) 2024-05-15 00:02:50 +00:00
2990767c4a feat(prompts): add table to compare metrics across prompt versions, [promptName]/metrics (#1991)
---------

Co-authored-by: Marc Klingen <git@marcklingen.com>
2024-05-14 20:09:18 +00:00
Max Deichmann ad10e6f76f chore: release v2.40.0
CI/CD / lint (push) Waiting to run
CI/CD / test-docker-build (push) Waiting to run
CI/CD / tests-web (20) (push) Waiting to run
CI/CD / tests-worker (20) (push) Waiting to run
CI/CD / e2e-tests (push) Waiting to run
CI/CD / all-ci-passed (push) Blocked by required conditions
CI/CD / push-docker-image (push) Blocked by required conditions
release.yml / release (push) Waiting to run
2024-05-14 18:03:59 +02:00
Hassieb PakzadandGitHub 1f9adec948 feat: adds gpt-4o model to evals (#2052) 2024-05-14 18:03:13 +02:00
Hassieb PakzadandGitHub a158d6da09 feat(playground): adds gpt-4o models to playground (#2051) 2024-05-14 10:40:14 +02:00
Marc Klingen e6f9e5e06d chore: release v2.39.0
CI/CD / lint (push) Waiting to run
CI/CD / test-docker-build (push) Waiting to run
CI/CD / tests-web (20) (push) Waiting to run
CI/CD / tests-worker (20) (push) Waiting to run
CI/CD / e2e-tests (push) Waiting to run
CI/CD / all-ci-passed (push) Blocked by required conditions
CI/CD / push-docker-image (push) Blocked by required conditions
release.yml / release (push) Waiting to run
2024-05-14 01:00:41 +02:00
Marc KlingenandGitHub c2463cb91e feat: add tokenization for gpt4o (#2048) 2024-05-14 00:58:22 +02:00
Marc KlingenandGitHub 38e472ec66 feat: add pricing information for gpt4o (#2047)
add pricing information for gpt4o
Fixes #2046
2024-05-13 22:05:18 +00:00
616e79238b feat: add env LANGFUSE_DEFAULT_TABLE_DATETIME_OFFSET (#2040)
---------

Co-authored-by: Marc Klingen <git@marcklingen.com>
2024-05-13 16:24:25 +02:00
Marc KlingenandGitHub e2a59a2639 ci: fix tests (#2041) 2024-05-13 13:52:45 +00:00
Marc KlingenandGitHub 07609ade4f refactor: rename memberships to projectMemberships and enum MembershipRole to ProjectRole (#2037) 2024-05-12 19:06:50 +02:00
Marc Klingen 6627c59db4 chore: remove unused worder/generated/types, monorepo cleanup 2024-05-12 19:01:11 +02:00
Marc Klingen 70e1426790 fix: seeder for priojectId on scores 2024-05-10 15:17:03 +02:00
Marc KlingenandGitHub 8550d7313f chore: backfill projectid on score (#2011) 2024-05-09 20:29:43 +02:00
Marc KlingenandGitHub 1d76ef3a18 perf: add index to projectid on scores (#2028) 2024-05-09 18:03:33 +02:00
Marc Klingen b68d15309b docs: update contributing 2024-05-09 17:50:11 +02:00
Marc KlingenandGitHub c7b3e95645 feat(scores): add nullable projectId to scores (#2010) 2024-05-09 17:47:06 +02:00
Marc KlingenandGitHub 2c55cd842a fix(ui): scroll dataset item editor on large inputs (#2027) 2024-05-09 14:39:08 +00:00
Marc Klingen dc4e217a6e chore: release v2.38.2
CI/CD / lint (push) Waiting to run
CI/CD / test-docker-build (push) Waiting to run
CI/CD / tests-web (20) (push) Waiting to run
CI/CD / tests-worker (20) (push) Waiting to run
CI/CD / e2e-tests (push) Waiting to run
CI/CD / all-ci-passed (push) Blocked by required conditions
CI/CD / push-docker-image (push) Blocked by required conditions
release.yml / release (push) Waiting to run
2024-05-09 15:39:47 +02:00
Marc KlingenandGitHub 90f89f8b33 feat: add optional LANGFUSE_DISABLE_EXPENSIVE_POSTGRES_QUERIES (#2026) 2024-05-09 15:38:32 +02:00
Marc KlingenandGitHub 2cbcf1fc02 perf: move client side envs to session instead of fetching them via trpc (#2025) 2024-05-09 13:19:29 +00:00
Marc KlingenandGitHub 04251e2451 fix: navigation after deleting project (#2022) 2024-05-09 12:39:55 +00:00
Hassieb PakzadandGitHub b3ff1e0bd1 fix: make latest label a reserved label (#2019) 2024-05-09 11:19:45 +02:00
Max Deichmann 9fc1f79b94 chore: release v2.38.1
CI/CD / lint (push) Waiting to run
CI/CD / test-docker-build (push) Waiting to run
CI/CD / tests-web (20) (push) Waiting to run
CI/CD / tests-worker (20) (push) Waiting to run
CI/CD / e2e-tests (push) Waiting to run
CI/CD / all-ci-passed (push) Blocked by required conditions
CI/CD / push-docker-image (push) Blocked by required conditions
release.yml / release (push) Waiting to run
2024-05-08 17:42:34 +02:00
Marc Klingen 06bf8302d6 fix: notification version 2024-05-08 01:00:51 +02:00
7acc8e1f4d feat: add frontend posthog events (#1983)
---------

Co-authored-by: Marc Klingen <git@marcklingen.com>
2024-05-08 00:59:12 +02:00
Max DeichmannandGitHub b88809f7e5 perf: add sentry metrics integration (#2002) 2024-05-07 14:15:53 +00:00
Hassieb PakzadandGitHub 7ee5c2d573 fix(ui): render chatml output correctly if array passed (#2000) 2024-05-07 13:40:55 +00:00
Max DeichmannandGitHub 702a591a0d perf: add sentry counter for ingested events (#1999) 2024-05-07 13:34:45 +00:00
Marc KlingenandGitHub 6021ef94c8 fix(rbac): viewers have read-only access to evaluation resources (#1997) 2024-05-07 13:24:19 +00:00
Marc KlingenandGitHub d2d10893e1 fix: help strings and links across eval feature (#1996) 2024-05-07 13:09:05 +00:00
Max DeichmannandGitHub dd78b9be7e perf: instrument tokenisation (#1995) 2024-05-07 12:54:39 +00:00
Max DeichmannandGitHub ec1a9c53e7 fix: improve filteroptions api (#1993) 2024-05-07 12:13:35 +00:00
Max DeichmannandGitHub 74da65f9b2 perf: limit filteroptions api (#1973) 2024-05-07 14:05:19 +02:00
654620c336 fix(prompts): filter by label to prompts table (#1980)
* fix(prompts): add filter by label to prompts table

* feat: adds label filter to prompt table

---------

Co-authored-by: Hassieb Pakzad <68423100+hassiebp@users.noreply.github.com>
2024-05-07 10:42:27 +02:00
Marc Klingen d1b935b280 docs: update api reference to specify datetime format (ISO 8601) 2024-05-06 18:10:01 +02:00
Max DeichmannandGitHub 7dfa914934 fix: validate query (#1985) 2024-05-06 15:17:09 +00:00
ClemoandGitHub a126b05bb9 fix: update entity name from finto technologies gmbh to langfuse gmbh 2024-05-06 17:01:01 +02:00
ClemoandGitHub b81a81d76f docs: add /ee license (enterprise edition) 2024-05-06 17:00:05 +02:00
Marc Klingen 15014eb8ea chore: release v2.38.0
CI/CD / lint (push) Waiting to run
CI/CD / test-docker-build (push) Waiting to run
CI/CD / tests-web (20) (push) Waiting to run
CI/CD / tests-worker (20) (push) Waiting to run
CI/CD / e2e-tests (push) Waiting to run
CI/CD / all-ci-passed (push) Blocked by required conditions
CI/CD / push-docker-image (push) Blocked by required conditions
release.yml / release (push) Waiting to run
2024-05-05 21:32:51 +02:00
Marc KlingenandGitHub e1fb0e4102 perf: exclude observation metadata in traces.byId to prevent OOM when viewing large traces (#1978) 2024-05-05 19:47:35 +02:00
Marc Klingen c7d99476eb chore(cloud): update terms and conditions link 2024-05-05 17:51:42 +02:00
Marc KlingenandGitHub 7e3980856c feat(ui): add container queries for embedded tables + accordion to hide generations on prompt versions (#1974) 2024-05-03 21:17:51 +00:00
marliessophieandGitHub 54991e2246 feat: show generations table per prompt version, add observationCount to traces table (#1971)
* feat: add number of observations as new col on traces

* perf: load `observationCount` directly

* feat: show generations table per prompt version

* feat: remove `isHideIfEmptyEnabled` config

* fix: remove `enableSorting` for `observationCount`
2024-05-03 17:36:00 +00:00
Marc KlingenandGitHub 10bbfd75c3 chore(db): add cursors to trace table (#1968) 2024-05-03 15:48:43 +02:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
4476d6c1e7 chore(deps): bump the prisma group with 2 updates (#1959)
Bumps the prisma group with 2 updates: [prisma](https://github.com/prisma/prisma/tree/HEAD/packages/cli) and [@prisma/client](https://github.com/prisma/prisma/tree/HEAD/packages/client).


Updates `prisma` from 5.12.1 to 5.13.0
- [Release notes](https://github.com/prisma/prisma/releases)
- [Commits](https://github.com/prisma/prisma/commits/5.13.0/packages/cli)

Updates `@prisma/client` from 5.12.1 to 5.13.0
- [Release notes](https://github.com/prisma/prisma/releases)
- [Commits](https://github.com/prisma/prisma/commits/5.13.0/packages/client)

---
updated-dependencies:
- dependency-name: prisma
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prisma
- dependency-name: "@prisma/client"
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prisma
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-03 12:18:44 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
9dc758693b chore(deps): bump @aws-sdk/lib-storage from 3.550.0 to 3.568.0 (#1961)
Bumps [@aws-sdk/lib-storage](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/lib/lib-storage) from 3.550.0 to 3.568.0.
- [Release notes](https://github.com/aws/aws-sdk-js-v3/releases)
- [Changelog](https://github.com/aws/aws-sdk-js-v3/blob/main/lib/lib-storage/CHANGELOG.md)
- [Commits](https://github.com/aws/aws-sdk-js-v3/commits/v3.568.0/lib/lib-storage)

---
updated-dependencies:
- dependency-name: "@aws-sdk/lib-storage"
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-03 12:08:19 +00:00
Marc Klingen 082031ccd2 docs: fix links to published specs in api spec 2024-05-03 14:01:35 +02:00
Marc KlingenandGitHub a88764db7e perf: limit max length of trpc batch requests (#1966) 2024-05-03 13:55:26 +02:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
b6e0acb6f3 chore(deps): bump the sentry group with 4 updates (#1958)
Bumps the sentry group with 4 updates: [@sentry/nextjs](https://github.com/getsentry/sentry-javascript), [@sentry/node](https://github.com/getsentry/sentry-javascript), [@sentry/profiling-node](https://github.com/getsentry/sentry-javascript) and [@sentry/types](https://github.com/getsentry/sentry-javascript).


Updates `@sentry/nextjs` from 7.110.1 to 7.113.0
- [Release notes](https://github.com/getsentry/sentry-javascript/releases)
- [Changelog](https://github.com/getsentry/sentry-javascript/blob/7.113.0/CHANGELOG.md)
- [Commits](https://github.com/getsentry/sentry-javascript/compare/7.110.1...7.113.0)

Updates `@sentry/node` from 7.110.1 to 7.113.0
- [Release notes](https://github.com/getsentry/sentry-javascript/releases)
- [Changelog](https://github.com/getsentry/sentry-javascript/blob/7.113.0/CHANGELOG.md)
- [Commits](https://github.com/getsentry/sentry-javascript/compare/7.110.1...7.113.0)

Updates `@sentry/profiling-node` from 7.110.1 to 7.113.0
- [Release notes](https://github.com/getsentry/sentry-javascript/releases)
- [Changelog](https://github.com/getsentry/sentry-javascript/blob/7.113.0/CHANGELOG.md)
- [Commits](https://github.com/getsentry/sentry-javascript/compare/7.110.1...7.113.0)

Updates `@sentry/types` from 7.110.1 to 7.113.0
- [Release notes](https://github.com/getsentry/sentry-javascript/releases)
- [Changelog](https://github.com/getsentry/sentry-javascript/blob/7.113.0/CHANGELOG.md)
- [Commits](https://github.com/getsentry/sentry-javascript/compare/7.110.1...7.113.0)

---
updated-dependencies:
- dependency-name: "@sentry/nextjs"
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: sentry
- dependency-name: "@sentry/node"
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: sentry
- dependency-name: "@sentry/profiling-node"
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: sentry
- dependency-name: "@sentry/types"
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: sentry
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-03 09:34:35 +00:00
Max DeichmannandGitHub 366ccde902 chore: improve logging (#1965) 2024-05-03 09:17:36 +00:00
Marc KlingenandGitHub 21c60fab76 fix: generate api spec and postman collection to public folder (#1964)
* fix: generate api spec and postman collection to public folder

* remove copy script from package

* remove old generation target
2024-05-03 08:55:33 +00:00
205 changed files with 4537 additions and 1985 deletions
+14
View File
@@ -80,6 +80,20 @@ LANGFUSE_CSP_ENFORCE_HTTPS="true"
# ENABLE_EVENT_LOG=false
### START Envs to be deprecated in Langfuse v3.0
# Disable the expensive analytics queries and related features
# LANGFUSE_DISABLE_EXPENSIVE_POSTGRES_QUERIES="true"
# Customize the default datetime offset filter used in for the UI tables
# LANGFUSE_DEFAULT_TABLE_DATETIME_OFFSET="7" # int (days)
### END Envs to be deprecated in Langfuse v3.0
### START Langfuse Cloud Config
# Used for Langfuse Cloud deployments
# Not recommended for self-hosted deployments as these are NOT COVERED BY SEMVER
-3
View File
@@ -109,9 +109,6 @@ jobs:
strategy:
matrix:
node-version: [20]
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
ENCRYPTION_KEY: ${{ secrets.ENCRYPTION_KEY }}
steps:
- name: Set Swap Space
uses: pierotofy/set-swap-space@master
+10
View File
@@ -290,6 +290,16 @@ When a new release is tagged on the `main` branch (excluding prereleases), it tr
1. The Docker image is published to GitHub Packages with the version number and `latest` tag.
2. The deployment is carried out on Langfuse Cloud. This is done by force pushing the `main` branch to the `production` branch during every release, using the [`release.yml`](.github/workflows/release.yml) GitHub Action.
## Using secrets stored in 1Password
When applying changes to non-local environments, you may need to use secrets stored in 1Password. We use the 1Password CLI for this purpose.
Example:
```bash
op run --env-file="./.env" -- pnpm --filter=shared run db:deploy
```
## License
Langfuse is MIT licensed, except for `ee/` folder. See [LICENSE](LICENSE) and [docs](https://langfuse.com/docs/open-source) for more details.
+1 -1
View File
@@ -1,4 +1,4 @@
Copyright (c) 2023 Finto Technologies GmbH
Copyright (c) 2023--2024 Langfuse GmbH
Portions of this software are licensed as follows:
+37
View File
@@ -0,0 +1,37 @@
Langfuse Enterprise License (the “Enterprise License”)
Copyright (c) 2024 Langfuse GmbH ('Langfuse')
With regard to the Langfuse Software:
This software and associated documentation files (the "Software") may only be
used in production, if you (and any entity that you represent) have agreed to,
and are in compliance with, the Langfuse Terms of Service, available
at https://langfuse.com/terms (the “Enterprise Terms”), or other
agreement governing the use of the Software, as agreed by you and Langfuse,
and otherwise have a valid Langfuse Enterprise license.
Subject to the foregoing sentence, you are free to
modify this Software and publish patches to the Software. You agree that Langfuse
and/or its licensors (as applicable) retain all right, title and interest in and
to all such modifications and/or patches, and all such modifications and/or
patches may only be used, copied, modified, displayed, distributed, or otherwise
exploited with a valid Langfuse Enterprise license.
Notwithstanding the foregoing, you may copy and modify
the Software for development and testing purposes, without requiring a
subscription. You agree that Langfuse and/or its licensors (as applicable) retain
all right, title and interest in and to all such modifications. You are not
granted any other rights beyond what is expressly stated herein. Subject to the
foregoing, it is forbidden to copy, merge, publish, distribute, sublicense,
and/or sell the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
For all third party components incorporated into the Langfuse Software, those
components are licensed under the original license provided by the owner of the
applicable component.
+1 -1
View File
@@ -15,7 +15,7 @@ groups:
version: 0.0.26
output:
location: local-file-system
path: ../../../web/generated/openapi-client
path: ../../../web/public/generated/api-client
config:
namespaceExport: Langfuse
allowCustomFetcher: true
+2 -2
View File
@@ -9,8 +9,8 @@ docs: |
## Exports
- OpenAPI spec: https://cloud.langfuse.com/openapi-server.yml
- Postman collection: https://cloud.langfuse.com/postman-collection.json
- OpenAPI spec: https://cloud.langfuse.com/generated/api/openapi.yml
- Postman collection: https://cloud.langfuse.com/generated/postman/collection.json
error-discrimination:
strategy: status-code
@@ -32,3 +32,6 @@ types:
id:
type: optional<string>
docs: Dataset items are upserted on their id
status:
type: optional<commons.DatasetStatus>
docs: Defaults to ACTIVE for newly created items
+2 -2
View File
@@ -31,10 +31,10 @@ service:
docs: Optional filter for metrics where traces include all of these tags
fromTimestamp:
type: optional<datetime>
docs: Optional filter to only include traces on or after a certain timestamp
docs: Optional filter to only include traces on or after a certain datetime (ISO 8601)
toTimestamp:
type: optional<datetime>
docs: Optional filter to only include traces before a certain timestamp
docs: Optional filter to only include traces before a certain datetime (ISO 8601)
response: DailyMetrics
types:
DailyMetrics:
+1 -1
View File
@@ -35,7 +35,7 @@ service:
parentObservationId: optional<string>
fromStartTime:
type: optional<datetime>
docs: Retrieve only observations with a start_time greater than this timestamp.
docs: Retrieve only observations with a start_time greater than this datetime (ISO 8601).
response: ObservationsViews
types:
+1 -1
View File
@@ -29,7 +29,7 @@ service:
name: optional<string>
fromTimestamp:
type: optional<datetime>
docs: Retrieve only scores newer than this timestamp.
docs: Retrieve only scores newer than this datetime (ISO 8601).
source:
type: commons.ScoreSource
docs: Retrieve only scores from a specific source.
+1 -1
View File
@@ -32,7 +32,7 @@ service:
name: optional<string>
fromTimestamp:
type: optional<datetime>
docs: Retrieve only traces newer than this timestamp.
docs: Retrieve only traces newer than this datetime (ISO 8601).
orderBy:
type: optional<string>
docs: "Format of the string [field].[asc/desc]. Fields: id, timestamp, name, userId, release, version, public, bookmarked, sessionId. Example: timestamp.asc"
+2 -2
View File
@@ -6,7 +6,7 @@ groups:
version: 0.0.28
output:
location: local-file-system
path: ../../../web/generated/openapi-server
path: ../../../web/public/generated/api
config:
namespaceExport: Langfuse
allowCustomFetcher: true
@@ -33,7 +33,7 @@ groups:
version: 0.0.45
output:
location: local-file-system
path: ../../../web/generated/postman
path: ../../../web/public/generated/postman
# published:
# generators:
# - name: fernapi/fern-python-sdk
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "langfuse",
"version": "2.37.3",
"version": "2.43.2",
"author": "engineering@langfuse.com",
"license": "MIT",
"private": true,
+2 -2
View File
@@ -47,7 +47,7 @@
"@langchain/anthropic": "^0.1.16",
"@langchain/core": "^0.1.61",
"@langchain/openai": "^0.0.28",
"@prisma/client": "^5.12.1",
"@prisma/client": "^5.13.0",
"@types/bcryptjs": "^2.4.6",
"bcryptjs": "^2.4.3",
"kysely": "^0.27.3",
@@ -71,7 +71,7 @@
"kysely-codegen": "^0.11.0",
"nodemon": "^3.0.3",
"prettier": "^3.2.5",
"prisma": "^5.12.1",
"prisma": "^5.13.0",
"prisma-erd-generator": "^1.11.2",
"prisma-kysely": "^1.8.0",
"ts-node": "^10.9.2",
+15 -12
View File
@@ -4,13 +4,13 @@ export type Generated<T> = T extends ColumnType<infer S, infer I, infer U>
: ColumnType<T, T | undefined, T>;
export type Timestamp = ColumnType<Date, Date | string, Date | string>;
export const MembershipRole = {
export const ProjectRole = {
OWNER: "OWNER",
ADMIN: "ADMIN",
MEMBER: "MEMBER",
VIEWER: "VIEWER"
} as const;
export type MembershipRole = (typeof MembershipRole)[keyof typeof MembershipRole];
export type ProjectRole = (typeof ProjectRole)[keyof typeof ProjectRole];
export const ObservationType = {
SPAN: "SPAN",
EVENT: "EVENT",
@@ -96,7 +96,7 @@ export type AuditLog = {
updated_at: Generated<Timestamp>;
user_id: string;
project_id: string;
user_project_role: MembershipRole;
user_project_role: ProjectRole;
resource_type: string;
resource_id: string;
action: string;
@@ -208,17 +208,10 @@ export type LlmApiKeys = {
secret_key: string;
project_id: string;
};
export type Membership = {
project_id: string;
user_id: string;
role: MembershipRole;
created_at: Generated<Timestamp>;
updated_at: Generated<Timestamp>;
};
export type MembershipInvitation = {
id: string;
email: string;
role: MembershipRole;
role: ProjectRole;
project_id: string;
sender_id: string | null;
created_at: Generated<Timestamp>;
@@ -324,6 +317,13 @@ export type Project = {
name: string;
cloud_config: unknown | null;
};
export type ProjectMembership = {
project_id: string;
user_id: string;
role: ProjectRole;
created_at: Generated<Timestamp>;
updated_at: Generated<Timestamp>;
};
export type Prompt = {
id: string;
created_at: Generated<Timestamp>;
@@ -342,6 +342,7 @@ export type Prompt = {
export type Score = {
id: string;
timestamp: Generated<Timestamp>;
project_id: string;
name: string;
value: number;
source: ScoreSource;
@@ -378,6 +379,8 @@ export type Trace = {
input: unknown | null;
output: unknown | null;
session_id: string | null;
created_at: Generated<Timestamp>;
updated_at: Generated<Timestamp>;
};
export type TraceSession = {
id: string;
@@ -437,12 +440,12 @@ export type DB = {
job_executions: JobExecution;
llm_api_keys: LlmApiKeys;
membership_invitations: MembershipInvitation;
memberships: Membership;
models: Model;
observations: Observation;
observations_view: ObservationView;
posthog_integrations: PosthogIntegration;
pricings: Pricing;
project_memberships: ProjectMembership;
projects: Project;
prompts: Prompt;
scores: Score;
@@ -0,0 +1,3 @@
-- AlterTable
ALTER TABLE "traces" ADD COLUMN "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
ADD COLUMN "updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP;
@@ -0,0 +1,3 @@
-- CreateIndex
CREATE INDEX CONCURRENTLY "traces_created_at_idx" ON "traces"("created_at");
@@ -0,0 +1,3 @@
-- CreateIndex
CREATE INDEX CONCURRENTLY "traces_updated_at_idx" ON "traces"("updated_at");
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "scores" ADD COLUMN "project_id" TEXT;
@@ -0,0 +1,2 @@
-- CreateIndex
CREATE INDEX CONCURRENTLY "scores_project_id_idx" ON "scores"("project_id");
@@ -0,0 +1,5 @@
-- Backfill project_id on existing scores based on linked trace_id
UPDATE scores
SET project_id = traces.project_id
FROM traces
WHERE scores.trace_id = traces.id AND scores.project_id IS NULL;
@@ -0,0 +1,15 @@
-- Rename table, will cause a downtime as it needs to be locked and synced with TRPC API
ALTER TABLE "memberships"
RENAME TO "project_memberships";
-- AlterTable
ALTER TABLE "project_memberships" RENAME CONSTRAINT "memberships_pkey" TO "project_memberships_pkey";
-- RenameForeignKey
ALTER TABLE "project_memberships" RENAME CONSTRAINT "memberships_project_id_fkey" TO "project_memberships_project_id_fkey";
-- RenameForeignKey
ALTER TABLE "project_memberships" RENAME CONSTRAINT "memberships_user_id_fkey" TO "project_memberships_user_id_fkey";
-- RenameIndex
ALTER INDEX "memberships_user_id_idx" RENAME TO "project_memberships_user_id_idx";
@@ -0,0 +1,37 @@
-- Goal: Rename the enum `MembershipRole` to `ProjectRole` and update all tables that use it
-- Create a new Enum
CREATE TYPE "ProjectRole" AS ENUM ('OWNER', 'ADMIN', 'MEMBER', 'VIEWER');
-- Add a temporary column to tables (for each table)
ALTER TABLE "audit_logs" ADD COLUMN "user_project_role_temp" text;
ALTER TABLE "membership_invitations" ADD COLUMN "role_temp" text;
ALTER TABLE "project_memberships" ADD COLUMN "role_temp" text;
-- Copy data over to temporary columns (for each table)
UPDATE "audit_logs" SET "user_project_role_temp" = "user_project_role"::text;
UPDATE "membership_invitations" SET "role_temp" = "role"::text;
UPDATE "project_memberships" SET "role_temp" = "role"::text;
-- Drop old columns
ALTER TABLE "audit_logs" DROP COLUMN "user_project_role";
ALTER TABLE "membership_invitations" DROP COLUMN "role";
ALTER TABLE "project_memberships" DROP COLUMN "role";
-- Rename temporary columns to old column names
ALTER TABLE "audit_logs" RENAME COLUMN "user_project_role_temp" TO "user_project_role";
ALTER TABLE "membership_invitations" RENAME COLUMN "role_temp" TO "role";
ALTER TABLE "project_memberships" RENAME COLUMN "role_temp" TO "role";
-- Convert the text columns to enum columns (for each table)
ALTER TABLE "audit_logs" ALTER COLUMN "user_project_role" TYPE "ProjectRole" USING "user_project_role"::"ProjectRole";
ALTER TABLE "membership_invitations" ALTER COLUMN "role" TYPE "ProjectRole" USING "role"::"ProjectRole";
ALTER TABLE "project_memberships" ALTER COLUMN "role" TYPE "ProjectRole" USING "role"::"ProjectRole";
-- Make the columns NOT NULL (for each table)
ALTER TABLE "audit_logs" ALTER COLUMN "user_project_role" SET NOT NULL;
ALTER TABLE "membership_invitations" ALTER COLUMN "role" SET NOT NULL;
ALTER TABLE "project_memberships" ALTER COLUMN "role" SET NOT NULL;
-- Now finally, you can drop your old enum
DROP TYPE "MembershipRole";
@@ -0,0 +1,20 @@
INSERT INTO models (
id,
project_id,
model_name,
match_pattern,
start_date,
input_price,
output_price,
total_price,
unit,
tokenizer_id,
tokenizer_config
)
VALUES
-- gpt-4o
('b9854a5c92dc496b997d99d20', NULL, 'gpt-4o', '(?i)^(gpt-4o)$', NULL, 0.000005, 0.000015, NULL, 'TOKENS', 'openai', '{ "tokensPerMessage": 3, "tokensPerName": 1, "tokenizerModel": "gpt-4o" }'),
-- gpt-4o-2024-05-13
('b9854a5c92dc496b997d99d21', NULL, 'gpt-4o-2024-05-13', '(?i)^(gpt-4o-2024-05-13)$', NULL, 0.000005, 0.000015, NULL, 'TOKENS', 'openai', '{ "tokensPerMessage": 3, "tokensPerName": 1, "tokenizerModel": "gpt-4o-2024-05-13" }')
@@ -0,0 +1,5 @@
-- DropForeignKey
ALTER TABLE "scores" DROP CONSTRAINT "scores_observation_id_fkey";
-- DropForeignKey
ALTER TABLE "scores" DROP CONSTRAINT "scores_trace_id_fkey";
@@ -0,0 +1,11 @@
/*
Warnings:
- Made the column `project_id` on table `scores` required. This step will fail if there are existing NULL values in that column.
*/
-- AlterTable
ALTER TABLE "scores" ALTER COLUMN "project_id" SET NOT NULL;
-- AddForeignKey
ALTER TABLE "scores" ADD CONSTRAINT "scores_project_id_fkey" FOREIGN KEY ("project_id") REFERENCES "projects"("id") ON DELETE CASCADE ON UPDATE CASCADE;
@@ -0,0 +1,8 @@
/*
Warnings:
- A unique constraint covering the columns `[id,project_id]` on the table `scores` will be added. If there are existing duplicate values, this will fail.
*/
-- DropIndex
DROP INDEX "scores_id_trace_id_key";
@@ -0,0 +1,8 @@
/*
Warnings:
- A unique constraint covering the columns `[id,project_id]` on the table `scores` will be added. If there are existing duplicate values, this will fail.
*/
-- CreateIndex
CREATE UNIQUE INDEX CONCURRENTLY "scores_id_project_id_key" ON "scores"("id", "project_id");
+55 -49
View File
@@ -63,21 +63,21 @@ model Session {
}
model User {
id String @id @default(cuid())
name String?
email String? @unique
emailVerified DateTime? @map("email_verified")
password String?
image String?
admin Boolean @default(false)
accounts Account[]
sessions Session[]
memberships Membership[]
invitations MembershipInvitation[]
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
featureFlags String[] @default([]) @map("feature_flags")
AuditLog AuditLog[]
id String @id @default(cuid())
name String?
email String? @unique
emailVerified DateTime? @map("email_verified")
password String?
image String?
admin Boolean @default(false)
accounts Account[]
sessions Session[]
projectMemberships ProjectMembership[]
invitations MembershipInvitation[]
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
featureFlags String[] @default([]) @map("feature_flags")
AuditLog AuditLog[]
@@map("users")
}
@@ -97,7 +97,7 @@ model Project {
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
name String
cloudConfig Json? @map("cloud_config") // Langfuse Cloud, for zod schema see projectsRouter.ts
members Membership[]
projectMembers ProjectMembership[]
traces Trace[]
observations Observation[]
apiKeys ApiKey[]
@@ -113,6 +113,7 @@ model Project {
JobExecution JobExecution[]
LlmApiKeys LlmApiKeys[]
PosthogIntegration PosthogIntegration[]
Score Score[]
@@map("projects")
}
@@ -154,37 +155,37 @@ model LlmApiKeys {
@@map("llm_api_keys")
}
model Membership {
projectId String @map("project_id")
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
userId String @map("user_id")
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
role MembershipRole
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
model ProjectMembership {
projectId String @map("project_id")
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
userId String @map("user_id")
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
role ProjectRole
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
@@id([projectId, userId])
@@index([userId])
@@map("memberships")
@@map("project_memberships")
}
model MembershipInvitation {
id String @id @unique @default(cuid())
id String @id @unique @default(cuid())
email String
role MembershipRole
projectId String @map("project_id")
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
senderId String? @map("sender_id")
sender User? @relation(fields: [senderId], references: [id], onDelete: SetNull)
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
role ProjectRole
projectId String @map("project_id")
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
senderId String? @map("sender_id")
sender User? @relation(fields: [senderId], references: [id], onDelete: SetNull)
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
@@index([projectId])
@@index([email])
@@map("membership_invitations")
}
enum MembershipRole {
enum ProjectRole {
OWNER
ADMIN
MEMBER
@@ -227,8 +228,9 @@ model Trace {
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")
scores Score[]
DatasetRunItems DatasetRunItems[]
DatasetItem DatasetItem[]
JobExecution JobExecution[]
@@ -241,6 +243,8 @@ model Trace {
@@index([externalId])
@@index(timestamp)
@@index(release)
@@index(updatedAt)
@@index(createdAt)
@@index([tags(ops: ArrayOps)], type: Gin)
@@map("traces")
}
@@ -272,6 +276,8 @@ view TraceView {
}
// Update ObservationView below when making changes to this model!
// traceId is optional only due to timing during data injestion
// (traceId is not necessarily known at the time of observation creation)
model Observation {
id String @id @default(cuid())
traceId String? @map("trace_id")
@@ -301,7 +307,6 @@ model Observation {
outputCost Decimal? @map("output_cost")
totalCost Decimal? @map("total_cost")
completionStartTime DateTime? @map("completion_start_time")
scores Score[]
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
derivedDatasetItems DatasetItem[]
datasetRunItems DatasetRunItems[]
@@ -388,19 +393,20 @@ enum ObservationLevel {
model Score {
id String @id @default(cuid())
timestamp DateTime @default(now())
projectId String @map("project_id")
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
name String
value Float
source ScoreSource
comment String?
traceId String @map("trace_id")
trace Trace @relation(fields: [traceId], references: [id], onDelete: Cascade)
observationId String? @map("observation_id")
observation Observation? @relation(fields: [observationId], references: [id], onDelete: SetNull)
JobExecution JobExecution[]
@@unique([id, traceId]) // used for upsert
@@unique([id, projectId]) // used for upserts via prisma
@@index(timestamp)
@@index([value])
@@index([projectId])
@@index([traceId], type: Hash)
@@index([observationId], type: Hash)
@@index([source])
@@ -594,16 +600,16 @@ model Model {
}
model AuditLog {
id String @id @default(cuid())
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
userId String @map("user_id")
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
projectId String @map("project_id")
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
userProjectRole MembershipRole @map("user_project_role")
resourceType String @map("resource_type")
resourceId String @map("resource_id")
id String @id @default(cuid())
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
userId String @map("user_id")
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
projectId String @map("project_id")
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
userProjectRole ProjectRole @map("user_project_role")
resourceType String @map("resource_type")
resourceId String @map("resource_id")
action String
before String? //stringified JSON
after String? // stringified JSON
+17 -10
View File
@@ -51,7 +51,7 @@ async function main() {
create: {
id: seedProjectId,
name: "llm-app",
members: {
projectMembers: {
create: {
role: "OWNER",
userId: user.id,
@@ -72,7 +72,7 @@ async function main() {
name: "summary-prompt",
project: { connect: { id: seedProjectId } },
prompt: "prompt {{variable}} {{anotherVariable}}",
labels: ["production"],
labels: ["production", "latest"],
version: 1,
createdBy: "user-1",
},
@@ -110,7 +110,7 @@ async function main() {
create: {
id: "239ad00f-562f-411d-af14-831c75ddd875",
name: "demo-app",
members: {
projectMembers: {
create: {
role: "OWNER",
userId: user.id,
@@ -459,7 +459,7 @@ function createObjects(
const session =
Math.random() > 0.3
? {
id: `session-${i % 10}`,
id: `session-${i % 3}`,
projectId: projectId,
}
: undefined;
@@ -501,6 +501,7 @@ function createObjects(
value: Math.floor(Math.random() * 3) - 1,
timestamp: traceTs,
source: ScoreSource.REVIEW,
projectId,
},
]
: []),
@@ -512,6 +513,7 @@ function createObjects(
value: Math.floor(Math.random() * 10) - 5,
timestamp: traceTs,
source: ScoreSource.API,
projectId,
},
]
: []),
@@ -590,7 +592,9 @@ function createObjects(
const model = models[Math.floor(Math.random() * models.length)];
const promptId =
promptIds.get(projectId)![
Math.floor(Math.random() * promptIds.get(projectId)!.length)
Math.floor(
Math.random() * Math.floor(promptIds.get(projectId)!.length / 2)
)
];
const generation = {
@@ -693,6 +697,7 @@ function createObjects(
observationId: generation.id,
traceId: trace.id,
source: ScoreSource.API,
projectId: trace.projectId,
});
if (Math.random() > 0.6)
scores.push({
@@ -701,6 +706,7 @@ function createObjects(
observationId: generation.id,
traceId: trace.id,
source: ScoreSource.API,
projectId: trace.projectId,
});
for (let l = 0; l < Math.floor(Math.random() * 2); l++) {
@@ -764,7 +770,7 @@ async function generatePrompts(project: Project) {
prompt: "Prompt 1 content",
name: "Prompt 1",
version: 1,
labels: ["production"],
labels: ["production", "latest"],
},
{
id: `prompt-${v4()}`,
@@ -773,7 +779,7 @@ async function generatePrompts(project: Project) {
prompt: "Prompt 2 content",
name: "Prompt 2",
version: 1,
labels: ["production"],
labels: ["production", "latest"],
},
{
id: `prompt-${v4()}`,
@@ -782,7 +788,7 @@ async function generatePrompts(project: Project) {
prompt: "Prompt 3 content",
name: "Prompt 3 by API",
version: 1,
labels: ["production"],
labels: ["production", "latest"],
},
{
id: `prompt-${v4()}`,
@@ -791,7 +797,7 @@ async function generatePrompts(project: Project) {
prompt: "Prompt 4 content",
name: "Prompt 4",
version: 1,
labels: ["production"],
labels: ["production", "latest"],
tags: ["tag1", "tag2"],
},
];
@@ -859,6 +865,7 @@ async function generatePrompts(project: Project) {
frequencyPenalty: 0.5,
},
version: 3,
labels: ["production", "latest"],
},
];
@@ -908,7 +915,7 @@ async function generatePrompts(project: Project) {
prompt: `${promptName} version ${i} content`,
name: promptName,
version: i,
labels: i === 20 ? ["production"] : [],
labels: i === 20 ? ["production", "latest"] : [],
},
update: {
id: promptId,
+27 -15
View File
@@ -1,5 +1,5 @@
import z from "zod";
import { ModelProvider } from "../..";
import { ModelProvider, UIModelParams } from "../..";
export const langfuseObjects = [
"trace",
@@ -77,26 +77,38 @@ export const availableEvalVariables = [
},
];
export const evalLLMModels = [
export const evalLLMModels: UIModelParams[] = [
{
provider: ModelProvider.OpenAI,
model: "gpt-3.5-turbo",
temperature: 1,
maxTemperature: 2,
max_tokens: 256,
top_p: 1,
provider: { value: ModelProvider.OpenAI, enabled: true },
model: { value: "gpt-3.5-turbo", enabled: true },
temperature: { value: 1, enabled: true },
maxTemperature: { value: 2, enabled: true },
max_tokens: { value: 256, enabled: true },
top_p: { value: 1, enabled: true },
},
{
provider: ModelProvider.OpenAI,
model: "gpt-4-turbo-preview",
temperature: 1,
maxTemperature: 2,
max_tokens: 256,
top_p: 1,
provider: { value: ModelProvider.OpenAI, enabled: true },
model: { value: "gpt-4-turbo-preview", enabled: true },
temperature: { value: 1, enabled: true },
maxTemperature: { value: 2, enabled: true },
max_tokens: { value: 256, enabled: true },
top_p: { value: 1, enabled: true },
},
{
provider: { value: ModelProvider.OpenAI, enabled: true },
model: { value: "gpt-4o", enabled: true },
temperature: { value: 1, enabled: true },
maxTemperature: { value: 2, enabled: true },
max_tokens: { value: 256, enabled: true },
top_p: { value: 1, enabled: true },
},
] as const;
export const EvalModelNames = z.enum(["gpt-3.5-turbo", "gpt-4-turbo-preview"]);
export const EvalModelNames = z.enum([
"gpt-3.5-turbo",
"gpt-4-turbo-preview",
"gpt-4o",
]);
export const OutputSchema = z.object({
reasoning: z.string(),
+10 -4
View File
@@ -20,11 +20,15 @@ export enum ChatMessageRole {
Assistant = "assistant",
}
export type ModelParams = AnthropicModelParams | OpenAIModelParams;
export type UIModelParams = Required<
AnthropicModelParams | OpenAIModelParams
> & {
maxTemperature: number;
type RecordWithEnabledFlag<T> = {
[K in keyof T]: { value: T[K]; enabled: boolean };
};
export type UIModelParams = RecordWithEnabledFlag<
Required<AnthropicModelParams | OpenAIModelParams> & {
maxTemperature: number;
}
>;
// Generic config
export type ModelConfig = z.infer<typeof ZodModelConfig>;
@@ -42,6 +46,8 @@ export type OpenAIModelParams = {
} & ModelConfig;
export const openAIModels = [
"gpt-4o",
"gpt-4o-2024-05-13",
"gpt-4-turbo-preview",
"gpt-4-1106-preview",
"gpt-4-0613",
+601 -184
View File
File diff suppressed because it is too large Load Diff
+14 -11
View File
@@ -1,14 +1,13 @@
{
"name": "web",
"version": "2.37.3",
"version": "2.43.2",
"private": true,
"license": "MIT",
"engines": {
"node": ">=20"
},
"scripts": {
"copy-openapi-spec": "cp generated/openapi-client/openapi.yml public/openapi-client.yml && cp generated/openapi-server/openapi.yml public/openapi-server.yml && cp generated/postman/collection.json public/postman-collection.json",
"build": "pnpm run copy-openapi-spec && INLINE_RUNTIME_CHUNK=false dotenv -e ../.env -- next build",
"build": "INLINE_RUNTIME_CHUNK=false dotenv -e ../.env -- next build",
"dev": "dotenv -e ../.env -- next dev",
"lint": "dotenv -e ../.env -- next lint",
"lint:fix": "dotenv -e ../.env -- next lint --fix",
@@ -23,7 +22,7 @@
"dependencies": {
"@anthropic-ai/tokenizer": "^0.0.4",
"@aws-sdk/client-s3": "^3.550.0",
"@aws-sdk/lib-storage": "^3.550.0",
"@aws-sdk/lib-storage": "^3.568.0",
"@aws-sdk/s3-request-presigner": "^3.554.0",
"@codemirror/lang-json": "^6.0.1",
"@headlessui/react": "^1.7.19",
@@ -32,8 +31,8 @@
"@langchain/anthropic": "^0.1.16",
"@langchain/core": "^0.1.61",
"@langchain/openai": "^0.0.28",
"@langfuse/shared": "workspace:*",
"@langfuse/ee": "workspace:*",
"@langfuse/shared": "workspace:*",
"@marsidev/react-turnstile": "^0.5.4",
"@next-auth/prisma-adapter": "^1.0.7",
"@radix-ui/react-accordion": "^1.1.2",
@@ -60,14 +59,16 @@
"@react-email/render": "^0.0.13",
"@repo/eslint-config": "workspace:*",
"@repo/typescript-config": "workspace:*",
"@sentry/nextjs": "^7.110.1",
"@sentry/node": "^7.110.1",
"@sentry/profiling-node": "^7.110.1",
"@sentry/types": "^7.110.1",
"@sentry/nextjs": "^7.113.0",
"@sentry/node": "^7.113.0",
"@sentry/profiling-node": "^7.113.0",
"@sentry/types": "^7.113.0",
"@t3-oss/env-nextjs": "^0.8.0",
"@tailwindcss/container-queries": "^0.1.1",
"@tailwindcss/forms": "^0.5.7",
"@tanstack/react-query": "^4.36.1",
"@tanstack/react-table": "^8.11.8",
"@tanstack/react-virtual": "^3.5.0",
"@tremor/react": "3.11.1",
"@trpc/client": "^10.45.0",
"@trpc/next": "^10.45.0",
@@ -84,8 +85,9 @@
"cors": "^2.8.5",
"date-fns": "^3.3.1",
"decimal.js": "^10.4.3",
"dompurify": "^3.1.2",
"exponential-backoff": "^3.1.1",
"js-tiktoken": "^1.0.11",
"js-tiktoken": "^1.0.12",
"kysely": "^0.27.3",
"langchain": "^0.1.36",
"lodash": "^4.17.21",
@@ -96,7 +98,7 @@
"nodemailer": "^6.9.13",
"posthog-js": "^1.122.0",
"posthog-node": "^3.6.3",
"prisma": "^5.12.1",
"prisma": "^5.13.0",
"react": "18.2.0",
"react-day-picker": "^8.10.1",
"react-dom": "18.2.0",
@@ -120,6 +122,7 @@
"@testing-library/react": "^15.0.6",
"@types/bcryptjs": "^2.4.6",
"@types/cors": "^2.8.17",
"@types/dompurify": "^3.0.5",
"@types/eslint": "^8.56.7",
"@types/jest": "^29.5.12",
"@types/lodash": "^4.14.202",
-2
View File
@@ -1,2 +0,0 @@
openapi*.yml
postman-collection.json
@@ -19,9 +19,10 @@ info:
## Exports
- OpenAPI spec: https://cloud.langfuse.com/openapi-server.yml
- OpenAPI spec: https://cloud.langfuse.com/generated/api/openapi.yml
- Postman collection: https://cloud.langfuse.com/postman-collection.json
- Postman collection:
https://cloud.langfuse.com/generated/postman/collection.json
paths:
/api/public/dataset-items:
post:
@@ -495,7 +496,7 @@ paths:
in: query
description: >-
Optional filter to only include traces on or after a certain
timestamp
datetime (ISO 8601)
required: false
schema:
type: string
@@ -503,7 +504,9 @@ paths:
nullable: true
- name: toTimestamp
in: query
description: Optional filter to only include traces before a certain timestamp
description: >-
Optional filter to only include traces before a certain datetime
(ISO 8601)
required: false
schema:
type: string
@@ -647,7 +650,7 @@ paths:
in: query
description: >-
Retrieve only observations with a start_time greater than this
timestamp.
datetime (ISO 8601).
required: false
schema:
type: string
@@ -985,7 +988,7 @@ paths:
nullable: true
- name: fromTimestamp
in: query
description: Retrieve only scores newer than this timestamp.
description: Retrieve only scores newer than this datetime (ISO 8601).
required: false
schema:
type: string
@@ -1261,7 +1264,7 @@ paths:
nullable: true
- name: fromTimestamp
in: query
description: Retrieve only traces newer than this timestamp.
description: Retrieve only traces newer than this datetime (ISO 8601).
required: false
schema:
type: string
@@ -1847,6 +1850,10 @@ components:
type: string
nullable: true
description: Dataset items are upserted on their id
status:
$ref: '#/components/schemas/DatasetStatus'
nullable: true
description: Defaults to ACTIVE for newly created items
required:
- datasetName
CreateDatasetRunItemRequest:
@@ -2,7 +2,7 @@
"info": {
"name": "Langfuse",
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json",
"description": "## Authentication\n\nAuthenticate with the API using [Basic Auth](https://en.wikipedia.org/wiki/Basic_access_authentication), get API keys in the project settings:\n\n- username: Langfuse Public Key\n- password: Langfuse Secret Key\n\n## Exports\n\n- OpenAPI spec: https://cloud.langfuse.com/openapi-server.yml\n- Postman collection: https://cloud.langfuse.com/postman-collection.json"
"description": "## Authentication\n\nAuthenticate with the API using [Basic Auth](https://en.wikipedia.org/wiki/Basic_access_authentication), get API keys in the project settings:\n\n- username: Langfuse Public Key\n- password: Langfuse Secret Key\n\n## Exports\n\n- OpenAPI spec: https://cloud.langfuse.com/generated/api/openapi.yml\n- Postman collection: https://cloud.langfuse.com/generated/postman/collection.json"
},
"variable": [
{
@@ -65,7 +65,7 @@
"auth": null,
"body": {
"mode": "raw",
"raw": "{\n \"datasetName\": \"example\",\n \"input\": \"UNKNOWN\",\n \"expectedOutput\": \"UNKNOWN\",\n \"metadata\": \"UNKNOWN\",\n \"sourceTraceId\": \"example\",\n \"sourceObservationId\": \"example\",\n \"id\": \"example\"\n}",
"raw": "{\n \"datasetName\": \"example\",\n \"input\": \"UNKNOWN\",\n \"expectedOutput\": \"UNKNOWN\",\n \"metadata\": \"UNKNOWN\",\n \"sourceTraceId\": \"example\",\n \"sourceObservationId\": \"example\",\n \"id\": \"example\",\n \"status\": \"ACTIVE\"\n}",
"options": {
"raw": {
"language": "json"
@@ -418,12 +418,12 @@
{
"key": "fromTimestamp",
"value": "",
"description": "Optional filter to only include traces on or after a certain timestamp"
"description": "Optional filter to only include traces on or after a certain datetime (ISO 8601)"
},
{
"key": "toTimestamp",
"value": "",
"description": "Optional filter to only include traces before a certain timestamp"
"description": "Optional filter to only include traces before a certain datetime (ISO 8601)"
}
],
"variable": []
@@ -528,7 +528,7 @@
{
"key": "fromStartTime",
"value": "",
"description": "Retrieve only observations with a start_time greater than this timestamp."
"description": "Retrieve only observations with a start_time greater than this datetime (ISO 8601)."
}
],
"variable": []
@@ -788,7 +788,7 @@
{
"key": "fromTimestamp",
"value": "",
"description": "Retrieve only scores newer than this timestamp."
"description": "Retrieve only scores newer than this datetime (ISO 8601)."
},
{
"key": "source",
@@ -996,7 +996,7 @@
{
"key": "fromTimestamp",
"value": "",
"description": "Retrieve only traces newer than this timestamp."
"description": "Retrieve only traces newer than this datetime (ISO 8601)."
},
{
"key": "orderBy",
+1
View File
@@ -28,6 +28,7 @@ if (process.env.NEXT_PUBLIC_SENTRY_DSN)
// Add profiling integration to list of integrations
new ProfilingIntegration(),
new Sentry.Integrations.Prisma({ client: prisma }),
Sentry.metrics.metricsAggregatorIntegration(),
],
// filter out passwords from the signup request body
+4
View File
@@ -210,11 +210,14 @@ describe("/api/public/datasets and /api/public/dataset-items API Endpoints", ()
datasetName: "dataset-name",
input: { key: "value2" },
metadata: ["hello-world"],
status: "ARCHIVED",
});
expect(item2.status).toBe(200);
expect(item2.body).toMatchObject({
id: "dataset-item-id",
input: { key: "value2" },
metadata: ["hello-world"],
status: "ARCHIVED",
});
const dbDatasetItem = await prisma.datasetItem.findFirst({
@@ -223,6 +226,7 @@ describe("/api/public/datasets and /api/public/dataset-items API Endpoints", ()
expect(dbDatasetItem).not.toBeNull();
expect(dbDatasetItem?.input).toMatchObject({ key: "value2" });
expect(dbDatasetItem?.metadata).toMatchObject(["hello-world"]);
expect(dbDatasetItem?.status).toBe("ARCHIVED");
});
it("should create and get a dataset run, include special characters", async () => {
+19
View File
@@ -0,0 +1,19 @@
import * as DOMPurify from "dompurify";
// Create test cases
describe("should sanitize dom", () => {
it("should sanitize urls", () => {
const dirty =
"http://www.some.site/page.html?default=<script>alert(document.cookie)</script>";
const cleam = DOMPurify.sanitize(dirty);
expect(cleam).toBe("http://www.some.site/page.html?default=");
});
it("should should not sanitize valid langfuse urls", () => {
const validLangfuseUrl =
"https://cloud.langfuse.com/project/abjdnksadn/traces?filter=timestamp%3Bdatetime%3B%3B%3E%3B2024-04-19T00%253A00%253A00.000Z%2Cid%3Bstring%3B%3Bcontains%3Ba%2Ctimestamp%3Bdatetime%3B%3B%3C%3B2024-04-30T22%253A00%253A00.000Z%2CinputTokens%3Bnumber%3B%3B%3E%3B1";
const cleam = DOMPurify.sanitize(validLangfuseUrl);
expect(cleam).toBe(validLangfuseUrl);
});
});
+1 -166
View File
@@ -281,6 +281,7 @@ describe("/api/public/ingestion API Endpoint", () => {
expect(dbScore?.name).toBe("score-name");
expect(dbScore?.value).toBe(100.5);
expect(dbScore?.observationId).toBeNull();
expect(dbScore?.projectId).toBe("7a88fb47-b4e2-43b8-a06c-a5ce950dc53a");
});
});
@@ -816,57 +817,6 @@ describe("/api/public/ingestion API Endpoint", () => {
expect(dbTrace.length).toBe(1);
});
it("should fail for auth errors", async () => {
const traceId = v4();
const scoreId = v4();
const responseOne = await makeAPICall("POST", "/api/public/ingestion", {
batch: [
{
id: v4(),
type: "trace-create",
timestamp: new Date().toISOString(),
body: {
id: traceId,
name: "trace-name",
userId: "user-1",
metadata: { key: "value" },
release: "1.0.0",
version: "2.0.0",
},
},
{
id: v4(),
type: "score-create",
timestamp: new Date().toISOString(),
body: {
id: scoreId,
name: "score-name",
value: 100.5,
traceId: "some-random-id",
},
},
],
});
expect(responseOne.status).toBe(207);
expect("errors" in responseOne.body).toBe(true);
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
expect(responseOne.body.errors.length).toBe(1);
expect("successes" in responseOne.body).toBe(true);
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
expect(responseOne.body.successes.length).toBe(1);
const dbTrace = await prisma.trace.findMany({
where: {
name: "trace-name",
},
});
expect(dbTrace.length).toBe(1);
});
it("should fail for resource not found", async () => {
const traceId = v4();
@@ -1653,121 +1603,6 @@ IB Home / . . . / News / News about the IB / Why ChatGPT is an o
expect(dbScore?.value).toBe(scoreValue);
});
it("should error on Bearer Auth for a trace from different project", async () => {
const otherProjectId = "other_project_id";
const traceId = "trace_id";
const bearerAuth = "Bearer pk-lf-1234567890";
const scoreId = "score_id";
const scoreEventId = "score_event_id";
const scoreName = "score-name";
const scoreValue = 100.5;
// Seed db with a trace to be scored
try {
await prisma.project.create({
data: {
id: otherProjectId,
name: "another-project",
},
});
await prisma.trace.create({
data: {
id: traceId,
name: "trace-name",
project: { connect: { id: otherProjectId } },
},
});
const response = await makeAPICall(
"POST",
"/api/public/ingestion",
{
batch: [
{
id: scoreEventId,
type: "score-create",
timestamp: new Date().toISOString(),
body: {
id: scoreId,
name: scoreName,
value: scoreValue,
traceId: traceId,
},
},
],
},
bearerAuth,
);
expect(response.status).toBe(207);
expect(response.body.successes.length).toBe(0);
expect(response.body).toHaveProperty("errors");
expect(response.body.errors.length).toEqual(1);
expect(response.body.errors[0]?.id).toEqual(scoreEventId);
expect(await prisma.trace.count()).toBe(1);
expect(await prisma.trace.count({ where: { id: traceId } })).toBe(1);
const dbScore = await prisma.score.findUnique({
where: {
id: scoreId,
},
});
expect(dbScore).toBeNull();
} finally {
await prisma.project.delete({ where: { id: otherProjectId } });
}
});
it("should error on Bearer Auth with a trace that does not exist", async () => {
const traceId = "trace_id";
const bearerAuth = "Bearer pk-lf-1234567890";
const scoreId = "score_id";
const scoreEventId = "score_event_id";
const scoreName = "score-name";
const scoreValue = 100.5;
const response = await makeAPICall(
"POST",
"/api/public/ingestion",
{
batch: [
{
id: scoreEventId,
type: "score-create",
timestamp: new Date().toISOString(),
body: {
id: scoreId,
name: scoreName,
value: scoreValue,
traceId: traceId,
},
},
],
},
bearerAuth,
);
expect(response.status).toBe(207);
expect(response.body.successes.length).toBe(0);
expect(response.body.errors.length).toBe(1);
expect(response.body.errors[0]?.id).toBe(scoreEventId);
expect(await prisma.trace.count()).toBe(0);
const dbScore = await prisma.score.findUnique({
where: {
id: scoreId,
},
});
expect(dbScore).toBeNull();
});
it("should error on wrong input", async () => {
const traceId = "trace_id";
const bearerAuth = "Bearer pk-lf-1234567890";
+10 -1
View File
@@ -73,6 +73,15 @@ describe("model match", () => {
input: "I am a prompt",
output: "I am a completion",
},
{
type: "GENERATION",
projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
model: "gpt-4o",
startTime: new Date("2024-01-01T00:00:00.000Z"),
unit: ModelUsageUnit.Tokens,
input: "I am a prompt",
output: "I am a completion",
},
],
});
@@ -86,7 +95,7 @@ describe("model match", () => {
expect(observation1?.completionTokens).toEqual(3000);
expect(observation1?.totalTokens).toEqual(0);
expect(observations.length).toEqual(3);
expect(observations.length).toEqual(4);
observations.forEach((observation) => {
expect(observation.internalModel).toBeDefined();
expect(observation.promptTokens).toBeGreaterThan(0);
+36 -29
View File
@@ -709,7 +709,7 @@ describe("/api/public/v2/prompts API Endpoint", () => {
create: {
id: otherProjectId,
name: "demo-app",
members: {
projectMembers: {
create: {
role: "OWNER",
userId: "user-test",
@@ -784,10 +784,17 @@ describe("/api/public/v2/prompts API Endpoint", () => {
expect(promptMeta3.tags).toEqual(["tag-1"]);
// Validate pagination
expect(body.pagination.page).toBe(1);
expect(body.pagination.limit).toBe(10);
expect(body.pagination.totalPages).toBe(1);
expect(body.pagination.totalItems).toBe(3);
expect(body.meta.page).toBe(1);
expect(body.meta.limit).toBe(10);
expect(body.meta.totalPages).toBe(1);
expect(body.meta.totalItems).toBe(3);
// Validate pagination backwards compatibility
// https://github.com/langfuse/langfuse/issues/2068
expect(body.pagination?.page).toBe(1);
expect(body.pagination?.limit).toBe(10);
expect(body.pagination?.totalPages).toBe(1);
expect(body.pagination?.totalItems).toBe(3);
});
it("should fetch a prompt meta list with name filter", async () => {
@@ -802,10 +809,10 @@ describe("/api/public/v2/prompts API Endpoint", () => {
expect(body.data[0].tags).toEqual([]);
// Validate pagination
expect(body.pagination.page).toBe(1);
expect(body.pagination.limit).toBe(10);
expect(body.pagination.totalPages).toBe(1);
expect(body.pagination.totalItems).toBe(1);
expect(body.meta.page).toBe(1);
expect(body.meta.limit).toBe(10);
expect(body.meta.totalPages).toBe(1);
expect(body.meta.totalItems).toBe(1);
// Test with a different name
const response2 = await makeAPICall("GET", `${baseURI}?name=prompt-2`);
@@ -819,10 +826,10 @@ describe("/api/public/v2/prompts API Endpoint", () => {
expect(body2.data[0].tags).toEqual([]);
// Validate pagination
expect(body2.pagination.page).toBe(1);
expect(body2.pagination.limit).toBe(10);
expect(body2.pagination.totalPages).toBe(1);
expect(body2.pagination.totalItems).toBe(1);
expect(body2.meta.page).toBe(1);
expect(body2.meta.limit).toBe(10);
expect(body2.meta.totalPages).toBe(1);
expect(body2.meta.totalItems).toBe(1);
// Return 200 with empty list if name does not exist
const response3 = await makeAPICall(
@@ -846,10 +853,10 @@ describe("/api/public/v2/prompts API Endpoint", () => {
expect(body.data[0].tags).toEqual(["tag-1"]);
// Validate pagination
expect(body.pagination.page).toBe(1);
expect(body.pagination.limit).toBe(10);
expect(body.pagination.totalPages).toBe(1);
expect(body.pagination.totalItems).toBe(1);
expect(body.meta.page).toBe(1);
expect(body.meta.limit).toBe(10);
expect(body.meta.totalPages).toBe(1);
expect(body.meta.totalItems).toBe(1);
// Return 200 with empty list if tag does not exist
const response3 = await makeAPICall("GET", `${baseURI}?tag=non-existent`);
@@ -875,10 +882,10 @@ describe("/api/public/v2/prompts API Endpoint", () => {
).toBe(true);
// Validate pagination
expect(body.pagination.page).toBe(1);
expect(body.pagination.limit).toBe(10);
expect(body.pagination.totalPages).toBe(1);
expect(body.pagination.totalItems).toBe(3);
expect(body.meta.page).toBe(1);
expect(body.meta.limit).toBe(10);
expect(body.meta.totalPages).toBe(1);
expect(body.meta.totalItems).toBe(3);
// Test with a different label
const response2 = await makeAPICall("GET", `${baseURI}?label=dev`);
@@ -892,10 +899,10 @@ describe("/api/public/v2/prompts API Endpoint", () => {
expect(body2.data[0].tags).toEqual([]);
// Validate pagination
expect(body2.pagination.page).toBe(1);
expect(body2.pagination.limit).toBe(10);
expect(body2.pagination.totalPages).toBe(1);
expect(body2.pagination.totalItems).toBe(1);
expect(body2.meta.page).toBe(1);
expect(body2.meta.limit).toBe(10);
expect(body2.meta.totalPages).toBe(1);
expect(body2.meta.totalItems).toBe(1);
// Return 200 with empty list if label does not exist
const response3 = await makeAPICall(
@@ -914,10 +921,10 @@ describe("/api/public/v2/prompts API Endpoint", () => {
const body = response.body as unknown as PromptsMetaResponse;
expect(body.data).toHaveLength(1);
expect(body.pagination.page).toBe(1);
expect(body.pagination.limit).toBe(1);
expect(body.pagination.totalPages).toBe(3);
expect(body.pagination.totalItems).toBe(3);
expect(body.meta.page).toBe(1);
expect(body.meta.limit).toBe(1);
expect(body.meta.totalPages).toBe(3);
expect(body.meta.totalItems).toBe(3);
});
});
@@ -50,7 +50,7 @@ describe("Build valid SQL queries", () => {
table: "traces_scores",
values: ["project-id"],
strings: [
' FROM traces t JOIN scores s ON t.id = s.trace_id WHERE t."project_id" = ',
' FROM traces t JOIN scores s ON t.id = s.trace_id AND t.project_id = s.project_id WHERE t."project_id" = ',
";",
],
} as const,
+1
View File
@@ -60,6 +60,7 @@ describe("/api/public/scores API Endpoint", () => {
expect(dbScore?.observationId).toBeNull();
expect(dbScore?.comment).toBe("comment");
expect(dbScore?.source).toBe("API");
expect(dbScore?.projectId).toBe("7a88fb47-b4e2-43b8-a06c-a5ce950dc53a");
});
it("should create score for a trace with int", async () => {
+1 -1
View File
@@ -90,7 +90,7 @@ export const setupUserAndProject = async () => {
},
],
},
members: {
projectMembers: {
create: {
role: "OWNER",
userId: user.id,
+1
View File
@@ -33,6 +33,7 @@ describe("Token Count Functions", () => {
{ model: "claude", tokenizer: "claude", tokens: 118 },
{ model: "claude-instant-1.2", tokenizer: "claude", tokens: 118 },
{ model: "gpt-3.5-turbo-1106", tokenizer: "openai", tokens: 114 },
{ model: "gpt-4o-2024-05-13", tokenizer: "openai", tokens: 112 },
].forEach(({ model, tokens, tokenizer }) => {
it(`should return token count ${tokens} for ${model}`, () => {
const result = tokenCount({
+3 -3
View File
@@ -39,7 +39,7 @@ export const LangfuseLogo = ({
{showEnvLabel && env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION && (
<div
className={cn(
"flex items-center gap-2 self-stretch rounded-md px-3 py-2 text-xs ring-1 lg:-mx-2",
"flex items-center gap-2 self-stretch rounded-md px-1 py-1 text-xs ring-1 sm:px-3 sm:py-2 lg:-mx-2",
env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION === "STAGING"
? "bg-blue-100 text-blue-500 ring-blue-500"
: env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION === "DEV"
@@ -48,9 +48,9 @@ export const LangfuseLogo = ({
)}
>
{env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION === "DEV" ? (
<Check size={16} />
<Check size={16} className="hidden sm:block" />
) : (
<AlertTriangle size={16} />
<AlertTriangle size={16} className="hidden sm:block" />
)}
<span className="whitespace-nowrap">
{["EU", "US"].includes(env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION)
+90 -53
View File
@@ -1,8 +1,3 @@
import {
supportedModels,
ModelProvider,
type UIModelParams,
} from "@langfuse/shared";
import { Input } from "@/src/components/ui/input";
import {
Select,
@@ -12,22 +7,31 @@ import {
SelectValue,
} from "@/src/components/ui/select";
import { Slider } from "@/src/components/ui/slider";
import { Switch } from "@/src/components/ui/switch";
import { cn } from "@/src/utils/tailwind";
import {
ModelProvider,
supportedModels,
type UIModelParams,
} from "@langfuse/shared";
export type ModelParamsContext = {
modelParams: UIModelParams;
availableModels?: UIModelParams[];
updateModelParam: <Key extends keyof UIModelParams>(
updateModelParamValue: <Key extends keyof UIModelParams>(
key: Key,
value: UIModelParams[Key],
value: UIModelParams[Key]["value"],
) => void;
disabled?: boolean;
setModelParamEnabled?: (key: keyof UIModelParams, enabled: boolean) => void;
formDisabled?: boolean;
};
export const ModelParameters: React.FC<ModelParamsContext> = ({
modelParams,
availableModels,
updateModelParam,
disabled,
updateModelParamValue,
setModelParamEnabled,
formDisabled = false,
}) => {
return (
<div className="flex flex-col space-y-4">
@@ -36,61 +40,69 @@ export const ModelParameters: React.FC<ModelParamsContext> = ({
<ModelParamsSelect
title="Provider"
modelParamsKey="provider"
disabled={disabled}
value={modelParams.provider}
disabled={formDisabled}
value={modelParams.provider.value}
options={
availableModels
? [...new Set(availableModels.map((m) => m.provider))]
? [...new Set(availableModels.map((m) => m.provider.value))]
: Object.values(ModelProvider)
}
updateModelParam={updateModelParam}
updateModelParam={updateModelParamValue}
/>
<ModelParamsSelect
title="Model name"
modelParamsKey="model"
disabled={disabled}
value={modelParams.model}
disabled={formDisabled}
value={modelParams.model.value}
options={Object.values(
availableModels
? availableModels
.filter((m) => m.provider === modelParams.provider)
.map((m) => m.model)
: supportedModels[modelParams.provider],
.filter(
(m) => m.provider.value === modelParams.provider.value,
)
.map((m) => m.model.value)
: supportedModels[modelParams.provider.value],
)}
updateModelParam={updateModelParam}
updateModelParam={updateModelParamValue}
/>
<ModelParamsSlider
title="Temperature"
modelParamsKey="temperature"
disabled={disabled}
value={modelParams.temperature}
formDisabled={formDisabled}
enabled={modelParams.temperature.enabled}
setModelParamEnabled={setModelParamEnabled}
value={modelParams.temperature.value}
min={0}
max={modelParams.maxTemperature}
max={modelParams.maxTemperature.value}
step={0.01}
tooltip="The sampling temperature. Higher values will make the output more random, while lower values like will make it more focused and deterministic."
updateModelParam={updateModelParam}
tooltip="The sampling temperature. Higher values will make the output more random, while lower values will make it more focused and deterministic."
updateModelParam={updateModelParamValue}
/>
<ModelParamsSlider
title="Output token limit"
modelParamsKey="max_tokens"
disabled={disabled}
value={modelParams.max_tokens}
formDisabled={formDisabled}
enabled={modelParams.max_tokens.enabled}
setModelParamEnabled={setModelParamEnabled}
value={modelParams.max_tokens.value}
min={1}
max={4096}
step={1}
tooltip="The maximum number of tokens that can be generated in the chat completion."
updateModelParam={updateModelParam}
updateModelParam={updateModelParamValue}
/>
<ModelParamsSlider
title="Top P"
modelParamsKey="top_p"
disabled={disabled}
value={modelParams.top_p}
formDisabled={formDisabled}
enabled={modelParams.top_p.enabled}
setModelParamEnabled={setModelParamEnabled}
value={modelParams.top_p.value}
min={0}
max={1}
step={0.01}
tooltip="An alternative to sampling with temperature, called nucleus sampling, where the model considers the results of the tokens with top_p probability mass. So 0.1 means only the tokens comprising the top 10% probability mass are considered. We generally recommend altering this or temperature but not both."
updateModelParam={updateModelParam}
updateModelParam={updateModelParamValue}
/>
</div>
</div>
@@ -102,7 +114,7 @@ type ModelParamsSelectProps = {
modelParamsKey: keyof UIModelParams;
value: string;
options: string[];
updateModelParam: ModelParamsContext["updateModelParam"];
updateModelParam: ModelParamsContext["updateModelParamValue"];
disabled?: boolean;
};
const ModelParamsSelect = ({
@@ -115,7 +127,9 @@ const ModelParamsSelect = ({
}: ModelParamsSelectProps) => {
return (
<div className="space-y-2">
<p className="text-xs font-semibold">{title}</p>
<p className={cn("text-xs font-semibold", disabled && "text-gray-400")}>
{title}
</p>
<Select
disabled={disabled}
onValueChange={(value) =>
@@ -149,8 +163,10 @@ type ModelParamsSliderProps = {
min: number;
max: number;
step: number;
updateModelParam: ModelParamsContext["updateModelParam"];
disabled?: boolean;
updateModelParam: ModelParamsContext["updateModelParamValue"];
enabled?: boolean;
formDisabled?: boolean;
setModelParamEnabled?: ModelParamsContext["setModelParamEnabled"];
};
const ModelParamsSlider = ({
title,
@@ -161,30 +177,51 @@ const ModelParamsSlider = ({
max,
step,
updateModelParam,
disabled,
setModelParamEnabled,
enabled,
formDisabled,
}: ModelParamsSliderProps) => {
return (
<div className="space-y-3" title={tooltip}>
<div className="flex flex-row">
<p className="flex-1 text-xs font-semibold">{title}</p>
<Input
className="h-6 w-14 appearance-none px-2 text-right"
type="number"
disabled={disabled}
min={min}
max={max}
step={step}
value={value}
onChange={(event) => {
updateModelParam(
modelParamsKey,
Math.max(Math.min(parseFloat(event.target.value), max), min),
);
}}
/>
<p
className={cn(
"flex-1 text-xs font-semibold",
(!enabled || formDisabled) && "text-gray-400",
)}
>
{title}
</p>
<div className="flex flex-row space-x-3">
<Input
className="h-6 w-14 appearance-none px-2 text-right"
type="number"
disabled={!enabled || formDisabled}
min={min}
max={max}
step={step}
value={value}
onChange={(event) => {
updateModelParam(
modelParamsKey,
Math.max(Math.min(parseFloat(event.target.value), max), min),
);
}}
/>
{setModelParamEnabled ? (
<Switch
title={`Control sending the ${title} parameter`}
disabled={formDisabled}
checked={enabled}
onCheckedChange={(checked) => {
setModelParamEnabled(modelParamsKey, checked);
}}
/>
) : null}
</div>
</div>
<Slider
disabled={disabled}
disabled={!enabled || formDisabled}
min={min}
max={max}
step={step}
+13 -4
View File
@@ -7,10 +7,10 @@ import {
} from "@/src/components/ui/popover";
import { Button } from "@/src/components/ui/button";
import { TrashIcon } from "lucide-react";
import { usePostHog } from "posthog-js/react";
import { useHasAccess } from "@/src/features/rbac/utils/checkAccess";
import { type Scope } from "@/src/features/rbac/constants/roleAccessRights";
import { api } from "@/src/utils/api";
import { usePostHogClientCapture } from "@/src/features/posthog-analytics/usePostHogClientCapture";
interface DeleteButtonProps {
itemId: string;
@@ -33,7 +33,7 @@ export function DeleteButton({
}: DeleteButtonProps) {
const [isDeleted, setIsDeleted] = useState(false);
const router = useRouter();
const posthog = usePostHog();
const capture = usePostHogClientCapture();
const hasAccess = useHasAccess({ projectId, scope: scope });
const traceMutation = api.traces.deleteMany.useMutation({
@@ -63,6 +63,15 @@ export function DeleteButton({
<Button
variant={isTableAction ? "ghost" : "outline"}
size={isTableAction ? "xs" : "icon"}
onClick={() =>
type === "trace"
? capture("trace:delete_form_open", {
source: isTableAction ? "table-single-row" : "trace detail",
})
: capture("datasets:delete_form_open", {
source: "dataset",
})
}
>
<TrashIcon className="h-4 w-4" />
</Button>
@@ -84,7 +93,7 @@ export function DeleteButton({
traceIds: [itemId],
projectId,
});
posthog.capture("trace:delete", {
capture("trace:delete", {
source: isTableAction ? "table-single-row" : "trace",
});
}}
@@ -101,7 +110,7 @@ export function DeleteButton({
projectId,
datasetId: itemId,
});
posthog.capture("dataset:delete", {
capture("datasets:delete_dataset_button_click", {
source: isTableAction ? "table-single-row" : "dataset",
});
}}
+4 -1
View File
@@ -1,6 +1,7 @@
import CodeMirror, { EditorView } from "@uiw/react-codemirror";
import { githubLight } from "@uiw/codemirror-theme-github";
import { json } from "@codemirror/lang-json";
import { cn } from "@/src/utils/tailwind";
// todo: add json linting
@@ -9,11 +10,13 @@ export function JsonEditor({
onChange,
editable = true,
lineWrapping = true,
className,
}: {
defaultValue: string;
onChange?: (value: string) => void;
editable?: boolean;
lineWrapping?: boolean;
className?: string;
}) {
return (
<CodeMirror
@@ -26,7 +29,7 @@ export function JsonEditor({
extensions={[json(), ...(lineWrapping ? [EditorView.lineWrapping] : [])]}
defaultValue={defaultValue}
onChange={onChange}
className="overflow-hidden rounded-md border"
className={cn("overflow-hidden rounded-md border", className)}
editable={editable}
/>
);
+12 -3
View File
@@ -3,9 +3,9 @@ import {
HoverCardContent,
HoverCardTrigger,
} from "@/src/components/ui/hover-card";
import { usePostHogClientCapture } from "@/src/features/posthog-analytics/usePostHogClientCapture";
import { HelpCircle, Info } from "lucide-react";
import Link from "next/link";
import { usePostHog } from "posthog-js/react";
export type DocPopupProps = {
description: React.ReactNode;
@@ -26,14 +26,17 @@ export default function DocPopup({
md: "w-6 h-6",
lg: "w-8 h-8",
};
const posthog = usePostHog();
const capture = usePostHogClientCapture();
return (
<HoverCard
openDelay={200}
onOpenChange={(open) => {
if (open) {
posthog.capture("help-popup:opened", { href, description });
capture("help_popup:opened", {
hfref: href,
description: description,
});
}
}}
>
@@ -44,6 +47,12 @@ export default function DocPopup({
rel="noopener"
target="_blank"
className="inline-block whitespace-nowrap text-gray-500 sm:pl-0"
onClick={() => {
capture("help_popup:href_clicked", {
href: href,
description: description,
});
}}
>
{
{
@@ -0,0 +1,9 @@
export const FullScreenPage: React.FC<React.PropsWithChildren<{}>> = ({
children,
}) => {
return (
<div className="flex h-[calc(100dvh-6rem)] flex-col overflow-hidden lg:h-[calc(100dvh-2rem)]">
{children}
</div>
);
};
+8 -3
View File
@@ -14,7 +14,6 @@ import {
AvatarFallback,
AvatarImage,
} from "@/src/components/ui/avatar";
import { api } from "@/src/utils/api";
import { NewProjectButton } from "@/src/features/projects/components/NewProjectButton";
import { FeedbackButtonWrapper } from "@/src/features/feedback/component/FeedbackButton";
import { Button } from "@/src/components/ui/button";
@@ -31,6 +30,7 @@ import {
import { ChevronDownIcon } from "@heroicons/react/20/solid";
import useLocalStorage from "@/src/components/useLocalStorage";
import { ProjectNavigation } from "@/src/components/projectNavigation";
import DOMPurify from "dompurify";
const userNavigation = [
{
@@ -61,7 +61,7 @@ export default function Layout(props: PropsWithChildren) {
useCheckNotification(NOTIFICATIONS, session.status === "authenticated");
const enableExperimentalFeatures =
api.environment.enableExperimentalFeatures.useQuery().data ?? false;
session.data?.environment.enableExperimentalFeatures ?? false;
const projectId = router.query.projectId as string | undefined;
@@ -163,7 +163,12 @@ export default function Layout(props: PropsWithChildren) {
unauthenticatedPaths.includes(router.pathname)
) {
const targetPath = router.query.targetPath as string | undefined;
void router.replace(targetPath ?? "/");
const sanitizedTargetPath = targetPath
? DOMPurify.sanitize(targetPath)
: undefined;
void router.replace(sanitizedTargetPath ?? "/");
return <Spinner message="Redirecting" />;
}
+2 -2
View File
@@ -75,13 +75,13 @@ export const ROUTES: Route[] = [
name: "Configs",
pathname: `/project/[projectId]/evals/configs`,
cloudOnly: true,
rbacScope: "job:read",
rbacScope: "evalJob:read",
},
{
name: "Log",
pathname: `/project/[projectId]/evals/log`,
cloudOnly: true,
rbacScope: "jobExecution:read",
rbacScope: "evalJobExecution:read",
},
],
},
+5 -5
View File
@@ -1,8 +1,8 @@
import { Switch } from "@/src/components/ui/switch";
import { usePostHogClientCapture } from "@/src/features/posthog-analytics/usePostHogClientCapture";
import { useHasAccess } from "@/src/features/rbac/utils/checkAccess";
import { api } from "@/src/utils/api";
import { Link, LockIcon } from "lucide-react";
import { usePostHog } from "posthog-js/react";
import { useState } from "react";
export const PublishTraceSwitch = (props: {
@@ -10,7 +10,7 @@ export const PublishTraceSwitch = (props: {
projectId: string;
isPublic: boolean;
}) => {
const posthog = usePostHog();
const capture = usePostHogClientCapture();
const hasAccess = useHasAccess({
projectId: props.projectId,
scope: "objects:publish",
@@ -30,7 +30,7 @@ export const PublishTraceSwitch = (props: {
traceId: props.traceId,
public: val,
});
posthog.capture("trace_detail:publish_trace_button_click");
capture("trace_detail:publish_button_click");
}}
isLoading={mut.isLoading}
disabled={!hasAccess}
@@ -43,7 +43,7 @@ export const PublishSessionSwitch = (props: {
projectId: string;
isPublic: boolean;
}) => {
const posthog = usePostHog();
const capture = usePostHogClientCapture();
const hasAccess = useHasAccess({
projectId: props.projectId,
scope: "objects:publish",
@@ -63,7 +63,7 @@ export const PublishSessionSwitch = (props: {
sessionId: props.sessionId,
public: val,
});
posthog.capture("session_detail:publish_session_button_click");
capture("session_detail:publish_button_click");
}}
isLoading={mut.isLoading}
disabled={!hasAccess}
+116 -32
View File
@@ -13,13 +13,32 @@ import { useDetailPageLists } from "@/src/features/navigate-detail-pages/context
import { api } from "@/src/utils/api";
import { usdFormatter } from "@/src/utils/numbers";
import Link from "next/link";
import { useEffect } from "react";
import { useEffect, useRef } from "react";
import { useWindowVirtualizer } from "@tanstack/react-virtual";
import { type RouterOutput } from "@/src/utils/types";
import {
DataTableRowHeightSwitch,
type RowHeight,
useRowHeightLocalStorage,
} from "@/src/components/table/data-table-row-height-switch";
import { ScrollArea } from "@/src/components/ui/scroll-area";
// do not use the usual table row heights here
const rowHeightMapping: Record<RowHeight, number> = {
s: 200,
m: 350,
l: 700,
};
export const SessionPage: React.FC<{
sessionId: string;
projectId: string;
}> = ({ sessionId, projectId }) => {
const { setDetailPageList } = useDetailPageLists();
const [rowHeight, setRowHeight] = useRowHeightLocalStorage(
"single-session",
"m",
);
const session = api.sessions.byId.useQuery(
{
sessionId,
@@ -77,6 +96,11 @@ export const SessionPage: React.FC<{
}
listKey="sessions"
/>,
<DataTableRowHeightSwitch
rowHeight={rowHeight}
setRowHeight={setRowHeight}
key="height"
/>,
]}
/>
<div className="flex flex-wrap gap-2">
@@ -97,36 +121,95 @@ export const SessionPage: React.FC<{
</Badge>
)}
</div>
<div className="mt-5 flex flex-col gap-2 border-t pt-5">
{session.data?.traces.map((trace) => (
<Card
className="border-border-gray-150 group grid gap-3 p-2 shadow-none hover:border-gray-300 md:grid-cols-3"
key={trace.id}
>
<SessionIO traceId={trace.id} />
<div className="-mt-1 p-1 opacity-50 transition-opacity group-hover:opacity-100">
<Link
href={`/project/${projectId}/traces/${trace.id}`}
className="text-xs hover:underline"
>
Trace: {trace.name} ({trace.id})&nbsp;
</Link>
<div className="text-xs text-gray-500">
{trace.timestamp.toLocaleString()}
</div>
<div className="mb-1 mt-2 text-xs text-gray-500">Scores</div>
<div className="flex flex-wrap content-start items-start gap-1">
<GroupedScoreBadges scores={trace.scores} />
</div>
<ManualScoreButton
projectId={projectId}
traceId={trace.id}
scores={trace.scores}
variant="badge"
/>
</div>
</Card>
))}
{session.data && (
<TraceCardList
session={session.data}
projectId={projectId}
rowHeight={rowHeightMapping[rowHeight]}
/>
)}
</div>
);
};
const TraceCardList = ({
session,
projectId,
rowHeight,
}: {
session: RouterOutput["sessions"]["byId"];
projectId: string;
rowHeight: number;
}) => {
const listVirtualizationRef = useRef<HTMLDivElement | null>(null);
const virtualizer = useWindowVirtualizer({
count: session.traces.length,
estimateSize: () => rowHeight,
overscan: 5,
scrollMargin: listVirtualizationRef.current?.offsetTop ?? 0,
gap: 10,
});
useEffect(() => {
// re-measure when rowHeight changes to update the virtualizer
virtualizer.measure();
}, [rowHeight, virtualizer]);
return (
<div className="mt-5 border-t pt-5">
<div
ref={listVirtualizationRef}
style={{
height: `${virtualizer.getTotalSize()}px`,
position: "relative",
}}
>
{virtualizer
.getVirtualItems()
.map((virtualItem) => ({
virtualItem,
trace: session.traces[virtualItem.index],
}))
.map(({ virtualItem, trace }) => (
<Card
className="border-border-gray-150 group grid w-full gap-3 overflow-hidden p-2 shadow-none hover:border-gray-300 md:grid-cols-3"
key={virtualItem.key}
data-index={virtualItem.index}
style={{
position: "absolute",
top: 0,
left: 0,
height: `${rowHeight}px`,
transform: `translateY(${virtualItem.start - virtualizer.options.scrollMargin}px)`,
}}
>
<ScrollArea className="col-span-2 pr-3">
<SessionIO traceId={trace.id} />
</ScrollArea>
<ScrollArea>
<div className="-mt-1 overflow-y-auto p-1 opacity-50 transition-opacity group-hover:opacity-100">
<Link
href={`/project/${projectId}/traces/${trace.id}`}
className="text-xs hover:underline"
>
Trace: {trace.name} ({trace.id})&nbsp;
</Link>
<div className="text-xs text-gray-500">
{trace.timestamp.toLocaleString()}
</div>
<div className="mb-1 mt-2 text-xs text-gray-500">Scores</div>
<div className="flex flex-wrap content-start items-start gap-1">
<GroupedScoreBadges scores={trace.scores} />
</div>
<ManualScoreButton
projectId={projectId}
traceId={trace.id}
scores={trace.scores}
variant="badge"
/>
</div>
</ScrollArea>
</Card>
))}
</div>
</div>
);
@@ -145,8 +228,9 @@ const SessionIO = ({ traceId }: { traceId: string }) => {
refetchOnMount: false, // prevents refetching loops
},
);
return (
<div className="col-span-2 flex flex-col gap-2 p-0">
<div className="flex flex-col gap-2 overflow-x-hidden overflow-y-scroll p-0">
{!trace.data ? (
<JsonSkeleton
className="h-full w-full overflow-hidden px-2 py-1"
+30 -14
View File
@@ -6,6 +6,7 @@ import { useHasAccess } from "@/src/features/rbac/utils/checkAccess";
import { cn } from "@/src/utils/tailwind";
import { type RouterOutput, type RouterInput } from "@/src/utils/types";
import { useState } from "react";
import { usePostHogClientCapture } from "@/src/features/posthog-analytics/usePostHogClientCapture";
export function StarToggle({
value,
@@ -53,7 +54,7 @@ export function StarTraceToggle({
}) {
const utils = api.useUtils();
const hasAccess = useHasAccess({ projectId, scope: "objects:bookmark" });
const capture = usePostHogClientCapture();
const [isLoading, setIsLoading] = useState(false);
const mutBookmarkTrace = api.traces.bookmark.useMutation({
@@ -109,13 +110,18 @@ export function StarTraceToggle({
size={size}
disabled={!hasAccess}
isLoading={isLoading}
onClick={(value) =>
mutBookmarkTrace.mutateAsync({
onClick={(value) => {
capture("table:bookmark_button_click", {
table: "traces",
id: traceId,
value: value,
});
return mutBookmarkTrace.mutateAsync({
projectId,
traceId,
bookmarked: value,
})
}
});
}}
/>
);
}
@@ -133,7 +139,7 @@ export function StarTraceDetailsToggle({
}) {
const utils = api.useUtils();
const hasAccess = useHasAccess({ projectId, scope: "objects:bookmark" });
const capture = usePostHogClientCapture();
const [isLoading, setIsLoading] = useState(false);
const mutBookmarkTrace = api.traces.bookmark.useMutation({
@@ -180,13 +186,17 @@ export function StarTraceDetailsToggle({
size={size}
disabled={!hasAccess}
isLoading={isLoading}
onClick={(value) =>
mutBookmarkTrace.mutateAsync({
onClick={(value) => {
capture("trace_detail:bookmark_button_click", {
id: traceId,
value: value,
});
return mutBookmarkTrace.mutateAsync({
projectId,
traceId,
bookmarked: value,
})
}
});
}}
/>
);
}
@@ -204,6 +214,7 @@ export function StarSessionToggle({
}) {
const utils = api.useUtils();
const hasAccess = useHasAccess({ projectId, scope: "objects:bookmark" });
const capture = usePostHogClientCapture();
const mutBookmarkSession = api.sessions.bookmark.useMutation({
onSuccess: () => {
void utils.sessions.invalidate();
@@ -216,13 +227,18 @@ export function StarSessionToggle({
size={size}
isLoading={mutBookmarkSession.isLoading}
disabled={!hasAccess}
onClick={(value) =>
mutBookmarkSession.mutateAsync({
onClick={(value) => {
capture("table:bookmark_button_click", {
table: "sessions",
id: sessionId,
value: value,
});
return mutBookmarkSession.mutateAsync({
projectId,
sessionId,
bookmarked: value,
})
}
});
}}
/>
);
}
@@ -14,6 +14,7 @@ import {
import { type VisibilityState } from "@tanstack/react-table";
import { ChevronDown, Columns } from "lucide-react";
import { type LangfuseColumnDef } from "@/src/components/table/types";
import { usePostHogClientCapture } from "@/src/features/posthog-analytics/usePostHogClientCapture";
interface DataTableColumnVisibilityFilterProps<TData, TValue> {
columns: LangfuseColumnDef<TData, TValue>[];
@@ -27,14 +28,24 @@ export function DataTableColumnVisibilityFilter<TData, TValue>({
setColumnVisibility,
}: DataTableColumnVisibilityFilterProps<TData, TValue>) {
const [isOpen, setIsOpen] = useState(false);
const capture = usePostHogClientCapture();
const toggleColumn = useCallback(
(columnId: string) => {
setColumnVisibility((old) => ({
...old,
[columnId]: !old[columnId],
}));
setColumnVisibility((old) => {
const newColumnVisibility = {
...old,
[columnId]: !old[columnId],
};
const selectedColumns = Object.keys(newColumnVisibility).filter(
(key) => newColumnVisibility[key],
);
capture("table:column_visibility_changed", {
selectedColumns: selectedColumns,
});
return newColumnVisibility;
});
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[setColumnVisibility],
);
@@ -9,7 +9,6 @@ import {
import { Button } from "@/src/components/ui/button";
import { useHasAccess } from "@/src/features/rbac/utils/checkAccess";
import { api } from "@/src/utils/api";
import { usePostHog } from "posthog-js/react";
import {
Dialog,
@@ -20,6 +19,7 @@ import {
DialogTitle,
} from "@/src/components/ui/dialog";
import { useState } from "react";
import { usePostHogClientCapture } from "@/src/features/posthog-analytics/usePostHogClientCapture";
export function TraceTableMultiSelectAction({
selectedTraceIds,
@@ -32,7 +32,7 @@ export function TraceTableMultiSelectAction({
}) {
const utils = api.useUtils();
const [open, setOpen] = useState(false);
const posthog = usePostHog();
const capture = usePostHogClientCapture();
const hasDeleteAccess = useHasAccess({ projectId, scope: "traces:delete" });
const mutDeleteTraces = api.traces.deleteMany.useMutation({
@@ -55,6 +55,10 @@ export function TraceTableMultiSelectAction({
<DropdownMenuItem
disabled={!hasDeleteAccess}
onClick={() => {
capture("trace:delete_form_open", {
count: selectedTraceIds.length,
source: "table-multi-select",
});
setOpen(true);
}}
>
@@ -94,7 +98,7 @@ export function TraceTableMultiSelectAction({
.then(() => {
setOpen(false);
});
posthog.capture("trace:delete", {
capture("trace:delete_form_submit", {
count: selectedTraceIds.length,
source: "table-multi-select",
});
@@ -14,6 +14,7 @@ import {
SelectTrigger,
SelectValue,
} from "@/src/components/ui/select";
import { usePostHogClientCapture } from "@/src/features/posthog-analytics/usePostHogClientCapture";
interface DataTablePaginationProps<TData> {
table: Table<TData>;
@@ -24,18 +25,27 @@ export function DataTablePagination<TData>({
table,
paginationOptions = [10, 20, 30, 40, 50],
}: DataTablePaginationProps<TData>) {
const capture = usePostHogClientCapture();
return (
<div className="mt-3 flex items-center justify-between overflow-x-auto px-2">
<div className="flex items-center justify-between">
<div className="flex-1 text-sm text-muted-foreground">
{/* {table.getFilteredSelectedRowModel().rows.length} of{" "}
{table.getFilteredRowModel().rows.length} row(s) selected. */}
</div>
<div className="flex items-center space-x-6 lg:space-x-8">
<div className="flex flex-wrap items-center space-x-6 lg:space-x-8">
<div className="flex items-center space-x-2">
<p className="whitespace-nowrap text-sm font-medium">Rows per page</p>
<p className="whitespace-nowrap text-sm font-medium md:hidden">
Rows
</p>
<p className="hidden whitespace-nowrap text-sm font-medium md:block">
Rows per page
</p>
<Select
value={`${table.getState().pagination.pageSize}`}
onValueChange={(value) => {
capture("table:pagination_page_size_select", {
pageSize: value,
});
table.setPageSize(Number(value));
}}
>
@@ -51,7 +61,7 @@ export function DataTablePagination<TData>({
</SelectContent>
</Select>
</div>
<div className="flex w-[100px] items-center justify-center whitespace-nowrap text-sm font-medium">
<div className="flex items-center justify-center whitespace-nowrap text-sm font-medium">
Page {table.getState().pagination.pageIndex + 1} of{" "}
{table.getPageCount()}
</div>
@@ -59,7 +69,12 @@ export function DataTablePagination<TData>({
<Button
variant="outline"
className="hidden h-8 w-8 p-0 lg:flex"
onClick={() => table.setPageIndex(0)}
onClick={() => {
table.setPageIndex(0);
capture("table:pagination_button_click", {
type: "firstPage",
});
}}
disabled={!table.getCanPreviousPage()}
>
<span className="sr-only">Go to first page</span>
@@ -68,7 +83,12 @@ export function DataTablePagination<TData>({
<Button
variant="outline"
className="h-8 w-8 p-0"
onClick={() => table.previousPage()}
onClick={() => {
table.previousPage();
capture("table:pagination_button_click", {
type: "previousPage",
});
}}
disabled={!table.getCanPreviousPage()}
>
<span className="sr-only">Go to previous page</span>
@@ -77,7 +97,12 @@ export function DataTablePagination<TData>({
<Button
variant="outline"
className="h-8 w-8 p-0"
onClick={() => table.nextPage()}
onClick={() => {
table.nextPage();
capture("table:pagination_button_click", {
type: "nextPage",
});
}}
disabled={!table.getCanNextPage()}
>
<span className="sr-only">Go to next page</span>
@@ -86,7 +111,12 @@ export function DataTablePagination<TData>({
<Button
variant="outline"
className="hidden h-8 w-8 p-0 lg:flex"
onClick={() => table.setPageIndex(table.getPageCount() - 1)}
onClick={() => {
table.setPageIndex(table.getPageCount() - 1);
capture("table:pagination_button_click", {
type: "lastPage",
});
}}
disabled={!table.getCanNextPage()}
>
<span className="sr-only">Go to last page</span>
@@ -1,5 +1,6 @@
import { Tabs, TabsList, TabsTrigger } from "@/src/components/ui/tabs";
import useLocalStorage from "@/src/components/useLocalStorage";
import { usePostHogClientCapture } from "@/src/features/posthog-analytics/usePostHogClientCapture";
import {
MdDensityLarge,
MdDensityMedium,
@@ -35,25 +36,33 @@ export const DataTableRowHeightSwitch = ({
}: {
rowHeight: RowHeight;
setRowHeight: (e: RowHeight) => void;
}) => (
<Tabs
//defaultValue={height}
value={rowHeight}
onValueChange={(e) => setRowHeight(e as any)}
key="height"
>
<TabsList className="gap-1 border bg-transparent px-2">
{heightOptions.map(({ id, label, icon }) => (
<TabsTrigger
key={id}
value={id}
className="px-2 shadow-none data-[state=active]:bg-slate-200 data-[state=active]:ring-border"
>
<span role="img" aria-label={`${label} size`}>
{icon}
</span>
</TabsTrigger>
))}
</TabsList>
</Tabs>
);
}) => {
const capture = usePostHogClientCapture();
return (
<Tabs
//defaultValue={height}
value={rowHeight}
onValueChange={(e) => {
capture("table:row_height_switch_select", {
rowHeight: e,
});
setRowHeight(e as any);
}}
key="height"
>
<TabsList className="gap-1 border bg-transparent px-2">
{heightOptions.map(({ id, label, icon }) => (
<TabsTrigger
key={id}
value={id}
className="px-2 shadow-none data-[state=active]:bg-slate-200 data-[state=active]:ring-border"
>
<span role="img" aria-label={`${label} size`}>
{icon}
</span>
</TabsTrigger>
))}
</TabsList>
</Tabs>
);
};
@@ -12,6 +12,7 @@ import {
type RowHeight,
} from "@/src/components/table/data-table-row-height-switch";
import { Search } from "lucide-react";
import { usePostHogClientCapture } from "@/src/features/posthog-analytics/usePostHogClientCapture";
interface SearchConfig {
placeholder: string;
@@ -47,9 +48,10 @@ export function DataTableToolbar<TData, TValue>({
const [searchString, setSearchString] = useState(
searchConfig?.currentQuery ?? "",
);
const capture = usePostHogClientCapture();
return (
<div className="my-2 flex flex-1 flex-wrap items-center gap-2">
<div className="my-2 flex flex-wrap items-center gap-2 @container">
{searchConfig && (
<div className="flex max-w-md items-center">
<Input
@@ -59,14 +61,18 @@ export function DataTableToolbar<TData, TValue>({
onChange={(event) => setSearchString(event.currentTarget.value)}
onKeyDown={(event) => {
if (event.key === "Enter") {
capture("table:search_submit");
searchConfig.updateQuery(searchString);
}
}}
className="h-10 w-[200px] rounded-r-none lg:w-[250] 2xl:w-[350px]"
className="h-10 w-[150px] rounded-r-none @6xl:w-[250px]"
/>
<Button
variant="outline"
onClick={() => searchConfig.updateQuery(searchString)}
onClick={() => {
capture("table:search_submit");
searchConfig.updateQuery(searchString);
}}
className="rounded-l-none border-l-0 p-3"
>
<Search className="h-4 w-4" />
@@ -80,7 +86,7 @@ export function DataTableToolbar<TData, TValue>({
onChange={setFilterState}
/>
)}
<div className="flex flex-row flex-wrap gap-2 lg:ml-auto">
<div className="flex flex-row flex-wrap gap-2 pr-0.5 @6xl:ml-auto">
{!!columnVisibility && !!setColumnVisibility && (
<DataTableColumnVisibilityFilter
columns={columns}
+28 -9
View File
@@ -12,11 +12,13 @@ import {
Table,
TableBody,
TableCell,
TableFooter,
TableHead,
TableHeader,
TableRow,
} from "@/src/components/ui/table";
import { type OrderByState } from "@/src/features/orderBy/types";
import { usePostHogClientCapture } from "@/src/features/posthog-analytics/usePostHogClientCapture";
import { cn } from "@/src/utils/tailwind";
import {
flexRender,
@@ -48,6 +50,7 @@ interface DataTableProps<TData, TValue> {
setOrderBy?: (s: OrderByState) => void;
help?: { description: string; href: string };
rowHeight?: RowHeight;
className?: string;
}
export interface AsyncTableData<T> {
@@ -72,6 +75,7 @@ export function DataTable<TData extends object, TValue>({
}: DataTableProps<TData, TValue>) {
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
const rowheighttw = getRowHeightTailwindClass(rowHeight);
const capture = usePostHogClientCapture();
const table = useReactTable({
data: data.data ?? [],
@@ -102,8 +106,8 @@ export function DataTable<TData extends object, TValue>({
return (
<>
<div className="space-y-4">
<div className="rounded-md border">
<div className="flex w-full max-w-full flex-1 flex-col gap-1 overflow-auto">
<div className="w-full overflow-auto rounded-md border">
<Table>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
@@ -129,14 +133,26 @@ export function DataTable<TData extends object, TValue>({
if (orderBy?.column === columnDef.id) {
if (orderBy.order === "DESC") {
capture("table:column_sorting_header_click", {
column: columnDef.id,
order: "ASC",
});
setOrderBy({
column: columnDef.id,
order: "ASC",
});
} else {
capture("table:column_sorting_header_click", {
column: columnDef.id,
order: "Disabled",
});
setOrderBy(null);
}
} else {
capture("table:column_sorting_header_click", {
column: columnDef.id,
order: "DESC",
});
setOrderBy({
column: columnDef.id,
order: "DESC",
@@ -176,10 +192,10 @@ export function DataTable<TData extends object, TValue>({
</TableHeader>
<TableBody>
{data.isLoading || !data.data ? (
<TableRow>
<TableRow className="h-svh">
<TableCell
colSpan={columns.length}
className="h-24 text-center"
className="content-start border-b text-center"
>
Loading...
</TableCell>
@@ -190,7 +206,7 @@ export function DataTable<TData extends object, TValue>({
{row.getVisibleCells().map((cell) => (
<TableCell
key={cell.id}
className="overflow-hidden whitespace-nowrap px-2 py-1 text-xs first:pl-2"
className="overflow-hidden whitespace-nowrap border-b px-2 py-1 text-xs first:pl-2"
>
<div className={cn("flex items-center", rowheighttw)}>
{flexRender(
@@ -224,12 +240,15 @@ export function DataTable<TData extends object, TValue>({
</TableBody>
</Table>
</div>
<div className="grow"></div>
</div>
{pagination !== undefined ? (
<DataTablePagination
table={table}
paginationOptions={pagination.options}
/>
<div className="sticky bottom-0 z-10 flex w-full justify-end bg-white font-medium">
<DataTablePagination
table={table}
paginationOptions={pagination.options}
/>
</div>
) : null}
</>
);
@@ -13,7 +13,6 @@ import {
} from "@/src/components/ui/dropdown-menu";
import { Button } from "@/src/components/ui/button";
import { ChevronDownIcon, Loader } from "lucide-react";
import { usePostHog } from "posthog-js/react";
import {
NumberParam,
StringParam,
@@ -29,7 +28,12 @@ import {
} from "@/src/utils/dates";
import useColumnVisibility from "@/src/features/column-visibility/hooks/useColumnVisibility";
import { type LangfuseColumnDef } from "@/src/components/table/types";
import { type Prisma, type ObservationLevel } from "@langfuse/shared";
import {
type Prisma,
type ObservationLevel,
type FilterState,
type ObservationOptions,
} from "@langfuse/shared";
import { cn } from "@/src/utils/tailwind";
import { LevelColors } from "@/src/components/level-colors";
import { usdFormatter } from "@/src/utils/numbers";
@@ -43,6 +47,8 @@ import type Decimal from "decimal.js";
import { type ScoreSimplified } from "@/src/server/api/routers/generations/getAllQuery";
import { useRowHeightLocalStorage } from "@/src/components/table/data-table-row-height-switch";
import { IOTableCell } from "@/src/components/ui/CodeJsonViewer";
import { usePostHogClientCapture } from "@/src/features/posthog-analytics/usePostHogClientCapture";
import { useSession } from "next-auth/react";
export type GenerationsTableRow = {
id: string;
@@ -76,10 +82,19 @@ export type GenerationsTableRow = {
export type GenerationsTableProps = {
projectId: string;
promptName?: string;
promptVersion?: number;
omittedFilter?: string[];
};
export default function GenerationsTable({ projectId }: GenerationsTableProps) {
const posthog = usePostHog();
export default function GenerationsTable({
projectId,
promptName,
promptVersion,
omittedFilter = [],
}: GenerationsTableProps) {
const session = useSession();
const capture = usePostHogClientCapture();
const [isExporting, setIsExporting] = useState(false);
const [searchQuery, setSearchQuery] = useQueryParam(
"search",
@@ -96,13 +111,15 @@ export default function GenerationsTable({ projectId }: GenerationsTableProps) {
"s",
);
const [filterState, setFilterState] = useQueryFilterState(
const [inputFilterState, setInputFilterState] = useQueryFilterState(
[
{
column: "Start Time",
type: "datetime",
operator: ">",
value: utcDateOffsetByDays(-14),
value: utcDateOffsetByDays(
session.data?.environment.defaultTableDateTimeOffset ?? -14,
),
},
],
"generations",
@@ -113,6 +130,33 @@ export default function GenerationsTable({ projectId }: GenerationsTableProps) {
order: "DESC",
});
const promptNameFilter: FilterState = promptName
? [
{
column: "Prompt Name",
type: "string",
operator: "=",
value: promptName,
},
]
: [];
const promptVersionFilter: FilterState = promptVersion
? [
{
column: "Prompt Version",
type: "number",
operator: "=",
value: promptVersion,
},
]
: [];
const filterState = inputFilterState.concat([
...promptNameFilter,
...promptVersionFilter,
]);
const generations = api.generations.all.useQuery({
page: paginationState.pageIndex,
limit: paginationState.pageSize,
@@ -137,11 +181,19 @@ export default function GenerationsTable({ projectId }: GenerationsTableProps) {
},
);
const transformFilterOptions = (
filterOptions: ObservationOptions | undefined,
) => {
return observationsTableColsWithOptions(filterOptions).filter(
(col) => !omittedFilter?.includes(col.name),
);
};
const handleExport = async (fileFormat: ExportFileFormats) => {
if (isExporting) return;
setIsExporting(true);
posthog.capture("generations:export", { file_format: fileFormat });
capture("generations:export", { file_format: fileFormat });
if (fileFormat === "OPENAI-JSONL")
alert(
"When exporting in OpenAI-JSONL, only generations that exactly match the `ChatML` format will be exported. For any questions, reach out to support.",
@@ -596,14 +648,12 @@ export default function GenerationsTable({ projectId }: GenerationsTableProps) {
: [];
return (
<div>
<>
<DataTableToolbar
columns={columns}
filterColumnDefinition={observationsTableColsWithOptions(
filterOptions.data,
)}
filterState={filterState}
setFilterState={setFilterState}
filterColumnDefinition={transformFilterOptions(filterOptions.data)}
filterState={inputFilterState}
setFilterState={setInputFilterState}
searchConfig={{
placeholder: "Search by id, name, traceName, model",
updateQuery: setSearchQuery,
@@ -617,9 +667,12 @@ export default function GenerationsTable({ projectId }: GenerationsTableProps) {
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" className="ml-auto whitespace-nowrap">
{filterState.length > 0 || searchQuery
? "Export selection"
: "Export all"}{" "}
<span className="hidden @6xl:inline">
{filterState.length > 0 || searchQuery
? "Export selection"
: "Export all"}{" "}
</span>
<span className="@6xl:hidden">Export</span>
{isExporting ? (
<Loader className="ml-2 h-4 w-4 animate-spin" />
) : (
@@ -669,7 +722,7 @@ export default function GenerationsTable({ projectId }: GenerationsTableProps) {
onColumnVisibilityChange={setColumnVisibilityState}
rowHeight={rowHeight}
/>
</div>
</>
);
}
+41 -36
View File
@@ -2,6 +2,7 @@ import { DataTable } from "@/src/components/table/data-table";
import { type LangfuseColumnDef } from "@/src/components/table/types";
import { Button } from "@/src/components/ui/button";
import useColumnVisibility from "@/src/features/column-visibility/hooks/useColumnVisibility";
import { usePostHogClientCapture } from "@/src/features/posthog-analytics/usePostHogClientCapture";
import { useHasAccess } from "@/src/features/rbac/utils/checkAccess";
import { api } from "@/src/utils/api";
import { usdFormatter } from "@/src/utils/numbers";
@@ -47,7 +48,6 @@ export default function ModelTable({ projectId }: { projectId: string }) {
pageIndex: withDefault(NumberParam, 0),
pageSize: withDefault(NumberParam, 50),
});
const models = api.models.all.useQuery({
page: paginationState.pageIndex,
limit: paginationState.pageSize,
@@ -256,33 +256,31 @@ export default function ModelTable({ projectId }: { projectId: string }) {
};
return (
<div>
<DataTable
columns={columns}
data={
models.isLoading
? { isLoading: true, isError: false }
: models.isError
? {
isLoading: false,
isError: true,
error: models.error.message,
}
: {
isLoading: false,
isError: false,
data: models.data.models.map((t) => convertToTableRow(t)),
}
}
pagination={{
pageCount: Math.ceil(totalCount / paginationState.pageSize),
onChange: setPaginationState,
state: paginationState,
}}
columnVisibility={columnVisibility}
onColumnVisibilityChange={setColumnVisibility}
/>
</div>
<DataTable
columns={columns}
data={
models.isLoading
? { isLoading: true, isError: false }
: models.isError
? {
isLoading: false,
isError: true,
error: models.error.message,
}
: {
isLoading: false,
isError: false,
data: models.data.models.map((t) => convertToTableRow(t)),
}
}
pagination={{
pageCount: Math.ceil(totalCount / paginationState.pageSize),
onChange: setPaginationState,
state: paginationState,
}}
columnVisibility={columnVisibility}
onColumnVisibilityChange={setColumnVisibility}
/>
);
}
@@ -294,6 +292,7 @@ const DeleteModelButton = ({
projectId: string;
}) => {
const utils = api.useUtils();
const capture = usePostHogClientCapture();
const mut = api.models.delete.useMutation({
onSuccess: () => {
void utils.models.invalidate();
@@ -314,14 +313,20 @@ const DeleteModelButton = ({
size="xs"
variant="ghost"
onClick={() => {
mut
.mutateAsync({
projectId,
modelId,
})
.catch((error) => {
console.error(error);
});
const confirmDelete = window.confirm(
"Are you sure you want to delete this model?",
);
if (confirmDelete) {
capture("models:delete_button_click");
mut
.mutateAsync({
projectId,
modelId,
})
.catch((error) => {
console.error(error);
});
}
}}
>
<Trash size={14} />
@@ -255,7 +255,7 @@ export default function ScoresTable({
};
return (
<div>
<>
<DataTableToolbar
columns={columns}
filterColumnDefinition={transformFilterOptions(filterOptions.data)}
@@ -294,6 +294,6 @@ export default function ScoresTable({
onColumnVisibilityChange={setColumnVisibility}
rowHeight={rowHeight}
/>
</div>
</>
);
}
@@ -17,6 +17,7 @@ import { type RouterOutput } from "@/src/utils/types";
import type Decimal from "decimal.js";
import { useEffect } from "react";
import { NumberParam, useQueryParams, withDefault } from "use-query-params";
import { useSession } from "next-auth/react";
export type SessionTableRow = {
id: string;
@@ -45,6 +46,7 @@ export default function SessionsTable({
omittedFilter = [],
}: SessionTableProps) {
const { setDetailPageList } = useDetailPageLists();
const session = useSession();
const [userFilterState, setUserFilterState] = useQueryFilterState(
[
@@ -52,7 +54,9 @@ export default function SessionsTable({
column: "Created At",
type: "datetime",
operator: ">",
value: utcDateOffsetByDays(-14),
value: utcDateOffsetByDays(
session.data?.environment.defaultTableDateTimeOffset ?? -14,
),
},
],
"sessions",
@@ -331,7 +335,7 @@ export default function SessionsTable({
useColumnVisibility<SessionTableRow>("sessionsColumnVisibility", columns);
return (
<div>
<>
<DataTableToolbar
filterColumnDefinition={transformFilterOptions()}
filterState={userFilterState}
@@ -372,6 +376,6 @@ export default function SessionsTable({
href: "https://langfuse.com/docs/tracing-features/sessions",
}}
/>
</div>
</>
);
}
+16 -3
View File
@@ -38,6 +38,7 @@ import {
} from "@langfuse/shared";
import { useRowHeightLocalStorage } from "@/src/components/table/data-table-row-height-switch";
import { IOTableCell } from "@/src/components/ui/CodeJsonViewer";
import { useSession } from "next-auth/react";
export type TracesTableRow = {
bookmarked: boolean;
@@ -47,6 +48,7 @@ export type TracesTableRow = {
userId: string;
metadata?: string;
level: ObservationLevel;
observationCount: number;
latency?: number;
release?: string;
version?: string;
@@ -80,6 +82,7 @@ export default function TracesTable({
omittedFilter = [],
}: TracesTableProps) {
const utils = api.useUtils();
const session = useSession();
const [selectedRows, setSelectedRows] = useState<RowSelectionState>({});
const { setDetailPageList } = useDetailPageLists();
const [searchQuery, setSearchQuery] = useQueryParam(
@@ -92,7 +95,9 @@ export default function TracesTable({
column: "Timestamp",
type: "datetime",
operator: ">",
value: utcDateOffsetByDays(-14),
value: utcDateOffsetByDays(
session.data?.environment.defaultTableDateTimeOffset ?? -14,
),
},
],
"traces",
@@ -173,6 +178,7 @@ export default function TracesTable({
timestamp: trace.timestamp.toLocaleString(),
name: trace.name ?? "",
level: trace.level,
observationCount: trace.observationCount,
metadata: JSON.stringify(trace.metadata),
release: trace.release ?? undefined,
version: trace.version ?? undefined,
@@ -528,6 +534,13 @@ export default function TracesTable({
defaultHidden: true,
enableSorting: true,
},
{
accessorKey: "observationCount",
id: "observationCount",
header: "Observation Count",
enableHiding: true,
defaultHidden: true,
},
{
accessorKey: "version",
id: "version",
@@ -586,7 +599,7 @@ export default function TracesTable({
useColumnVisibility<TracesTableRow>("tracesColumnVisibility", columns);
return (
<div>
<>
<DataTableToolbar
columns={columns}
filterColumnDefinition={transformFilterOptions(traceFilterOptions.data)}
@@ -648,7 +661,7 @@ export default function TracesTable({
onColumnVisibilityChange={setColumnVisibility}
rowHeight={rowHeight}
/>
</div>
</>
);
}
+23 -14
View File
@@ -6,6 +6,7 @@ import { useState } from "react";
import { Button } from "@/src/components/ui/button";
import { Tabs, TabsList, TabsTrigger } from "@/src/components/ui/tabs";
import { Fragment } from "react";
import { usePostHogClientCapture } from "@/src/features/posthog-analytics/usePostHogClientCapture";
export const IOPreview: React.FC<{
input?: unknown;
@@ -14,7 +15,7 @@ export const IOPreview: React.FC<{
hideIfNull?: boolean;
}> = ({ isLoading = false, hideIfNull = false, ...props }) => {
const [currentView, setCurrentView] = useState<"pretty" | "json">("pretty");
const capture = usePostHogClientCapture();
const input = deepParseJson(props.input);
const output = deepParseJson(props.output);
@@ -54,7 +55,9 @@ export const IOPreview: React.FC<{
}
}
}
const outChatMlMessage = ChatMlMessageSchema.safeParse(output);
const outChatMlArray = ChatMlArraySchema.safeParse(
Array.isArray(output) ? output : [output],
);
// Pretty view available
const isPrettyViewAvailable = inChatMlArray.success;
@@ -65,7 +68,10 @@ export const IOPreview: React.FC<{
{isPrettyViewAvailable ? (
<Tabs
value={currentView}
onValueChange={(v) => setCurrentView(v as "pretty" | "json")}
onValueChange={(v) => {
setCurrentView(v as "pretty" | "json"),
capture("trace_detail:io_mode_switch", { view: v });
}}
>
<TabsList>
<TabsTrigger value="pretty">Pretty </TabsTrigger>
@@ -75,17 +81,20 @@ export const IOPreview: React.FC<{
) : null}
{isPrettyViewAvailable && currentView === "pretty" ? (
<OpenAiMessageView
messages={inChatMlArray.data.concat(
outChatMlMessage.success
? {
...outChatMlMessage.data,
role: outChatMlMessage.data.role ?? "assistant",
}
: ChatMlMessageSchema.parse({
role: "assistant",
content: outputClean ? JSON.stringify(outputClean) : null,
}),
)}
messages={[
...inChatMlArray.data,
...(outChatMlArray.success
? outChatMlArray.data.map((m) => ({
...m,
role: m.role ?? "assistant",
}))
: [
ChatMlMessageSchema.parse({
role: "assistant",
content: outputClean ? JSON.stringify(outputClean) : null,
}),
]),
]}
/>
) : null}
{currentView === "json" || !isPrettyViewAvailable ? (
@@ -155,11 +155,11 @@ export const ObservationPreview = (props: {
/>
) : null}
{preloadedObservation.metadata ? (
{observationWithInputAndOutput.data?.metadata ? (
<JSONView
key={preloadedObservation.id + "-metadata"}
key={observationWithInputAndOutput.data.id + "-metadata"}
title="Metadata"
json={preloadedObservation.metadata}
json={observationWithInputAndOutput.data.metadata}
/>
) : null}
+129 -116
View File
@@ -9,6 +9,7 @@ import { formatIntervalSeconds } from "@/src/utils/dates";
import { MinusCircle, MinusIcon, PlusCircleIcon, PlusIcon } from "lucide-react";
import { Toggle } from "@/src/components/ui/toggle";
import { Button } from "@/src/components/ui/button";
import { usePostHogClientCapture } from "@/src/features/posthog-analytics/usePostHogClientCapture";
export const ObservationTree = (props: {
observations: ObservationReturnType[];
@@ -120,126 +121,138 @@ const ObservationTreeNode = (props: {
setCurrentObservationId: (id: string | undefined) => void;
showMetrics?: boolean;
showScores?: boolean;
}) => (
<>
{props.observations
.sort((a, b) => a.startTime.getTime() - b.startTime.getTime())
.map((observation) => {
const collapsed = props.collapsedObservations.includes(observation.id);
return (
<Fragment key={observation.id}>
<div className="flex">
{Array.from({ length: props.indentationLevel }, (_, i) => (
<div className="mx-2 border-r" key={i} />
))}
<div
className={cn(
"group my-0.5 flex flex-1 cursor-pointer flex-col gap-1 rounded-sm p-1",
props.currentObservationId === observation.id
? "bg-gray-100"
: "hover:bg-gray-50",
)}
onClick={() => props.setCurrentObservationId(observation.id)}
>
<div className="flex gap-2">
<ColorCodedObservationType
observationType={observation.type}
/>
<span className="flex-1 break-all text-sm">
{observation.name}
</span>
{observation.children.length === 0 ? null : (
<Toggle
onClick={(ev) => (
ev.stopPropagation(),
props.toggleCollapsedObservation(observation.id)
)}
variant="default"
pressed={collapsed}
size="xs"
className="w-7"
title={
collapsed ? "Expand children" : "Collapse children"
}
>
{collapsed ? (
<PlusIcon className="h-4 w-4" />
) : (
<MinusIcon className="h-4 w-4" />
)}
</Toggle>
}) => {
const capture = usePostHogClientCapture();
return (
<>
{props.observations
.sort((a, b) => a.startTime.getTime() - b.startTime.getTime())
.map((observation) => {
const collapsed = props.collapsedObservations.includes(
observation.id,
);
return (
<Fragment key={observation.id}>
<div className="flex">
{Array.from({ length: props.indentationLevel }, (_, i) => (
<div className="mx-2 border-r" key={i} />
))}
<div
className={cn(
"group my-0.5 flex flex-1 cursor-pointer flex-col gap-1 rounded-sm p-1",
props.currentObservationId === observation.id
? "bg-gray-100"
: "hover:bg-gray-50",
)}
</div>
{props.showMetrics &&
(observation.promptTokens ||
observation.completionTokens ||
observation.totalTokens ||
observation.endTime) && (
<div className="flex gap-2">
{observation.endTime ? (
<span className="text-xs text-gray-500">
{formatIntervalSeconds(
(observation.endTime.getTime() -
observation.startTime.getTime()) /
1000,
)}
</span>
) : null}
{observation.promptTokens ||
observation.completionTokens ||
observation.totalTokens ? (
<span className="text-xs text-gray-500">
{observation.promptTokens} {" "}
{observation.completionTokens} ({" "}
{observation.totalTokens})
</span>
) : null}
</div>
)}
{observation.level !== "DEFAULT" ? (
<div className="flex">
<span
className={cn(
"rounded-sm p-0.5 text-xs",
LevelColors[observation.level].bg,
LevelColors[observation.level].text,
)}
>
{observation.level}
</span>
</div>
) : null}
{props.showScores &&
props.scores.find((s) => s.observationId === observation.id) ? (
<div className="flex flex-wrap gap-1">
<GroupedScoreBadges
scores={props.scores.filter(
(s) => s.observationId === observation.id,
)}
onClick={() => props.setCurrentObservationId(observation.id)}
>
<div className="flex gap-2">
<ColorCodedObservationType
observationType={observation.type}
/>
<span className="flex-1 break-all text-sm">
{observation.name}
</span>
{observation.children.length === 0 ? null : (
<Toggle
onClick={(ev) => (
ev.stopPropagation(),
props.toggleCollapsedObservation(observation.id),
capture(
collapsed
? "trace_detail:observation_tree_expand"
: "trace_detail:observation_tree_collapse",
{ type: "single" },
)
)}
variant="default"
pressed={collapsed}
size="xs"
className="w-7"
title={
collapsed ? "Expand children" : "Collapse children"
}
>
{collapsed ? (
<PlusIcon className="h-4 w-4" />
) : (
<MinusIcon className="h-4 w-4" />
)}
</Toggle>
)}
</div>
) : null}
{props.showMetrics &&
(observation.promptTokens ||
observation.completionTokens ||
observation.totalTokens ||
observation.endTime) && (
<div className="flex gap-2">
{observation.endTime ? (
<span className="text-xs text-gray-500">
{formatIntervalSeconds(
(observation.endTime.getTime() -
observation.startTime.getTime()) /
1000,
)}
</span>
) : null}
{observation.promptTokens ||
observation.completionTokens ||
observation.totalTokens ? (
<span className="text-xs text-gray-500">
{observation.promptTokens} {" "}
{observation.completionTokens} ({" "}
{observation.totalTokens})
</span>
) : null}
</div>
)}
{observation.level !== "DEFAULT" ? (
<div className="flex">
<span
className={cn(
"rounded-sm p-0.5 text-xs",
LevelColors[observation.level].bg,
LevelColors[observation.level].text,
)}
>
{observation.level}
</span>
</div>
) : null}
{props.showScores &&
props.scores.find(
(s) => s.observationId === observation.id,
) ? (
<div className="flex flex-wrap gap-1">
<GroupedScoreBadges
scores={props.scores.filter(
(s) => s.observationId === observation.id,
)}
/>
</div>
) : null}
</div>
</div>
</div>
{!collapsed && (
<ObservationTreeNode
observations={observation.children}
collapsedObservations={props.collapsedObservations}
toggleCollapsedObservation={props.toggleCollapsedObservation}
scores={props.scores}
indentationLevel={props.indentationLevel + 1}
currentObservationId={props.currentObservationId}
setCurrentObservationId={props.setCurrentObservationId}
showMetrics={props.showMetrics}
showScores={props.showScores}
/>
)}
</Fragment>
);
})}
</>
);
{!collapsed && (
<ObservationTreeNode
observations={observation.children}
collapsedObservations={props.collapsedObservations}
toggleCollapsedObservation={props.toggleCollapsedObservation}
scores={props.scores}
indentationLevel={props.indentationLevel + 1}
currentObservationId={props.currentObservationId}
setCurrentObservationId={props.setCurrentObservationId}
showMetrics={props.showMetrics}
showScores={props.showScores}
/>
)}
</Fragment>
);
})}
</>
);
};
const ColorCodedObservationType = (props: {
observationType: $Enums.ObservationType;
+12 -2
View File
@@ -23,6 +23,7 @@ import { usdFormatter } from "@/src/utils/numbers";
import Decimal from "decimal.js";
import { useCallback, useState } from "react";
import { DeleteButton } from "@/src/components/deleteButton";
import { usePostHogClientCapture } from "@/src/features/posthog-analytics/usePostHogClientCapture";
export function Trace(props: {
observations: Array<ObservationReturnType>;
@@ -30,6 +31,7 @@ export function Trace(props: {
scores: Score[];
projectId: string;
}) {
const capture = usePostHogClientCapture();
const [currentObservationId, setCurrentObservationId] = useQueryParam(
"observation",
StringParam,
@@ -77,17 +79,19 @@ export function Trace(props: {
.filter((id) => !excludeParentObservations.has(id)),
);
} while (newExcludeParentObservations.size > 0);
capture("trace_detail:observation_tree_collapse", { type: "all" });
setCollapsedObservations(
props.observations
.map((o) => o.id)
.filter((id) => !excludeParentObservations.has(id)),
);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [props.observations, currentObservationId]);
const expandAll = useCallback(() => {
capture("trace_detail:observation_tree_expand", { type: "all" });
setCollapsedObservations([]);
}, [setCollapsedObservations]);
}, []);
return (
<div className="grid gap-4 md:h-full md:grid-cols-5 lg:grid-cols-6 xl:grid-cols-7">
@@ -115,6 +119,9 @@ export function Trace(props: {
<Toggle
pressed={scoresOnObservationTree}
onPressedChange={(e) => {
capture("trace_detail:observation_tree_toggle_scores", {
show: e,
});
setScoresOnObservationTree(e);
}}
size="sm"
@@ -125,6 +132,9 @@ export function Trace(props: {
<Toggle
pressed={metricsOnObservationTree}
onPressedChange={(e) => {
capture("trace_detail:observation_tree_toggle_metrics", {
show: e,
});
setMetricsOnObservationTree(e);
}}
size="sm"
+58
View File
@@ -0,0 +1,58 @@
"use client"
import * as React from "react"
import * as AccordionPrimitive from "@radix-ui/react-accordion"
import { ChevronDown } from "lucide-react"
import { cn } from "@/src/utils/tailwind"
const Accordion = AccordionPrimitive.Root
const AccordionItem = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
>(({ className, ...props }, ref) => (
<AccordionPrimitive.Item
ref={ref}
className={cn("border-b", className)}
{...props}
/>
))
AccordionItem.displayName = "AccordionItem"
const AccordionTrigger = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<AccordionPrimitive.Header className="flex">
<AccordionPrimitive.Trigger
ref={ref}
className={cn(
"flex flex-1 items-center justify-between py-4 font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180",
className
)}
{...props}
>
{children}
<ChevronDown className="h-4 w-4 shrink-0 transition-transform duration-200" />
</AccordionPrimitive.Trigger>
</AccordionPrimitive.Header>
))
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName
const AccordionContent = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<AccordionPrimitive.Content
ref={ref}
className="overflow-hidden text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
{...props}
>
<div className={cn("pb-4 pt-0", className)}>{children}</div>
</AccordionPrimitive.Content>
))
AccordionContent.displayName = AccordionPrimitive.Content.displayName
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
+12 -2
View File
@@ -18,9 +18,19 @@ const Slider = React.forwardRef<
{...props}
>
<SliderPrimitive.Track className="relative h-2 w-full grow overflow-hidden rounded-full bg-secondary">
<SliderPrimitive.Range className="absolute h-full bg-primary" />
<SliderPrimitive.Range
className={cn(
"absolute h-full",
props.disabled ? "bg-input" : "bg-primary",
)}
/>
</SliderPrimitive.Track>
<SliderPrimitive.Thumb className="block h-5 w-5 rounded-full border-2 border-primary bg-background ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50" />
<SliderPrimitive.Thumb
className={cn(
"block h-5 w-5 rounded-full border-2 bg-background ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
props.disabled ? "border-secondary" : "border-primary",
)}
/>
</SliderPrimitive.Root>
));
Slider.displayName = SliderPrimitive.Root.displayName;
+13 -9
View File
@@ -6,13 +6,14 @@ const Table = React.forwardRef<
HTMLTableElement,
React.HTMLAttributes<HTMLTableElement>
>(({ className, ...props }, ref) => (
<div className="relative w-full overflow-auto">
<table
ref={ref}
className={cn("w-full caption-bottom text-sm", className)}
{...props}
/>
</div>
<table
ref={ref}
className={cn(
"w-full table-auto caption-bottom border-separate border-spacing-0 space-y-4 overflow-auto text-sm",
className,
)}
{...props}
/>
));
Table.displayName = "Table";
@@ -73,7 +74,7 @@ const TableHead = React.forwardRef<
<th
ref={ref}
className={cn(
"h-12 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0",
"sticky top-0 z-10 border-b bg-white px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0",
className,
)}
{...props}
@@ -87,7 +88,10 @@ const TableCell = React.forwardRef<
>(({ className, ...props }, ref) => (
<td
ref={ref}
className={cn("p-4 align-middle [&:has([role=checkbox])]:pr-0", className)}
className={cn(
"whitespace-nowrap p-4 align-middle [&:has([role=checkbox])]:pr-0",
className,
)}
{...props}
/>
));
+1 -1
View File
@@ -1 +1 @@
export const VERSION = "v2.37.3";
export const VERSION = "v2.43.2";
@@ -21,6 +21,7 @@ import {
import { Label } from "@/src/components/ui/label";
import TableLink from "@/src/components/table/table-link";
import EvalLogTable from "@/src/ee/features/evals/components/eval-log";
import { usePostHogClientCapture } from "@/src/features/posthog-analytics/usePostHogClientCapture";
export const EvalConfigDetail = () => {
const router = useRouter();
@@ -122,8 +123,9 @@ export function DeactivateConfig({
isLoading: boolean;
}) {
const utils = api.useUtils();
const hasAccess = useHasAccess({ projectId, scope: "job:CUD" });
const hasAccess = useHasAccess({ projectId, scope: "evalJob:CUD" });
const [isOpen, setIsOpen] = useState(false);
const capture = usePostHogClientCapture();
const mutEvalConfig = api.evals.updateEvalJob.useMutation({
onSuccess: () => {
@@ -141,6 +143,7 @@ export function DeactivateConfig({
evalConfigId: config?.id ?? "",
updatedStatus: "INACTIVE",
});
capture("eval_config:delete");
setIsOpen(false);
};
@@ -1,4 +1,3 @@
import { usePostHog } from "posthog-js/react";
import { useFieldArray, useForm } from "react-hook-form";
import { Input } from "@/src/components/ui/input";
import { Button } from "@/src/components/ui/button";
@@ -42,6 +41,7 @@ import { Card } from "@/src/components/ui/card";
import { JSONView } from "@/src/components/ui/CodeJsonViewer";
import { Label } from "@/src/components/ui/label";
import DocPopup from "@/src/components/layouts/doc-popup";
import { usePostHogClientCapture } from "@/src/features/posthog-analytics/usePostHogClientCapture";
const formSchema = z.object({
scoreName: z.string(),
@@ -113,7 +113,7 @@ export const InnerEvalConfigForm = (props: {
onFormSuccess?: () => void;
}) => {
const [formError, setFormError] = useState<string | null>(null);
const posthog = usePostHog();
const capture = usePostHogClientCapture();
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
@@ -182,7 +182,7 @@ export const InnerEvalConfigForm = (props: {
});
function onSubmit(values: z.infer<typeof formSchema>) {
posthog.capture("evals:new_config_form");
capture("eval_config:new_form_submit");
const validatedFilter = z.array(singleFilter).safeParse(values.filter);
@@ -92,32 +92,30 @@ export default function EvalConfigTable({ projectId }: { projectId: string }) {
};
return (
<div>
<DataTable
columns={columns}
data={
templates.isLoading
? { isLoading: true, isError: false }
: templates.isError
? {
isLoading: false,
isError: true,
error: templates.error.message,
}
: {
isLoading: false,
isError: false,
data: templates.data.configs.map((t) => convertToTableRow(t)),
}
}
pagination={{
pageCount: Math.ceil(totalCount / paginationState.pageSize),
onChange: setPaginationState,
state: paginationState,
}}
columnVisibility={columnVisibility}
onColumnVisibilityChange={setColumnVisibility}
/>
</div>
<DataTable
columns={columns}
data={
templates.isLoading
? { isLoading: true, isError: false }
: templates.isError
? {
isLoading: false,
isError: true,
error: templates.error.message,
}
: {
isLoading: false,
isError: false,
data: templates.data.configs.map((t) => convertToTableRow(t)),
}
}
pagination={{
pageCount: Math.ceil(totalCount / paginationState.pageSize),
onChange: setPaginationState,
state: paginationState,
}}
columnVisibility={columnVisibility}
onColumnVisibilityChange={setColumnVisibility}
/>
);
}
@@ -16,6 +16,7 @@ import { useHasAccess } from "@/src/features/rbac/utils/checkAccess";
import { Button } from "@/src/components/ui/button";
import { Pencil } from "lucide-react";
import { useState } from "react";
import { usePostHogClientCapture } from "@/src/features/posthog-analytics/usePostHogClientCapture";
export const EvalTemplateDetail = () => {
const router = useRouter();
@@ -92,12 +93,14 @@ export function EvalVersionDropdown(props: {
defaultOption?: EvalTemplate;
onSelect?: (template: EvalTemplate) => void;
}) {
const capture = usePostHogClientCapture();
const handleSelect = (value: string) => {
const selectedTemplate = props.options?.find(
(template) => template.id === value,
);
if (selectedTemplate && props.onSelect) {
props.onSelect(selectedTemplate);
capture("eval_templates:view_version");
}
};
@@ -133,9 +136,11 @@ export function UpdateTemplate({
setIsEditing: (isEditing: boolean) => void;
}) {
const hasAccess = useHasAccess({ projectId, scope: "evalTemplate:create" });
const capture = usePostHogClientCapture();
const handlePromptEdit = () => {
setIsEditing(true);
capture("eval_templates:update_form_open");
};
return (
@@ -87,34 +87,30 @@ export default function EvalsTemplateTable({
};
return (
<div>
<DataTable
columns={columns}
data={
templates.isLoading
? { isLoading: true, isError: false }
: templates.isError
? {
isLoading: false,
isError: true,
error: templates.error.message,
}
: {
isLoading: false,
isError: false,
data: templates.data.templates.map((t) =>
convertToTableRow(t),
),
}
}
pagination={{
pageCount: Math.ceil(totalCount / paginationState.pageSize),
onChange: setPaginationState,
state: paginationState,
}}
columnVisibility={columnVisibility}
onColumnVisibilityChange={setColumnVisibility}
/>
</div>
<DataTable
columns={columns}
data={
templates.isLoading
? { isLoading: true, isError: false }
: templates.isError
? {
isLoading: false,
isError: true,
error: templates.error.message,
}
: {
isLoading: false,
isError: false,
data: templates.data.templates.map((t) => convertToTableRow(t)),
}
}
pagination={{
pageCount: Math.ceil(totalCount / paginationState.pageSize),
onChange: setPaginationState,
state: paginationState,
}}
columnVisibility={columnVisibility}
onColumnVisibilityChange={setColumnVisibility}
/>
);
}
@@ -1,5 +1,4 @@
import { usePostHog } from "posthog-js/react";
import { useEffect, useState } from "react";
import { useEffect, useState, useMemo } from "react";
import { useForm } from "react-hook-form";
import * as z from "zod";
import { Input } from "@/src/components/ui/input";
@@ -31,6 +30,7 @@ import {
ModelProvider,
type OpenAIModel,
type OpenAIModelParams,
type ModelParams,
} from "@langfuse/shared";
import { PromptDescription } from "@/src/features/prompts/components/prompt-description";
import Link from "next/dist/client/link";
@@ -45,6 +45,8 @@ import {
import { TEMPLATES } from "@/src/ee/features/evals/components/templates";
import { Label } from "@/src/components/ui/label";
import { useHasAccess } from "@/src/features/rbac/utils/checkAccess";
import { usePostHogClientCapture } from "@/src/features/posthog-analytics/usePostHogClientCapture";
import { getFinalModelParams } from "@/src/ee/utils/getFinalModelParams";
export const EvalTemplateForm = (props: {
projectId: string;
@@ -186,29 +188,28 @@ export const InnerEvalTemplateForm = (props: {
isEditing?: boolean;
setIsEditing?: (isEditing: boolean) => void;
}) => {
const posthog = usePostHog();
const capture = usePostHogClientCapture();
const [formError, setFormError] = useState<string | null>(null);
const preFilledModel = useMemo(
() => getModelParamsWithEnabledFlag(props.preFilledFormValues),
[props.preFilledFormValues],
);
// updates the model params based on the pre-filled data
// either form update or from langfuse-generated template
const [modelParams, setModelParams] = useState<UIModelParams>({
model: props.preFilledFormValues?.model ?? "gpt-3.5-turbo",
provider:
props.preFilledFormValues?.modelParams.provider ?? ModelProvider.OpenAI,
max_tokens: props.preFilledFormValues?.modelParams.max_tokens ?? 100,
maxTemperature:
props.preFilledFormValues?.modelParams.provider === ModelProvider.OpenAI
? 2
: 1,
top_p: props.preFilledFormValues?.modelParams.top_p ?? 1,
temperature: props.preFilledFormValues?.modelParams.temperature ?? 1,
});
const updateModelParam: ModelParamsContext["updateModelParam"] = (
const [modelParams, setModelParams] = useState<UIModelParams>(preFilledModel);
const updateModelParamValue: ModelParamsContext["updateModelParamValue"] = (
key,
value,
) => {
setModelParams((prev) => ({ ...prev, [key]: value }));
setModelParams((prev) => ({ ...prev, [key]: { ...prev[key], value } }));
};
const setModelParamEnabled: ModelParamsContext["setModelParamEnabled"] = (
key,
enabled,
) => {
setModelParams((prev) => ({ ...prev, [key]: { ...prev[key], enabled } }));
};
// updates the form based on the pre-filled data
@@ -249,26 +250,30 @@ export const InnerEvalTemplateForm = (props: {
// state for the model params is outside of the form, hence needs to be handled individually
// also set the context for the playground
const model = EvalModelNames.parse(props.preFilledFormValues.model);
updateModelParam("model", model);
const model = EvalModelNames.parse(preFilledModel.model.value);
updateModelParamValue("model", model);
setModelParams((prev) => ({
...prev,
...(props.preFilledFormValues?.modelParams as UIModelParams),
...preFilledModel,
}));
const modelProvider = evalLLMModels.find(
(m) => m.model === model,
)?.provider;
const modelProvider = evalLLMModels.find((m) => m.model.value === model)
?.provider.value;
if (modelProvider) {
updateModelParam("provider", modelProvider); // updating the provider based on the model
updateModelParam(
updateModelParamValue("provider", modelProvider); // updating the provider based on the model
updateModelParamValue(
"maxTemperature",
modelProvider === ModelProvider.OpenAI ? 2 : 1,
); // setting the max value of the slider based on the provider
}
}
}, [props.preFilledFormValues, form, props.existingEvalTemplateName]);
}, [
props.preFilledFormValues,
preFilledModel,
form,
props.existingEvalTemplateName,
]);
const extractedVariables = form.watch("prompt")
? extractVariables(form.watch("prompt")).filter(getIsCharOrUnderscore)
@@ -281,9 +286,13 @@ export const InnerEvalTemplateForm = (props: {
});
function onSubmit(values: z.infer<typeof formSchema>) {
posthog.capture("evals:new_template_form");
capture(
props.isEditing
? "eval_templates:update_form_submit"
: "eval_templates:new_form_submit",
);
const model = EvalModelNames.safeParse(modelParams.model);
const model = EvalModelNames.safeParse(modelParams.model.value);
if (!model.success) {
setFormError("Please select a model.");
@@ -296,7 +305,7 @@ export const InnerEvalTemplateForm = (props: {
projectId: props.projectId,
prompt: values.prompt,
model: model.data,
modelParams: modelParams,
modelParams: getFinalModelParams(modelParams),
vars: extractedVariables ?? [],
outputSchema: {
score: values.outputScore,
@@ -422,9 +431,13 @@ export const InnerEvalTemplateForm = (props: {
<div className="col-span-1 row-span-3">
<div className="flex flex-col gap-6">
<ModelParameters
{...{ modelParams, updateModelParam }}
{...{
modelParams,
updateModelParamValue: updateModelParamValue,
setModelParamEnabled,
}}
availableModels={[...evalLLMModels]}
disabled={!props.isEditing}
formDisabled={!props.isEditing}
/>
<LLMApiKeyComponent
projectId={props.projectId}
@@ -486,7 +499,7 @@ export const LLMApiKeyComponent = (p: {
}
const getModelProvider = (model: string) => {
return evalLLMModels.find((m) => m.model === model)?.provider;
return evalLLMModels.find((m) => m.model.value === model)?.provider.value;
};
const getApiKeyForModel = (model: string) => {
@@ -498,14 +511,14 @@ export const LLMApiKeyComponent = (p: {
<div>
<Label>API key</Label>
<div>
{getApiKeyForModel(p.modelParams.model) ? (
{getApiKeyForModel(p.modelParams.model.value) ? (
<span className="mr-2 rounded-sm bg-gray-200 p-1 text-xs">
{getApiKeyForModel(p.modelParams.model)?.displaySecretKey}
{getApiKeyForModel(p.modelParams.model.value)?.displaySecretKey}
</span>
) : undefined}
</div>
{/* Custom form message to include a link to the already existing prompt */}
{!getApiKeyForModel(p.modelParams.model) ? (
{!getApiKeyForModel(p.modelParams.model.value) ? (
<div className="flex flex-col text-sm font-medium text-destructive">
{"No LLM API key found."}
@@ -523,3 +536,33 @@ export const LLMApiKeyComponent = (p: {
</div>
);
};
function getModelParamsWithEnabledFlag(
evalPreFill?: EvalTemplateFormPreFill,
): UIModelParams {
const defaultModelParams: ModelParams & { maxTemperature: number } = {
model: evalPreFill?.model ?? "gpt-3.5-turbo",
provider: ModelProvider.OpenAI,
max_tokens: 100,
maxTemperature:
evalPreFill?.modelParams?.provider === ModelProvider.OpenAI ? 2 : 1,
top_p: 1,
temperature: 1,
};
return Object.entries({
...defaultModelParams,
...evalPreFill?.modelParams,
}).reduce(
(params, [key, value]) => ({
...params,
[key]: {
enabled: Boolean(
!evalPreFill || evalPreFill.modelParams[key as keyof UIModelParams],
),
value,
},
}),
{} as UIModelParams,
);
}
+14 -7
View File
@@ -5,26 +5,33 @@ import Link from "next/link";
import { useHasAccess } from "@/src/features/rbac/utils/checkAccess";
import { Lock } from "lucide-react";
import EvalConfigTable from "@/src/ee/features/evals/components/eval-config-table";
import { usePostHogClientCapture } from "@/src/features/posthog-analytics/usePostHogClientCapture";
import { FullScreenPage } from "@/src/components/layouts/full-screen-page";
export default function ConfigsPage() {
const router = useRouter();
const projectId = router.query.projectId as string;
const capture = usePostHogClientCapture();
const hasWriteAccess = useHasAccess({
projectId,
scope: "job:CUD",
scope: "evalJob:CUD",
});
return (
<div>
<FullScreenPage>
<Header
title="Eval configs"
help={{
description: "XXX",
href: "https://langfuse.com/docs/evals",
description:
"Eval configs let you define how your evaluations templates are applied to incoming traces in Langfuse.",
href: "https://langfuse.com/docs/scores/model-based-evals",
}}
actionButtons={
<Button disabled={!hasWriteAccess} asChild>
<Button
disabled={!hasWriteAccess}
onClick={() => capture("eval_config:new_form_open")}
asChild
>
<Link
href={
hasWriteAccess ? `/project/${projectId}/evals/configs/new` : "#"
@@ -37,6 +44,6 @@ export default function ConfigsPage() {
}
/>
<EvalConfigTable projectId={projectId} />
</div>
</FullScreenPage>
);
}
@@ -16,7 +16,14 @@ export default function NewConfigsPage() {
return (
<div>
<Header title="Create eval config" />
<Header
title="Create eval config"
help={{
description:
"Eval configs let you define how your evaluations templates are applied to incoming traces in Langfuse.",
href: "https://langfuse.com/docs/scores/model-based-evals",
}}
/>
<EvalConfigForm
projectId={projectId}
evalTemplates={evalTemplates.data?.templates ?? []}
@@ -16,7 +16,14 @@ export default function NewTemplatesPage() {
return (
<div className="md:container">
<Header title="Create eval template" />
<Header
title="Create eval template"
help={{
description:
"Create an evaluation template. Choose from one of the pre-defined templates or create your own.",
href: "https://langfuse.com/docs/scores/model-based-evals",
}}
/>
<EvalTemplateForm projectId={projectId} isEditing={true} />
</div>
);
+11 -5
View File
@@ -5,26 +5,32 @@ import Link from "next/link";
import { useHasAccess } from "@/src/features/rbac/utils/checkAccess";
import { Lock } from "lucide-react";
import EvalsTemplateTable from "@/src/ee/features/evals/components/eval-templates-table";
import { usePostHogClientCapture } from "@/src/features/posthog-analytics/usePostHogClientCapture";
export default function TemplatesPage() {
const router = useRouter();
const projectId = router.query.projectId as string;
const capture = usePostHogClientCapture();
const hasWriteAccess = useHasAccess({
projectId,
scope: "evalTemplate:create",
});
return (
<div>
<div className="flex h-[calc(100vh-6rem)] flex-col overflow-hidden md:h-[calc(100vh-2rem)]">
<Header
title="Eval Templates"
help={{
description: "XXX",
href: "https://langfuse.com/docs/evals",
description:
"Create an evaluation template. Choose from one of the pre-defined templates or create your own.",
href: "https://langfuse.com/docs/scores/model-based-evals",
}}
actionButtons={
<Button disabled={!hasWriteAccess} asChild>
<Button
disabled={!hasWriteAccess}
onClick={() => capture("eval_templates:new_form_open")}
asChild
>
<Link
href={
hasWriteAccess
+5 -5
View File
@@ -45,7 +45,7 @@ export const evalRouter = createTRPCRouter({
throwIfNoAccess({
session: ctx.session,
projectId: input.projectId,
scope: "job:read",
scope: "evalJob:read",
});
const configs = await ctx.prisma.jobConfiguration.findMany({
@@ -86,7 +86,7 @@ export const evalRouter = createTRPCRouter({
throwIfNoAccess({
session: ctx.session,
projectId: input.projectId,
scope: "job:read",
scope: "evalJob:read",
});
const config = await ctx.prisma.jobConfiguration.findUnique({
@@ -260,7 +260,7 @@ export const evalRouter = createTRPCRouter({
throwIfNoAccess({
session: ctx.session,
projectId: input.projectId,
scope: "job:CUD",
scope: "evalJob:CUD",
});
const evalTemplate = await ctx.prisma.evalTemplate.findUnique({
@@ -359,7 +359,7 @@ export const evalRouter = createTRPCRouter({
throwIfNoAccess({
session: ctx.session,
projectId: input.projectId,
scope: "job:CUD",
scope: "evalJob:CUD",
});
await ctx.prisma.jobConfiguration.update({
@@ -396,7 +396,7 @@ export const evalRouter = createTRPCRouter({
throwIfNoAccess({
session: ctx.session,
projectId: input.projectId,
scope: "job:read",
scope: "evalJob:read",
});
const jobExecutions = await ctx.prisma.jobExecution.findMany({
@@ -1,10 +1,11 @@
import { type ChangeEvent } from "react";
import { usePlaygroundContext } from "../context";
import { Input } from "@/src/components/ui/input";
import { CheckCircle2, Circle, Trash2Icon } from "lucide-react";
import { Button } from "@/src/components/ui/button";
import { Textarea } from "@/src/components/ui/textarea";
import { type PromptVariable } from "@langfuse/shared";
import { usePlaygroundContext } from "../context";
export const PromptVariableComponent: React.FC<{
promptVariable: PromptVariable;
}> = ({ promptVariable }) => {
@@ -12,7 +13,7 @@ export const PromptVariableComponent: React.FC<{
usePlaygroundContext();
const { name, value, isUsed } = promptVariable;
const handleInputChange = (event: ChangeEvent<HTMLInputElement>) => {
const handleInputChange = (event: ChangeEvent<HTMLTextAreaElement>) => {
updatePromptVariableValue(name, event.target.value);
};
const handleDeleteVariable = () => {
@@ -29,22 +30,31 @@ export const PromptVariableComponent: React.FC<{
: "Variable is not in use";
return (
<div className="my-2 flex flex-row items-center justify-center space-x-2 text-xs">
<p title={isUsedTooltip}>{isUsedIcon}</p>
<p className="min-w-[90px] font-mono" title={name}>
{displayName}
</p>
<Input value={value} onChange={handleInputChange} placeholder={name} />
<Button
variant="ghost"
size="icon"
title="Delete variable"
disabled={isUsed}
onClick={handleDeleteVariable}
className="p-0"
>
{!isUsed && <Trash2Icon size={16} />}
</Button>
<div className="p-1">
<div className="mb-1 flex flex-row items-center">
<span className="flex flex-1 flex-row space-x-2 text-xs">
<p title={isUsedTooltip}>{isUsedIcon}</p>
<p className="min-w-[90px] font-mono" title={name}>
{displayName}
</p>
</span>
<Button
variant="ghost"
size="icon"
title="Delete variable"
disabled={isUsed}
onClick={handleDeleteVariable}
className="p-0"
>
{!isUsed && <Trash2Icon size={16} />}
</Button>
</div>
<Textarea
className="max-h-[10rem] min-h-[3rem] w-full resize-y pt-3 font-mono text-xs focus:outline-none"
value={value}
onChange={handleInputChange}
placeholder={name}
/>
</div>
);
};
@@ -1,10 +1,12 @@
import { Divider } from "@tremor/react";
import { usePlaygroundContext } from "../context";
import { PromptVariableComponent } from "./PromptVariableComponent";
export const Variables = () => {
const { promptVariables } = usePlaygroundContext();
const renderNoVariables = () => (
<div className="text-xs">
<div className="mt-4 text-xs">
<p className="mb-2">No variables defined.</p>
<p>
Use handlebars in your prompts to add a variable:
@@ -21,19 +23,24 @@ export const Variables = () => {
return a.name.localeCompare(b.name);
})
.map((promptVariable) => {
.map((promptVariable, index) => {
return (
<PromptVariableComponent
promptVariable={promptVariable}
key={promptVariable.name}
/>
<>
<PromptVariableComponent
promptVariable={promptVariable}
key={promptVariable.name}
/>
{index !== promptVariables.length - 1 ? (
<Divider className="my-2 text-gray-400" />
) : null}
</>
);
})}
</div>
);
return (
<div className="flex h-full flex-col space-y-4">
<div className="flex h-full flex-col">
<p className="font-semibold">Variables</p>
{promptVariables.length === 0 ? renderNoVariables() : renderVariables()}
</div>
@@ -12,6 +12,8 @@ import { v4 as uuidv4 } from "uuid";
import { createEmptyMessage } from "@/src/components/ChatMessages/utils/createEmptyMessage";
import useCommandEnter from "@/src/ee/features/playground/page/hooks/useCommandEnter";
import { getFinalModelParams } from "@/src/ee/utils/getFinalModelParams";
import { usePostHogClientCapture } from "@/src/features/posthog-analytics/usePostHogClientCapture";
import { ChatMessageListSchema } from "@/src/features/prompts/components/NewPromptForm/validation";
import { PromptType } from "@/src/features/prompts/server/utils/validation";
import useProjectIdFromURL from "@/src/hooks/useProjectIdFromURL";
@@ -60,6 +62,7 @@ export const PlaygroundProvider: React.FC<PropsWithChildren> = ({
children,
}) => {
const projectId = useProjectIdFromURL();
const capture = usePostHogClientCapture();
const [initialPromptId] = useQueryParam("promptId", StringParam);
const [promptVariables, setPromptVariables] = useState<PromptVariable[]>([]);
const [output, setOutput] = useState("");
@@ -104,8 +107,8 @@ export const PlaygroundProvider: React.FC<PropsWithChildren> = ({
}, [initialPrompt]);
useEffect(() => {
setModelParams(getDefaultModelParams(modelParams.provider));
}, [modelParams.provider]);
setModelParams(getDefaultModelParams(modelParams.provider.value));
}, [modelParams.provider.value]);
const updatePromptVariables = useCallback(() => {
const messageContents = messages.map((m) => m.content).join("\n");
@@ -188,6 +191,12 @@ export const PlaygroundProvider: React.FC<PropsWithChildren> = ({
setOutput(response);
}
setOutputJson(getOutputJson(response, finalMessages, modelParams));
capture("playground:execute_button_click", {
inputLength: finalMessages.length,
modelName: modelParams.model,
modelProvider: modelParams.provider,
outputLength: response.length,
});
} catch (err) {
console.error(err);
@@ -196,15 +205,27 @@ export const PlaygroundProvider: React.FC<PropsWithChildren> = ({
} finally {
setIsStreaming(false);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [messages, modelParams, promptVariables]);
useCommandEnter(!isStreaming, handleSubmit);
const updateModelParam: PlaygroundContextType["updateModelParam"] = (
const updateModelParamValue: PlaygroundContextType["updateModelParamValue"] =
(key, value) => {
setModelParams((prev) => ({
...prev,
[key]: { ...prev[key], value },
}));
};
const setModelParamEnabled: PlaygroundContextType["setModelParamEnabled"] = (
key,
value,
enabled,
) => {
setModelParams((prev) => ({ ...prev, [key]: value }));
setModelParams((prev) => ({
...prev,
[key]: { ...prev[key], enabled },
}));
};
const updatePromptVariableValue = (variable: string, value: string) => {
@@ -230,7 +251,8 @@ export const PlaygroundProvider: React.FC<PropsWithChildren> = ({
deleteMessage,
modelParams,
updateModelParam,
updateModelParamValue,
setModelParamEnabled,
output,
outputJson,
@@ -248,7 +270,10 @@ async function* getChatCompletionStream(
messages: ChatMessageWithId[],
modelParams: UIModelParams,
) {
const body = JSON.stringify({ messages, modelParams });
const body = JSON.stringify({
messages,
modelParams: getFinalModelParams(modelParams),
});
const result = await fetch("/api/chatCompletion", {
method: "POST",
headers: { "Content-Type": "application/json" },
@@ -316,23 +341,29 @@ function getDefaultModelParams(provider: ModelProvider): UIModelParams {
// Docs: https://platform.openai.com/docs/api-reference/chat/create
case ModelProvider.OpenAI:
return {
provider,
model: "gpt-3.5-turbo",
temperature: 1,
maxTemperature: 2,
max_tokens: 256,
top_p: 1,
provider: {
value: provider,
enabled: true,
},
model: { value: "gpt-3.5-turbo", enabled: true },
temperature: { value: 1, enabled: true },
maxTemperature: { value: 2, enabled: true },
max_tokens: { value: 256, enabled: true },
top_p: { value: 1, enabled: true },
};
// Docs: https://docs.anthropic.com/claude/reference/messages_post
case ModelProvider.Anthropic:
return {
provider,
model: "claude-3-opus-20240229",
temperature: 0,
maxTemperature: 1,
max_tokens: 256,
top_p: 1,
provider: {
value: provider,
enabled: true,
},
model: { value: "claude-3-opus-20240229", enabled: true },
temperature: { value: 0, enabled: true },
maxTemperature: { value: 1, enabled: true },
max_tokens: { value: 256, enabled: true },
top_p: { value: 1, enabled: true },
};
}
}
@@ -344,9 +375,9 @@ function getOutputJson(
) {
return JSON.stringify(
{
output,
input: messages.map((obj) => filterKeyFromObject(obj, "id")),
model: filterKeyFromObject(modelParams, "maxTemperature"),
output,
model: getFinalModelParams(modelParams),
},
null,
2,
@@ -1,5 +1,5 @@
import type { ValidatedChatCompletionBody } from "@/src/ee/features/playground/server/validateChatCompletionBody";
import { ServerPosthog } from "@/src/server/services/posthog";
import { ServerPosthog } from "@/src/features/posthog-analytics/ServerPosthog";
import type { LLMResult } from "@langchain/core/outputs";
import { BaseCallbackHandler } from "@langchain/core/callbacks/base";
+10
View File
@@ -0,0 +1,10 @@
import { type ModelParams, type UIModelParams } from "@langfuse/shared";
export function getFinalModelParams(modelParams: UIModelParams): ModelParams {
return Object.entries(modelParams)
.filter(([key, value]) => value.enabled && key !== "maxTemperature")
.reduce(
(params, [key, value]) => ({ ...params, [key]: value.value }),
{} as ModelParams,
);
}
+12
View File
@@ -30,6 +30,14 @@ export const env = createEnv({
LANGFUSE_NEW_USER_SIGNUP_WEBHOOK: z.string().url().optional(),
// Add `.min(1) on ID and SECRET if you want to make sure they're not empty
LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES: z.enum(["true", "false"]).optional(),
LANGFUSE_DEFAULT_TABLE_DATETIME_OFFSET: z.string().optional().refine((v) =>
v === undefined || !isNaN(Number.parseInt(v))
, {
message: "LANGFUSE_DEFAULT_TABLE_DATETIME_OFFSET must be a number when set"
}).transform((v) =>
v === undefined ? undefined : -Number.parseInt(v) // negative offset
),
LANGFUSE_DISABLE_EXPENSIVE_POSTGRES_QUERIES: z.enum(["true", "false"]).optional().default("false"),
SALT: z.string({
required_error:
"A strong Salt is required to encrypt API keys securely. See: https://langfuse.com/docs/deployment/self-host#deploy-the-container",
@@ -121,6 +129,10 @@ export const env = createEnv({
NEXT_PUBLIC_SIGN_UP_DISABLED: process.env.NEXT_PUBLIC_SIGN_UP_DISABLED,
LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES:
process.env.LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES,
LANGFUSE_DEFAULT_TABLE_DATETIME_OFFSET:
process.env.LANGFUSE_DEFAULT_TABLE_DATETIME_OFFSET,
LANGFUSE_DISABLE_EXPENSIVE_POSTGRES_QUERIES:
process.env.LANGFUSE_DISABLE_EXPENSIVE_POSTGRES_QUERIES,
LANGFUSE_TEAM_SLACK_WEBHOOK: process.env.LANGFUSE_TEAM_SLACK_WEBHOOK,
LANGFUSE_TEAM_BETTERSTACK_TOKEN:
process.env.LANGFUSE_TEAM_BETTERSTACK_TOKEN,
+3 -3
View File
@@ -1,5 +1,5 @@
import { prisma as _prisma } from "@langfuse/shared/src/db";
import { type MembershipRole } from "@langfuse/shared";
import { type ProjectRole } from "@langfuse/shared";
export type AuditableResource =
| "membership"
@@ -29,14 +29,14 @@ type AuditLog = {
| {
projectId: string;
userId: string;
userProjectRole: MembershipRole;
userProjectRole: ProjectRole;
}
| {
session: {
user: {
id: string;
};
projectRole: MembershipRole;
projectRole: ProjectRole;
projectId: string;
};
}
@@ -2,15 +2,15 @@ import { env } from "@/src/env.mjs";
export const CloudPrivacyNotice = ({ action }: { action: string }) =>
env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION !== undefined ? (
<div className="mt-5 text-center text-xs text-gray-500">
<div className="mx-auto mt-10 max-w-lg text-center text-xs text-gray-500">
By {action} you are agreeing to our{" "}
<a
href="https://langfuse.com/tos"
href="https://langfuse.com/terms"
target="_blank"
rel="noopener noreferrer"
className="italic"
>
Terms of Service
Terms and Conditions
</a>
,{" "}
<a
@@ -1,13 +1,25 @@
import { env } from "@/src/env.mjs";
import { Tabs, TabsList, TabsTrigger } from "@/src/components/ui/tabs";
import { Divider } from "@tremor/react";
import { usePostHog } from "posthog-js/react";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/src/components/ui/select";
import { usePostHogClientCapture } from "@/src/features/posthog-analytics/usePostHogClientCapture";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/src/components/ui/dialog";
const regions =
env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION === "STAGING"
? [
{
name: "EU (Staging)",
name: "STAGING",
hostname: "staging.langfuse.com",
flag: "🇪🇺",
},
@@ -16,6 +28,7 @@ const regions =
? [
{
name: "DEV",
hostname: null,
flag: "🚧",
},
]
@@ -37,54 +50,107 @@ export function CloudRegionSwitch({
}: {
isSignUpPage?: boolean;
}) {
const posthog = usePostHog();
const capture = usePostHogClientCapture();
if (env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION === undefined) return null;
const currentRegion = regions.find(
(region) => region.name === env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION,
);
return (
<div>
<div className="flex w-full flex-col justify-between gap-3 md:flex-row md:items-center">
<div className="mb-6 flex w-full flex-col gap-3">
<div>
<span className="text-sm font-medium leading-none">Data Region</span>
<p className="text-xs text-gray-500">
Regions are strictly separated.
</p>
{isSignUpPage ? (
<span className="text-sm font-medium leading-none">
Data Region
<DataRegionInfo />
</span>
{isSignUpPage && env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION === "US" ? (
<p className="text-xs text-gray-500">
{env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION === "EU"
? "✅ Demo project available"
: "❌ Choose EU for demo project access"}
Demo project is only available in the EU region.
</p>
) : null}
</div>
<Tabs value={env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION}>
<TabsList>
<Select
value={currentRegion?.name}
onValueChange={(value) => {
const region = regions.find((region) => region.name === value);
if (!region) return;
capture(
"sign_in:cloud_region_switch",
{
region: region.name,
},
{
send_instantly: true,
},
);
if (region.hostname) {
window.location.hostname = region.hostname;
}
}}
>
<SelectTrigger className="w-full">
<SelectValue />
</SelectTrigger>
<SelectContent>
{regions.map((region) => (
<TabsTrigger
key={region.name}
value={region.name}
onClick={() => {
posthog.capture(
"cloud_region_switch",
{
region: region.name,
},
{
send_instantly: true,
},
);
if ("hostname" in region)
window.location.hostname = region.hostname;
}}
>
<SelectItem key={region.name} value={region.name}>
<span className="mr-2 text-xl leading-none">{region.flag}</span>
{region.name}
</TabsTrigger>
</SelectItem>
))}
</TabsList>
</Tabs>
</SelectContent>
</Select>
</div>
<Divider className="text-gray-400" />
</div>
);
}
const DataRegionInfo = () => (
<Dialog>
<DialogTrigger asChild>
<a
href="#"
className="ml-1 text-xs text-indigo-600 hover:text-indigo-500"
title="What is this?"
>
(what is this?)
</a>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Data Regions</DialogTitle>
</DialogHeader>
<div className="flex flex-col gap-2">
<p>Langfuse Cloud is available in two data regions:</p>
<ul className="list-disc pl-5">
<li>US: Northern California (us-west-1)</li>
<li>EU: Frankfurt, Germany (eu-central-1)</li>
</ul>
<p>
Regions are strictly separated, and no data is shared across regions.
Choosing a region close to you can help improve speed and comply with
local data residency laws and privacy regulations.
</p>
<p>
You can have accounts in both regions and data migrations are
available on Team plans.
</p>
<p>
For more information, visit{" "}
<a
href="https://langfuse.com/docs/data-security-privacy"
target="_blank"
rel="noopener noreferrer"
className="text-blue-500 underline"
>
langfuse.com/security
</a>
.
</p>
</div>
</DialogContent>
</Dialog>
);
@@ -17,7 +17,7 @@ export async function createProjectMembershipsOnSignup(user: {
)?.id
: undefined;
if (demoProjectId !== undefined) {
await prisma.membership.create({
await prisma.projectMembership.create({
data: {
projectId: demoProjectId,
userId: user.id,
@@ -37,7 +37,7 @@ export async function createProjectMembershipsOnSignup(user: {
)?.id
: undefined;
if (defaultProjectID !== undefined) {
await prisma.membership.create({
await prisma.projectMembership.create({
data: {
projectId: defaultProjectID,
userId: user.id,
@@ -69,7 +69,7 @@ async function processMembershipInvitations(email: string, userId: string) {
});
await prisma.$transaction([
prisma.membership.createMany({
prisma.projectMembership.createMany({
data: membershipsData,
}),
prisma.membershipInvitation.deleteMany({

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