Compare commits

...
246 Commits
Author SHA1 Message Date
Max Deichmann 87611fc4f8 chore: release v1.27.1
CI/CD / push-docker-image (push) Blocked by required conditions
CI/CD / tests (20) (push) Failing after 1m58s
release.yml / release (push) Failing after 3s
CI/CD / tests (18) (push) Failing after 2m42s
CI/CD / e2e-tests (push) Failing after 2m9s
CI/CD / lint (push) Successful in 5m23s
CI/CD / all-ci-passed (push) Waiting to run
2024-01-03 15:48:38 +01:00
3886821091 fix(api): pricing for gpt-3.5-turbo-1106 and gpt-3.5-turbo-instruct (#780)
Co-authored-by: Max Deichmann <m.deichmann@tum.de>
2024-01-03 14:45:42 +00:00
Max DeichmannandGitHub 523ec0dd1f refactor: reduce sentry sample rate (#798)
sample
2024-01-03 13:37:41 +00:00
Marc Klingen 071becb796 chore: release v1.27.0
CI/CD / push-docker-image (push) Blocked by required conditions
CI/CD / tests (20) (push) Failing after 1m59s
CI/CD / tests (18) (push) Failing after 2m29s
release.yml / release (push) Failing after 2s
CI/CD / e2e-tests (push) Failing after 2m0s
CI/CD / lint (push) Successful in 5m26s
CI/CD / all-ci-passed (push) Waiting to run
2024-01-02 19:56:50 +01:00
Yash KhareandGitHub 2fe920bbba bug(ui): close sidebar after click on mobile (#788) 2024-01-02 17:46:33 +01:00
Marc Klingen 509dc92539 refactor(ui): use CodeView for prompt rendering 2024-01-02 16:18:38 +01:00
1116783587 feat(ui, api): Add Ordering functionality to traces (#750)
Co-authored-by: Max Deichmann <m.deichmann@tum.de>
2024-01-02 15:00:07 +01:00
Marc Klingen 5b3e8bd9d1 chore: release v1.26.0
CI/CD / push-docker-image (push) Blocked by required conditions
CI/CD / tests (20) (push) Failing after 2m3s
CI/CD / tests (18) (push) Failing after 2m41s
CI/CD / e2e-tests (push) Failing after 2m15s
release.yml / release (push) Failing after 3s
CI/CD / lint (push) Successful in 5m27s
CI/CD / all-ci-passed (push) Waiting to run
2024-01-02 14:51:58 +01:00
Marc KlingenandGitHub daa3292814 fix(ui): show linebreaks in prompts (#794) 2024-01-02 14:45:18 +01:00
Max DeichmannandGitHub 6abea1a028 refactor: upgrade follow-redirects library (#793)
upgrade follow-redirects
2024-01-02 12:47:42 +00:00
Marc KlingenandGitHub 660a238731 feat(api): return langfuse version on /health (#792)
Fixes #787
2024-01-02 10:16:44 +00:00
Marc Klingen 1c24604228 feat(cloud): link to langfuse.com/pricing in settings 2024-01-02 10:48:25 +01:00
Max Deichmann ba50a6f45d chore: release v1.25.0
CI/CD / push-docker-image (push) Blocked by required conditions
CI/CD / e2e-tests (push) Failing after 1m58s
CI/CD / tests (18) (push) Failing after 2m24s
CI/CD / tests (20) (push) Failing after 2m14s
release.yml / release (push) Failing after 3s
CI/CD / lint (push) Successful in 5m39s
CI/CD / all-ci-passed (push) Waiting to run
2023-12-31 14:51:14 +01:00
Marc KlingenandGitHub b65b055630 fix(rbac): add/fix rbac scopes for prompt mgmt (#786) 2023-12-31 14:05:13 +01:00
Marc Klingen c7c72efeac fix(rbac): VIEWER has access to prompts:read 2023-12-31 14:04:31 +01:00
Marc KlingenandGitHub 6489d2f27e feat(rbac): hide features in main menu based on rbac scopes (#785) 2023-12-31 13:01:40 +00:00
Marc KlingenandGitHub 2d8a239d6d feat(ui): ui improvements of prompt mgmt (#784) 2023-12-31 12:46:20 +00:00
Max DeichmannandGitHub 71a1f81b4d feat: allow fetching specific prompt version (#781) 2023-12-30 16:46:54 +00:00
Max DeichmannandGitHub 8574d028c0 feat: prompt management (#777) 2023-12-30 17:28:19 +01:00
Marc Klingen 0be0628d33 docs: Contributing.md 2023-12-28 18:44:28 +01:00
782e119add fix(UI): username and logo goes down as no. of projects increases (#759)
Co-authored-by: Max Deichmann <m.deichmann@tum.de>
2023-12-28 11:11:10 +01:00
Max DeichmannandGitHub 9079ffcdb1 refactor: upgrade axios (#771)
upgrade
2023-12-27 18:09:18 +00:00
Marc Klingen a7c1ad1b0d chore: release v1.24.2
CI/CD / push-docker-image (push) Blocked by required conditions
CI/CD / tests (18) (push) Failing after 2m27s
CI/CD / tests (20) (push) Failing after 2m20s
CI/CD / e2e-tests (push) Failing after 2m12s
release.yml / release (push) Failing after 3s
CI/CD / lint (push) Successful in 5m15s
CI/CD / all-ci-passed (push) Waiting to run
2023-12-27 16:00:11 +01:00
Marc Klingen 091757973f feat(ui): display id in tooltip on table link 2023-12-27 15:59:08 +01:00
Marc Klingen 762ac24f47 feat(ui): improve compact data tables 2023-12-27 15:52:20 +01:00
Marc Klingen 6f8ac2b796 chore: improve tailwind class 2023-12-27 15:52:00 +01:00
Marc Klingen 5157b3f599 feat(ui): extend openai message schema
* content json
* name
2023-12-27 15:32:07 +01:00
Marc Klingen 760879a183 feat(ui): less table padding 2023-12-27 12:42:07 +01:00
Marc Klingen 63e3dda268 fix: more test ids on wrong forms 2023-12-27 09:50:20 +01:00
Marc Klingen 284d38f3c3 fix: test ids on wrong forms 2023-12-27 09:50:03 +01:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
cc72964343 chore(deps-dev): bump @typescript-eslint/parser from 6.15.0 to 6.16.0 (#763)
Bumps [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) from 6.15.0 to 6.16.0.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/parser/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v6.16.0/packages/parser)

---
updated-dependencies:
- dependency-name: "@typescript-eslint/parser"
  dependency-type: direct:development
  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>
2023-12-26 22:58:02 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
de4984cef6 chore(deps-dev): bump @typescript-eslint/eslint-plugin from 6.15.0 to 6.16.0 (#764)
chore(deps-dev): bump @typescript-eslint/eslint-plugin

Bumps [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) from 6.15.0 to 6.16.0.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v6.16.0/packages/eslint-plugin)

---
updated-dependencies:
- dependency-name: "@typescript-eslint/eslint-plugin"
  dependency-type: direct:development
  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>
2023-12-26 22:54:05 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
dba56de614 chore(deps): bump lucide-react from 0.300.0 to 0.302.0 (#765)
Bumps [lucide-react](https://github.com/lucide-icons/lucide/tree/HEAD/packages/lucide-react) from 0.300.0 to 0.302.0.
- [Release notes](https://github.com/lucide-icons/lucide/releases)
- [Commits](https://github.com/lucide-icons/lucide/commits/0.302.0/packages/lucide-react)

---
updated-dependencies:
- dependency-name: lucide-react
  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>
2023-12-26 22:50:04 +00:00
Max DeichmannandGitHub bb102ba986 refactoring(ui): upgrade shadcn components (#767) 2023-12-26 16:23:03 +01:00
Marc Klingen 199bbcd6ae docs: add note regarding authentication to the openapi spec 2023-12-24 13:31:31 +01:00
Marc Klingen c8ee036dd2 chore: release v1.24.1
CI/CD / push-docker-image (push) Blocked by required conditions
CI/CD / tests (18) (push) Failing after 2m15s
CI/CD / tests (20) (push) Failing after 2m18s
release.yml / release (push) Failing after 3s
CI/CD / e2e-tests (push) Failing after 1m59s
CI/CD / lint (push) Successful in 5m30s
CI/CD / all-ci-passed (push) Waiting to run
2023-12-24 05:26:45 +01:00
Marc KlingenandGitHub dfff42074d fix(ui): add missing array polyfills for better browser compatibility (#757)
- Fixes #653
- methods: .toReversed, .toSorted, .toSpliced
- Fix until they are included in next-core: https://github.com/vercel/next.js/issues/58242
2023-12-24 04:23:26 +00:00
Marc Klingen 565893307e chore: release v1.24.0
CI/CD / push-docker-image (push) Blocked by required conditions
CI/CD / tests (18) (push) Failing after 41s
CI/CD / tests (20) (push) Failing after 44s
CI/CD / e2e-tests (push) Failing after 35s
release.yml / release (push) Failing after 3s
CI/CD / lint (push) Successful in 3m58s
CI/CD / all-ci-passed (push) Waiting to run
2023-12-24 03:35:53 +01:00
Marc KlingenandGitHub ff32270464 feat(auth): add AzureAD (#396) 2023-12-24 03:28:13 +01:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
98a91830f2 chore(deps): bump the sentry group with 1 update (#748)
Bumps the sentry group with 1 update: [@sentry/nextjs](https://github.com/getsentry/sentry-javascript).


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

---
updated-dependencies:
- dependency-name: "@sentry/nextjs"
  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>
2023-12-24 00:52:04 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
f72707ead5 chore(deps): bump lucide-react from 0.299.0 to 0.300.0 (#749)
Bumps [lucide-react](https://github.com/lucide-icons/lucide/tree/HEAD/packages/lucide-react) from 0.299.0 to 0.300.0.
- [Release notes](https://github.com/lucide-icons/lucide/releases)
- [Commits](https://github.com/lucide-icons/lucide/commits/0.300.0/packages/lucide-react)

---
updated-dependencies:
- dependency-name: lucide-react
  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>
2023-12-24 01:47:03 +01:00
Marc KlingenandGitHub 963cf945cb feat(cloud): display custom plan limits in project settings (#756) 2023-12-24 01:41:21 +01:00
Marc Klingen d5aa809ff9 chore: release v1.23.0
CI/CD / push-docker-image (push) Blocked by required conditions
CI/CD / tests (18) (push) Failing after 40s
CI/CD / tests (20) (push) Failing after 34s
CI/CD / e2e-tests (push) Failing after 37s
CI/CD / lint (push) Successful in 3m58s
CI/CD / all-ci-passed (push) Waiting to run
release.yml / release (push) Failing after 3s
2023-12-22 19:36:57 +01:00
1dd9a0e04e feat(ui): display host name on project settings page (#746)
* feat(ui): added hostName on the settings page

* simplify component

---------

Co-authored-by: Marc Klingen <git@marcklingen.com>
2023-12-22 18:35:58 +00:00
Marc KlingenandGitHub dbf16ba433 feat(ui): use native <a/> in tables to support middle click / open in new window (#747)
Fixes #744
2023-12-22 18:28:58 +00:00
Max DeichmannandGitHub d421c1e106 fix: add openai model for auto tokenisation (#745) 2023-12-22 17:37:25 +00:00
Marc Klingen 50cf25d949 chore: release v1.22.0
CI/CD / push-docker-image (push) Blocked by required conditions
CI/CD / tests (18) (push) Failing after 49s
CI/CD / tests (20) (push) Failing after 42s
CI/CD / e2e-tests (push) Failing after 38s
CI/CD / lint (push) Successful in 3m57s
CI/CD / all-ci-passed (push) Waiting to run
release.yml / release (push) Failing after 2s
2023-12-22 12:37:32 +01:00
Marc Klingen 1aeec5329e chore: pr/issue comments on new releases 2023-12-22 12:37:03 +01:00
Marc KlingenandGitHub 80120b0455 feat(docker): optionally set SHADOW_DATABASE_URL (#743)
Fixes #738
2023-12-22 12:10:28 +01:00
Marc Klingen 3f5ee4664a feat(ui): display current application version 2023-12-22 07:46:06 +01:00
Marc Klingen 659003d8e2 chore: release v1.21.0
CI/CD / push-docker-image (push) Blocked by required conditions
CI/CD / tests (18) (push) Failing after 38s
CI/CD / e2e-tests (push) Failing after 34s
CI/CD / tests (20) (push) Failing after 43s
CI/CD / lint (push) Successful in 3m58s
CI/CD / all-ci-passed (push) Waiting to run
release.yml / release (push) Failing after 2s
2023-12-22 07:36:41 +01:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2f48b4b33e chore(deps): bump lucide-react from 0.298.0 to 0.299.0 (#740)
Bumps [lucide-react](https://github.com/lucide-icons/lucide/tree/HEAD/packages/lucide-react) from 0.298.0 to 0.299.0.
- [Release notes](https://github.com/lucide-icons/lucide/releases)
- [Commits](https://github.com/lucide-icons/lucide/commits/0.299.0/packages/lucide-react)

---
updated-dependencies:
- dependency-name: lucide-react
  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>
2023-12-22 06:28:03 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
1830e0285a chore(deps): bump tailwind-merge from 2.1.0 to 2.2.0 (#741)
Bumps [tailwind-merge](https://github.com/dcastil/tailwind-merge) from 2.1.0 to 2.2.0.
- [Release notes](https://github.com/dcastil/tailwind-merge/releases)
- [Commits](https://github.com/dcastil/tailwind-merge/compare/v2.1.0...v2.2.0)

---
updated-dependencies:
- dependency-name: tailwind-merge
  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>
2023-12-22 06:24:08 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
d31b8ebc8b chore(deps): bump @tanstack/react-table from 8.10.7 to 8.11.2 (#742)
Bumps [@tanstack/react-table](https://github.com/tanstack/table) from 8.10.7 to 8.11.2.
- [Release notes](https://github.com/tanstack/table/releases)
- [Commits](https://github.com/tanstack/table/compare/v8.10.7...v8.11.2)

---
updated-dependencies:
- dependency-name: "@tanstack/react-table"
  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>
2023-12-22 07:18:03 +01:00
Marc Klingen acb431e77f docs: add infra chart to contributing 2023-12-21 22:56:27 +01:00
Marc Klingen bfd3665d70 docs: more details on used technologies 2023-12-21 21:41:28 +01:00
Marc Klingen a181eb13f5 docs: improve architecture overview 2023-12-21 21:12:10 +01:00
Marc KlingenandGitHub 43dd396a64 feat(docker): optionally set DIRECT_URL (#739)
Helpful to run migrations using a direct db connection when using pgbouncer on DATABASE_URL
2023-12-21 19:27:55 +01:00
Marc KlingenandGitHub 921d4fa1d3 feat: optionally use S3 for data exports (#735) 2023-12-21 17:46:52 +00:00
Max Deichmann 91aec645ba chore: release v1.20.0
CI/CD / push-docker-image (push) Blocked by required conditions
CI/CD / tests (20) (push) Failing after 35s
CI/CD / tests (18) (push) Failing after 46s
CI/CD / e2e-tests (push) Failing after 37s
release.yml / release (push) Failing after 3s
CI/CD / lint (push) Successful in 3m50s
CI/CD / all-ci-passed (push) Waiting to run
2023-12-21 16:38:32 +01:00
Max Deichmann c209e338d4 fix: add object to api docs 2023-12-21 16:38:12 +01:00
Max DeichmannandGitHub 0ba3e4f9e1 feat: add sdk log event to ingestion API (#737) 2023-12-21 14:55:08 +00:00
Marc Klingen 253b0cc38b Revert "fix(cloud): no response limit for trpc routes"
This reverts commit 8d6b57ec21.
2023-12-21 04:47:28 +01:00
dependabot[bot]andGitHub 8c7c7a34e5 chore(deps-dev): bump @typescript-eslint/eslint-plugin from 6.14.0 to 6.15.0 (#723) 2023-12-21 02:11:33 +00:00
Marc Klingen 8d6b57ec21 fix(cloud): no response limit for trpc routes
Fixes production bug of failing exports with responses > 4MB
2023-12-21 03:04:18 +01:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
8994a9c663 chore(deps): bump the sentry group with 1 update (#730)
Bumps the sentry group with 1 update: [@sentry/nextjs](https://github.com/getsentry/sentry-javascript).


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

---
updated-dependencies:
- dependency-name: "@sentry/nextjs"
  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>
2023-12-21 01:00:19 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
c048372928 chore(deps-dev): bump @types/eslint from 8.44.9 to 8.56.0 (#731)
Bumps [@types/eslint](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/eslint) from 8.44.9 to 8.56.0.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/eslint)

---
updated-dependencies:
- dependency-name: "@types/eslint"
  dependency-type: direct:development
  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>
2023-12-21 00:56:05 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
0271582c0d chore(deps-dev): bump tailwindcss from 3.3.6 to 3.4.0 (#733)
Bumps [tailwindcss](https://github.com/tailwindlabs/tailwindcss) from 3.3.6 to 3.4.0.
- [Release notes](https://github.com/tailwindlabs/tailwindcss/releases)
- [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/master/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/tailwindcss/compare/v3.3.6...v3.4.0)

---
updated-dependencies:
- dependency-name: tailwindcss
  dependency-type: direct:development
  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>
2023-12-21 01:51:49 +01:00
Marc KlingenandGitHub a4b71e0805 fix: remove console log 2023-12-20 14:33:01 +01:00
Max DeichmannandGitHub 66ac595850 style: add comment on filter state (#729) 2023-12-20 13:16:15 +00:00
Max Deichmann d4f037f2a5 chore: release v1.19.4
CI/CD / push-docker-image (push) Blocked by required conditions
CI/CD / tests (20) (push) Failing after 1m0s
CI/CD / tests (18) (push) Failing after 1m15s
release.yml / release (push) Failing after 3s
CI/CD / e2e-tests (push) Failing after 1m0s
CI/CD / lint (push) Successful in 4m15s
CI/CD / all-ci-passed (push) Waiting to run
2023-12-20 13:53:32 +01:00
Max Deichmann c1fe9e0ace fix table filter 2023-12-20 13:46:48 +01:00
dependabot[bot]andGitHub a8a108e3fd chore(deps): bump the prisma group with 2 updates (#720) 2023-12-20 01:41:11 +00:00
dependabot[bot]andGitHub 0d1c6bb062 chore(deps-dev): bump @typescript-eslint/parser from 6.14.0 to 6.15.0 (#722) 2023-12-20 01:36:53 +00:00
dependabot[bot]andGitHub 071ec219ce chore(deps): bump @heroicons/react from 2.0.18 to 2.1.1 (#724) 2023-12-20 01:32:41 +00:00
dependabot[bot]andGitHub 626eab5258 chore(deps): bump the patches group with 1 update (#727) 2023-12-20 02:28:06 +01:00
Max Deichmann f683678224 chore: release v1.19.3
CI/CD / push-docker-image (push) Blocked by required conditions
CI/CD / tests (18) (push) Failing after 1m10s
release.yml / release (push) Failing after 2s
CI/CD / tests (20) (push) Failing after 1m5s
CI/CD / e2e-tests (push) Failing after 1m0s
CI/CD / lint (push) Successful in 4m12s
CI/CD / all-ci-passed (push) Waiting to run
2023-12-19 13:13:49 +01:00
Max DeichmannandGitHub 2d88bdb27f fix: remove log (#726) 2023-12-19 12:11:32 +00:00
Max DeichmannandGitHub 0b9481b7c4 feat: add cost column to generations table (#725) 2023-12-19 11:55:21 +00:00
Max Deichmann 603bd2a740 chore: release v1.19.2
CI/CD / push-docker-image (push) Blocked by required conditions
CI/CD / tests (18) (push) Failing after 1m7s
release.yml / release (push) Failing after 3s
CI/CD / tests (20) (push) Failing after 1m0s
CI/CD / e2e-tests (push) Failing after 59s
CI/CD / lint (push) Successful in 4m13s
CI/CD / all-ci-passed (push) Waiting to run
2023-12-18 17:09:57 +01:00
Max DeichmannandGitHub 576b7387be fix: downgrade tremor react (#718) 2023-12-18 16:09:00 +00:00
Marc Klingen e6662a9b18 chore: release v1.19.1
CI/CD / push-docker-image (push) Blocked by required conditions
CI/CD / tests (20) (push) Failing after 1m0s
CI/CD / tests (18) (push) Failing after 1m9s
release.yml / release (push) Failing after 2s
CI/CD / e2e-tests (push) Failing after 1m5s
CI/CD / lint (push) Successful in 4m12s
CI/CD / all-ci-passed (push) Waiting to run
2023-12-18 09:26:37 +01:00
Marc Klingen f2d2234c51 feat(ui): remove beta label from sessions 2023-12-18 09:26:14 +01:00
Max DeichmannandGitHub 24f20bd54f refactor: remove logs (#715)
remove logs
2023-12-18 07:56:56 +00:00
Marc KlingenandGitHub bd6821347f chore(deps): bump deps (#709) 2023-12-17 23:29:11 +00:00
Marc Klingen 15d4f5b262 chore: release v1.19.0
CI/CD / push-docker-image (push) Blocked by required conditions
CI/CD / tests (18) (push) Failing after 1m10s
CI/CD / tests (20) (push) Failing after 1m5s
release.yml / release (push) Failing after 3s
CI/CD / e2e-tests (push) Failing after 1m0s
CI/CD / lint (push) Successful in 4m13s
CI/CD / all-ci-passed (push) Waiting to run
2023-12-18 00:06:26 +01:00
Marc Klingen eb2c1fc9fe feat(analytics): add trace deletions events 2023-12-18 00:02:30 +01:00
b00e473e40 feat(ui): Multi select deletion on trace table (#580)
---------

Co-authored-by: Marc Klingen <git@marcklingen.com>
2023-12-17 23:50:29 +01:00
Felix WuandGitHub b70d554756 fix(db): use utc time in prisma datetime filter (#708) 2023-12-17 21:09:32 +00:00
Marc Klingen 50d3fbfaf5 Revert "feat: tag legacy endpoints in events table"
This reverts commit 01bce79e6f.
2023-12-17 21:54:19 +01:00
Marc Klingen 1fb58ea9fa chore(deps): revert to tremor 3.11.1 to fix license warnings 2023-12-17 21:23:29 +01:00
Marc Klingen 01bce79e6f feat: tag legacy endpoints in events table 2023-12-17 21:07:51 +01:00
Felix WuandGitHub 0b4388107e feat(ui): search tables upon hitting enter (#704) 2023-12-17 20:52:37 +01:00
Marc KlingenandGitHub d8ef8aa560 refactor: add strict-type-checked lint rule and clean up types (#706) 2023-12-17 20:44:05 +01:00
Felix WuandGitHub 373f6de51e docs: fix Contributing guide link (#705) 2023-12-17 20:02:24 +01:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
58e87f57ec chore(deps-dev): bump eslint from 8.55.0 to 8.56.0 (#702)
Bumps [eslint](https://github.com/eslint/eslint) from 8.55.0 to 8.56.0.
- [Release notes](https://github.com/eslint/eslint/releases)
- [Changelog](https://github.com/eslint/eslint/blob/main/CHANGELOG.md)
- [Commits](https://github.com/eslint/eslint/compare/v8.55.0...v8.56.0)

---
updated-dependencies:
- dependency-name: eslint
  dependency-type: direct:development
  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>
2023-12-17 16:23:04 +01:00
Marc Klingen 63ed992ef6 ci: add merge_group trigger 2023-12-17 16:02:26 +01:00
Max DeichmannandGitHub 977f006a74 feat: add metadata to ingestion api (#703)
add metadata to ingestion api
2023-12-17 15:26:36 +01:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
f81fe96997 chore(deps): bump @tremor/react from 3.11.1 to 3.12.0 (#693)
Bumps [@tremor/react](https://github.com/tremorlabs/tremor) from 3.11.1 to 3.12.0.
- [Release notes](https://github.com/tremorlabs/tremor/releases)
- [Commits](https://github.com/tremorlabs/tremor/compare/v3.11.1...v3.12.0)

---
updated-dependencies:
- dependency-name: "@tremor/react"
  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>
2023-12-16 04:48:07 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
d77b4a09e6 chore(deps-dev): bump the patches group with 1 update (#700)
Bumps the patches group with 1 update: [@types/react-dom](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react-dom).

- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react-dom)

---
updated-dependencies:
- dependency-name: "@types/react-dom"
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: patches
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-16 04:44:03 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
86ab1a0cf3 chore(deps): bump the sentry group with 1 update (#701)
Bumps the sentry group with 1 update: [@sentry/nextjs](https://github.com/getsentry/sentry-javascript).

- [Release notes](https://github.com/getsentry/sentry-javascript/releases)
- [Changelog](https://github.com/getsentry/sentry-javascript/blob/develop/CHANGELOG.md)
- [Commits](https://github.com/getsentry/sentry-javascript/compare/7.87.0...7.88.0)

---
updated-dependencies:
- dependency-name: "@sentry/nextjs"
  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>
2023-12-16 04:34:51 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
b3139ae853 chore(deps): bump @sentry/types from 7.87.0 to 7.88.0 (#695)
Bumps [@sentry/types](https://github.com/getsentry/sentry-javascript) from 7.87.0 to 7.88.0.
- [Release notes](https://github.com/getsentry/sentry-javascript/releases)
- [Changelog](https://github.com/getsentry/sentry-javascript/blob/develop/CHANGELOG.md)
- [Commits](https://github.com/getsentry/sentry-javascript/compare/7.87.0...7.88.0)

---
updated-dependencies:
- dependency-name: "@sentry/types"
  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>
2023-12-16 04:30:40 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
c9c0a4c207 chore(deps): bump posthog-js from 1.94.2 to 1.96.1 (#698)
Bumps [posthog-js](https://github.com/PostHog/posthog-js) from 1.94.2 to 1.96.1.
- [Release notes](https://github.com/PostHog/posthog-js/releases)
- [Changelog](https://github.com/PostHog/posthog-js/blob/master/CHANGELOG.md)
- [Commits](https://github.com/PostHog/posthog-js/compare/v1.94.2...v1.96.1)

---
updated-dependencies:
- dependency-name: posthog-js
  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>
2023-12-16 04:26:11 +00:00
Marc Klingen ead12b3838 chore: dependabot fix sentry group 2023-12-16 05:22:10 +01:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2849954f71 chore(deps): bump lucide-react from 0.295.0 to 0.297.0 (#699)
Bumps [lucide-react](https://github.com/lucide-icons/lucide/tree/HEAD/packages/lucide-react) from 0.295.0 to 0.297.0.
- [Release notes](https://github.com/lucide-icons/lucide/releases)
- [Commits](https://github.com/lucide-icons/lucide/commits/0.297.0/packages/lucide-react)

---
updated-dependencies:
- dependency-name: lucide-react
  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>
2023-12-16 05:20:40 +01:00
Max Deichmann 06305385f7 chore: release v1.18.1
CI/CD / push-docker-image (push) Blocked by required conditions
CI/CD / tests (18) (push) Failing after 1m7s
CI/CD / tests (20) (push) Failing after 1m6s
CI/CD / e2e-tests (push) Failing after 1m1s
CI/CD / lint (push) Successful in 4m14s
CI/CD / all-ci-passed (push) Waiting to run
release.yml / release (push) Failing after 3s
2023-12-15 11:34:11 +01:00
Max DeichmannandGitHub e25c29e654 fix(api): handle empty usage object gracefully (#696) 2023-12-15 10:33:35 +00:00
Max Deichmann 1a037e84e6 chore: release v1.18.0
CI/CD / push-docker-image (push) Blocked by required conditions
CI/CD / tests (18) (push) Failing after 1m10s
CI/CD / tests (20) (push) Failing after 1m7s
CI/CD / e2e-tests (push) Failing after 1m1s
CI/CD / lint (push) Successful in 4m13s
CI/CD / all-ci-passed (push) Waiting to run
release.yml / release (push) Failing after 3s
2023-12-15 10:20:26 +01:00
Max DeichmannandGitHub 45f091eeee feat(api): support v2 SDKs (#644) (#656) 2023-12-15 09:06:36 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
41a9e4f38c chore(deps): bump lucide-react from 0.294.0 to 0.295.0 (#689)
Bumps [lucide-react](https://github.com/lucide-icons/lucide/tree/HEAD/packages/lucide-react) from 0.294.0 to 0.295.0.
- [Release notes](https://github.com/lucide-icons/lucide/releases)
- [Commits](https://github.com/lucide-icons/lucide/commits/0.295.0/packages/lucide-react)

---
updated-dependencies:
- dependency-name: lucide-react
  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>
2023-12-14 23:01:48 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
b50c7e4e80 chore(deps-dev): bump @typescript-eslint/parser from 6.13.2 to 6.14.0 (#681)
Bumps [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) from 6.13.2 to 6.14.0.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/parser/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v6.14.0/packages/parser)

---
updated-dependencies:
- dependency-name: "@typescript-eslint/parser"
  dependency-type: direct:development
  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>
2023-12-14 01:18:38 +00:00
74fbdac607 fix: upgrade sentry (#687)
* upgrade

* add sentry as a dependabto group

* add @sentry/types

---------

Co-authored-by: Marc Klingen <git@marcklingen.com>
2023-12-14 02:13:24 +01:00
Marc Klingen f86d24669e chore: release v1.17.3
CI/CD / push-docker-image (push) Blocked by required conditions
CI/CD / tests (18) (push) Failing after 1m6s
CI/CD / tests (20) (push) Failing after 1m8s
CI/CD / e2e-tests (push) Failing after 1m1s
CI/CD / lint (push) Successful in 4m15s
CI/CD / all-ci-passed (push) Waiting to run
release.yml / release (push) Failing after 2s
2023-12-14 01:38:44 +01:00
Marc Klingen 9cb9e64aba fix(ui): no pretty obervation rendering if other properties exist 2023-12-14 01:37:58 +01:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
5866048be8 chore(deps): bump the patches group with 3 updates (#688)
Bumps the patches group with 3 updates: [react-hook-form](https://github.com/react-hook-form/react-hook-form), [@types/eslint](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/eslint) and [@types/react](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react).


Updates `react-hook-form` from 7.49.0 to 7.49.2
- [Release notes](https://github.com/react-hook-form/react-hook-form/releases)
- [Changelog](https://github.com/react-hook-form/react-hook-form/blob/master/CHANGELOG.md)
- [Commits](https://github.com/react-hook-form/react-hook-form/compare/v7.49.0...v7.49.2)

Updates `@types/eslint` from 8.44.8 to 8.44.9
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/eslint)

Updates `@types/react` from 18.2.43 to 18.2.45
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react)

---
updated-dependencies:
- dependency-name: react-hook-form
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: patches
- dependency-name: "@types/eslint"
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: patches
- dependency-name: "@types/react"
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: patches
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-14 00:35:04 +00:00
Marc Klingen b18e75d5a4 fix(ui): scroll on single trace view 2023-12-14 01:22:09 +01:00
Marc Klingen 0e433b5f5c feat: improve display of time intervals 2023-12-13 19:37:46 +01:00
Marc Klingen 6274cc442d chore: release v1.17.2
CI/CD / push-docker-image (push) Blocked by required conditions
CI/CD / tests (18) (push) Failing after 1m12s
CI/CD / tests (20) (push) Failing after 1m7s
CI/CD / e2e-tests (push) Failing after 1m2s
CI/CD / lint (push) Successful in 4m14s
CI/CD / all-ci-passed (push) Waiting to run
release.yml / release (push) Failing after 2s
2023-12-13 12:19:49 +01:00
Marc Klingen f2e468893f fix(ui): trpc error sessions.all if no userid 2023-12-13 12:14:53 +01:00
Marc Klingen f3817a4aa7 chore: remove unused searchQuery 2023-12-13 12:13:47 +01:00
Marc KlingenandGitHub cb99998c53 chore: remove unused export (#684) 2023-12-13 10:44:03 +00:00
Max Deichmann f6a6f99954 chore: release v1.17.1
CI/CD / push-docker-image (push) Blocked by required conditions
CI/CD / tests (18) (push) Failing after 1m10s
CI/CD / tests (20) (push) Failing after 1m7s
CI/CD / e2e-tests (push) Failing after 1m3s
CI/CD / lint (push) Successful in 4m16s
CI/CD / all-ci-passed (push) Waiting to run
release.yml / release (push) Failing after 3s
2023-12-13 10:44:52 +01:00
Max DeichmannandGitHub 248bd1ea88 fix(api): add logging to sessions router (#683) 2023-12-13 09:40:51 +00:00
Marc Klingen 13a79c4206 chore: release v1.17.0
CI/CD / push-docker-image (push) Blocked by required conditions
CI/CD / tests (18) (push) Failing after 1m9s
CI/CD / tests (20) (push) Failing after 1m0s
CI/CD / e2e-tests (push) Failing after 1m2s
CI/CD / lint (push) Successful in 4m15s
CI/CD / all-ci-passed (push) Waiting to run
release.yml / release (push) Failing after 3s
2023-12-13 03:25:45 +01:00
Marc Klingen 425e3ca0e4 fix: session filters 2023-12-13 03:21:59 +01:00
Marc Klingen d1c4eff4fc fix(ui): sessionduration in seconds 2023-12-13 02:17:42 +01:00
Marc KlingenandGitHub e6bd4e926d feat(ui): in-app help popups re sessions (#678) 2023-12-12 20:06:44 +00:00
Marc KlingenandGitHub 35422e9793 feat(ui): show score comments on hover across ui (#677)
* refactor: tooltip provider in _app

* feat(ui): show score comments on hover across ui
2023-12-12 19:42:18 +00:00
Marc Klingen e106fef923 feat(cloud): include link to chatbot demo in staging 2023-12-12 19:40:14 +01:00
Max DeichmannandGitHub 41ef404699 fix(logging): remove log from trpc router (#675)
remove log
2023-12-12 15:20:39 +00:00
Marc KlingenandGitHub 45e31c0b3f docs: update SECURITY.md 2023-12-12 16:12:14 +01:00
Max DeichmannandGitHub 0df4c97819 fix(fern): fix python generator (#674) 2023-12-12 15:52:22 +01:00
Max DeichmannandGitHub 77b448a7f7 feat(docs): architecture overview added to contributing file (#673) 2023-12-12 14:41:23 +00:00
Marc KlingenandGitHub 4e520f7e52 feat: add sessions and i/o on traces (#588) 2023-12-12 14:27:57 +00:00
Marc Klingen df0f01eae2 chore: release v1.16.0
CI/CD / push-docker-image (push) Blocked by required conditions
CI/CD / tests (18) (push) Failing after 1m16s
CI/CD / e2e-tests (push) Failing after 1m2s
CI/CD / tests (20) (push) Failing after 1m5s
CI/CD / lint (push) Successful in 4m15s
CI/CD / all-ci-passed (push) Waiting to run
release.yml / release (push) Failing after 3s
2023-12-12 14:55:26 +01:00
Marc KlingenandGitHub 82626a0d57 feat: improve project renaming (#670) 2023-12-12 13:36:03 +00:00
Marc KlingenandGitHub 0c36c4166f feat(security): add x-frame-options header (#669) 2023-12-12 13:20:00 +00:00
Max DeichmannandGitHub 57c6bcd5bb fix(ui): pretty project settings UI (#667) 2023-12-12 11:25:25 +00:00
16d8665cd2 feat: rename project (#613) (#666)
Co-authored-by: Adrian Stobbe <stobbe.adrian@gmail.com>
Co-authored-by: Marc Klingen <git@marcklingen.com>
2023-12-12 12:20:21 +01:00
Max Deichmann 34373dc841 chore: release v1.15.1
CI/CD / push-docker-image (push) Blocked by required conditions
CI/CD / tests (18) (push) Failing after 1m8s
CI/CD / tests (20) (push) Failing after 1m1s
CI/CD / e2e-tests (push) Failing after 1m6s
CI/CD / lint (push) Successful in 4m28s
CI/CD / all-ci-passed (push) Waiting to run
release.yml / release (push) Failing after 3s
2023-12-12 09:59:58 +01:00
Max DeichmannandGitHub 8410315920 fix(api): remove middleware (#665) 2023-12-12 09:59:11 +01:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
78db8d339a chore(deps-dev): bump the patches group with 2 updates (#659)
Bumps the patches group with 2 updates: [@types/react](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react) and [prettier](https://github.com/prettier/prettier).


Updates `@types/react` from 18.2.42 to 18.2.43
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react)

Updates `prettier` from 3.1.0 to 3.1.1
- [Release notes](https://github.com/prettier/prettier/releases)
- [Changelog](https://github.com/prettier/prettier/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prettier/prettier/compare/3.1.0...3.1.1)

---
updated-dependencies:
- dependency-name: "@types/react"
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: patches
- dependency-name: prettier
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: patches
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-11 22:54:19 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
39cd086024 chore(deps-dev): bump @typescript-eslint/eslint-plugin from 6.13.2 to 6.14.0 (#663)
chore(deps-dev): bump @typescript-eslint/eslint-plugin

Bumps [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) from 6.13.2 to 6.14.0.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v6.14.0/packages/eslint-plugin)

---
updated-dependencies:
- dependency-name: "@typescript-eslint/eslint-plugin"
  dependency-type: direct:development
  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>
2023-12-11 22:45:03 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
82c379d273 chore(deps): bump react-hook-form from 7.48.2 to 7.49.0 (#660)
Bumps [react-hook-form](https://github.com/react-hook-form/react-hook-form) from 7.48.2 to 7.49.0.
- [Release notes](https://github.com/react-hook-form/react-hook-form/releases)
- [Changelog](https://github.com/react-hook-form/react-hook-form/blob/master/CHANGELOG.md)
- [Commits](https://github.com/react-hook-form/react-hook-form/compare/v7.48.2...v7.49.0)

---
updated-dependencies:
- dependency-name: react-hook-form
  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>
2023-12-11 22:39:11 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
20e27bb652 chore(deps): bump posthog-js from 1.93.6 to 1.94.2 (#661)
Bumps [posthog-js](https://github.com/PostHog/posthog-js) from 1.93.6 to 1.94.2.
- [Release notes](https://github.com/PostHog/posthog-js/releases)
- [Changelog](https://github.com/PostHog/posthog-js/blob/master/CHANGELOG.md)
- [Commits](https://github.com/PostHog/posthog-js/compare/v1.93.6...v1.94.2)

---
updated-dependencies:
- dependency-name: posthog-js
  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>
2023-12-11 23:33:50 +01:00
Marc Klingen a7e74a8937 chore: release v1.15.0
CI/CD / push-docker-image (push) Blocked by required conditions
CI/CD / tests (18) (push) Failing after 1m3s
CI/CD / tests (20) (push) Failing after 57s
CI/CD / e2e-tests (push) Failing after 1m0s
CI/CD / lint (push) Successful in 4m40s
CI/CD / all-ci-passed (push) Waiting to run
release.yml / release (push) Failing after 3s
2023-12-11 20:55:22 +01:00
Marc KlingenandGitHub 77f5a92901 feat(ui): add level, status message, metadata to generations table (#658) 2023-12-11 19:44:15 +00:00
Marc KlingenandGitHub 6256bf0dd5 refactor: improve column visibility interfaces (#655) 2023-12-11 09:55:09 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
ecc5a58d2b chore(deps): bump the patches group with 7 updates (#654)
Bumps the patches group with 7 updates:

| Package | From | To |
| --- | --- | --- |
| [next](https://github.com/vercel/next.js) | `14.0.3` | `14.0.4` |
| [@types/jest](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/jest) | `29.5.10` | `29.5.11` |
| [eslint-config-next](https://github.com/vercel/next.js/tree/HEAD/packages/eslint-config-next) | `14.0.3` | `14.0.4` |
| [prettier-plugin-tailwindcss](https://github.com/tailwindlabs/prettier-plugin-tailwindcss) | `0.5.8` | `0.5.9` |
| [release-it](https://github.com/release-it/release-it) | `17.0.0` | `17.0.1` |
| [ts-node](https://github.com/TypeStrong/ts-node) | `10.9.1` | `10.9.2` |
| [typescript](https://github.com/Microsoft/TypeScript) | `5.3.2` | `5.3.3` |


Updates `next` from 14.0.3 to 14.0.4
- [Release notes](https://github.com/vercel/next.js/releases)
- [Changelog](https://github.com/vercel/next.js/blob/canary/release.js)
- [Commits](https://github.com/vercel/next.js/compare/v14.0.3...v14.0.4)

Updates `@types/jest` from 29.5.10 to 29.5.11
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/jest)

Updates `eslint-config-next` from 14.0.3 to 14.0.4
- [Release notes](https://github.com/vercel/next.js/releases)
- [Changelog](https://github.com/vercel/next.js/blob/canary/release.js)
- [Commits](https://github.com/vercel/next.js/commits/v14.0.4/packages/eslint-config-next)

Updates `prettier-plugin-tailwindcss` from 0.5.8 to 0.5.9
- [Release notes](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/releases)
- [Changelog](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/compare/v0.5.8...v0.5.9)

Updates `release-it` from 17.0.0 to 17.0.1
- [Release notes](https://github.com/release-it/release-it/releases)
- [Changelog](https://github.com/release-it/release-it/blob/main/CHANGELOG.md)
- [Commits](https://github.com/release-it/release-it/compare/17.0.0...17.0.1)

Updates `ts-node` from 10.9.1 to 10.9.2
- [Release notes](https://github.com/TypeStrong/ts-node/releases)
- [Changelog](https://github.com/TypeStrong/ts-node/blob/main/development-docs/release-template.md)
- [Commits](https://github.com/TypeStrong/ts-node/compare/v10.9.1...v10.9.2)

Updates `typescript` from 5.3.2 to 5.3.3
- [Release notes](https://github.com/Microsoft/TypeScript/releases)
- [Commits](https://github.com/Microsoft/TypeScript/compare/v5.3.2...v5.3.3)

---
updated-dependencies:
- dependency-name: next
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: patches
- dependency-name: "@types/jest"
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: patches
- dependency-name: eslint-config-next
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: patches
- dependency-name: prettier-plugin-tailwindcss
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: patches
- dependency-name: release-it
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: patches
- dependency-name: ts-node
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: patches
- dependency-name: typescript
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: patches
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-11 10:51:44 +01:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2cccdceda7 chore(deps): bump the prisma group with 2 updates (#646)
Bumps the prisma group with 2 updates: [@prisma/client](https://github.com/prisma/prisma/tree/HEAD/packages/client) and [prisma](https://github.com/prisma/prisma/tree/HEAD/packages/cli).


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

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

---
updated-dependencies:
- dependency-name: "@prisma/client"
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prisma
- dependency-name: prisma
  dependency-type: direct:development
  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>
2023-12-06 22:54:53 +00:00
Marc Klingen f7993152b7 chore: release v1.14.0
CI/CD / push-docker-image (push) Blocked by required conditions
CI/CD / tests (18) (push) Failing after 1m9s
CI/CD / e2e-tests (push) Failing after 1m0s
CI/CD / tests (20) (push) Failing after 1m24s
CI/CD / lint (push) Successful in 4m14s
CI/CD / all-ci-passed (push) Waiting to run
release.yml / release (push) Failing after 2s
2023-12-06 23:44:36 +01:00
Marc KlingenandGitHub cf0bbfd93f feat(ui): filter traces by latency (#645) 2023-12-06 22:07:28 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
803a19ff3b chore(deps): bump posthog-node from 3.1.3 to 3.2.0 (#641)
Bumps [posthog-node](https://github.com/PostHog/posthog-node) from 3.1.3 to 3.2.0.
- [Release notes](https://github.com/PostHog/posthog-node/releases)
- [Commits](https://github.com/PostHog/posthog-node/commits)

---
updated-dependencies:
- dependency-name: posthog-node
  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>
2023-12-06 11:08:55 +00:00
Max DeichmannandGitHub 44e4010e15 feat: skip batching trpc routes for dashboards (#643) 2023-12-06 12:03:53 +01:00
Max DeichmannandGitHub 5cbcf00595 feat: upgrade fern python settings and versions (#642) 2023-12-05 23:02:25 +00:00
Marc Klingen 49e75e15e5 chore: release v1.13.3
CI/CD / push-docker-image (push) Blocked by required conditions
CI/CD / tests (18) (push) Failing after 1m17s
CI/CD / tests (20) (push) Failing after 57s
CI/CD / e2e-tests (push) Failing after 1m6s
release.yml / release (push) Failing after 3s
CI/CD / lint (push) Successful in 4m21s
CI/CD / all-ci-passed (push) Waiting to run
2023-12-05 21:45:54 +01:00
Bernd StrehlandGitHub 1a3fcc0041 feat: add separate db variables as alternative to connection string (#639) 2023-12-05 21:44:00 +01:00
Marc Klingen 9b76029686 chore: release v1.13.2
CI/CD / push-docker-image (push) Blocked by required conditions
CI/CD / tests (18) (push) Failing after 1m5s
CI/CD / tests (20) (push) Failing after 1m2s
CI/CD / e2e-tests (push) Failing after 1m1s
CI/CD / lint (push) Successful in 4m13s
CI/CD / all-ci-passed (push) Waiting to run
release.yml / release (push) Failing after 3s
2023-12-05 20:09:28 +01:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
d6902e124d chore(deps): bump the patches group with 8 updates (#638)
Bumps the patches group with 8 updates:

| Package | From | To |
| --- | --- | --- |
| [posthog-js](https://github.com/PostHog/posthog-js) | `1.93.3` | `1.93.6` |
| [ts-pattern](https://github.com/gvergnaud/ts-pattern) | `5.0.5` | `5.0.6` |
| [@types/react](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react) | `18.2.40` | `18.2.42` |
| [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) | `6.13.1` | `6.13.2` |
| [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) | `6.13.1` | `6.13.2` |
| [postcss](https://github.com/postcss/postcss) | `8.4.31` | `8.4.32` |
| [prettier-plugin-tailwindcss](https://github.com/tailwindlabs/prettier-plugin-tailwindcss) | `0.5.7` | `0.5.8` |
| [tailwindcss](https://github.com/tailwindlabs/tailwindcss) | `3.3.5` | `3.3.6` |


Updates `posthog-js` from 1.93.3 to 1.93.6
- [Release notes](https://github.com/PostHog/posthog-js/releases)
- [Changelog](https://github.com/PostHog/posthog-js/blob/master/CHANGELOG.md)
- [Commits](https://github.com/PostHog/posthog-js/compare/v1.93.3...v1.93.6)

Updates `ts-pattern` from 5.0.5 to 5.0.6
- [Release notes](https://github.com/gvergnaud/ts-pattern/releases)
- [Commits](https://github.com/gvergnaud/ts-pattern/compare/v5.0.5...v5.0.6)

Updates `@types/react` from 18.2.40 to 18.2.42
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react)

Updates `@typescript-eslint/eslint-plugin` from 6.13.1 to 6.13.2
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v6.13.2/packages/eslint-plugin)

Updates `@typescript-eslint/parser` from 6.13.1 to 6.13.2
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/parser/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v6.13.2/packages/parser)

Updates `postcss` from 8.4.31 to 8.4.32
- [Release notes](https://github.com/postcss/postcss/releases)
- [Changelog](https://github.com/postcss/postcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/postcss/postcss/compare/8.4.31...8.4.32)

Updates `prettier-plugin-tailwindcss` from 0.5.7 to 0.5.8
- [Release notes](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/releases)
- [Changelog](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/compare/v0.5.7...v0.5.8)

Updates `tailwindcss` from 3.3.5 to 3.3.6
- [Release notes](https://github.com/tailwindlabs/tailwindcss/releases)
- [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/master/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/tailwindcss/compare/v3.3.5...v3.3.6)

---
updated-dependencies:
- dependency-name: posthog-js
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: patches
- dependency-name: ts-pattern
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: patches
- dependency-name: "@types/react"
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: patches
- dependency-name: "@typescript-eslint/eslint-plugin"
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: patches
- dependency-name: "@typescript-eslint/parser"
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: patches
- dependency-name: postcss
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: patches
- dependency-name: prettier-plugin-tailwindcss
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: patches
- dependency-name: tailwindcss
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: patches
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-05 16:57:34 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
075deb99c2 chore(deps): bump tailwind-merge from 2.0.0 to 2.1.0 (#632)
Bumps [tailwind-merge](https://github.com/dcastil/tailwind-merge) from 2.0.0 to 2.1.0.
- [Release notes](https://github.com/dcastil/tailwind-merge/releases)
- [Commits](https://github.com/dcastil/tailwind-merge/compare/v2.0.0...v2.1.0)

---
updated-dependencies:
- dependency-name: tailwind-merge
  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>
2023-12-05 16:30:18 +00:00
Marc KlingenandGitHub 6da21c6c22 refactor: sequential database access (#637) 2023-12-05 16:23:33 +00:00
Marc Klingen 59ef3e898e Merge branch 'main' of github.com:langfuse/langfuse 2023-12-05 16:50:07 +01:00
Marc Klingen c737447a83 chore: add middleware 2023-12-05 16:50:03 +01:00
Max Deichmann 474a1e28ff chore: release v1.13.1
CI/CD / push-docker-image (push) Blocked by required conditions
CI/CD / tests (18) (push) Failing after 1m16s
CI/CD / tests (20) (push) Failing after 1m7s
CI/CD / e2e-tests (push) Failing after 1m2s
CI/CD / lint (push) Successful in 4m22s
CI/CD / all-ci-passed (push) Waiting to run
release.yml / release (push) Failing after 3s
2023-12-04 17:34:25 +01:00
Max DeichmannandGitHub a0b84773dc fix(ingestion): filter NULL string values from ingested events (#628) 2023-12-04 17:23:04 +01:00
Marc Klingen 00547bf93d chore: release v1.13.0
CI/CD / push-docker-image (push) Blocked by required conditions
CI/CD / tests (18) (push) Failing after 1m17s
CI/CD / tests (20) (push) Failing after 1m1s
CI/CD / e2e-tests (push) Failing after 1m7s
CI/CD / lint (push) Successful in 4m23s
CI/CD / all-ci-passed (push) Waiting to run
release.yml / release (push) Failing after 3s
2023-12-04 13:22:22 +01:00
Marc Klingen feb964f12b fix(ui): move column selector to the right of the toolbar 2023-12-04 12:57:33 +01:00
Max DeichmannandGitHub be72460af2 fix: remove custom hook for column selector (#627) 2023-12-04 10:57:25 +00:00
Marc KlingenandGitHub d5a86d394c feat(api): publish trace (#626) 2023-12-04 09:30:42 +00:00
Max DeichmannGitHub@RichardKruemmel
4e89d7d65a feature: configure table column visibility (#623) (#624)
Co-authored-by: @RichardKruemmel
2023-12-04 10:09:14 +01:00
Marc Klingen 28b7eab060 chore: release v1.12.0
CI/CD / push-docker-image (push) Blocked by required conditions
CI/CD / tests (18) (push) Failing after 1m18s
CI/CD / tests (20) (push) Failing after 57s
CI/CD / e2e-tests (push) Failing after 1m8s
CI/CD / lint (push) Successful in 4m21s
CI/CD / all-ci-passed (push) Waiting to run
release.yml / release (push) Failing after 4s
2023-12-03 01:00:37 +01:00
Marc KlingenandGitHub 2ccd140e9a feat(ui): copy formatted json (#621)
Fixes #617
2023-12-02 22:31:59 +00:00
Marc Klingen d47a4d67d4 chore: release v1.11.1
CI/CD / push-docker-image (push) Blocked by required conditions
CI/CD / tests (18) (push) Failing after 1m18s
CI/CD / tests (20) (push) Failing after 1m3s
CI/CD / e2e-tests (push) Failing after 1m7s
CI/CD / lint (push) Successful in 4m14s
CI/CD / all-ci-passed (push) Waiting to run
release.yml / release (push) Failing after 3s
2023-12-02 01:20:59 +01:00
Marc Klingen e20ad85a13 fix(ui): main menu css 2023-12-02 01:20:26 +01:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
1f3beabbdf chore(deps-dev): bump eslint from 8.54.0 to 8.55.0 (#620)
Bumps [eslint](https://github.com/eslint/eslint) from 8.54.0 to 8.55.0.
- [Release notes](https://github.com/eslint/eslint/releases)
- [Changelog](https://github.com/eslint/eslint/blob/main/CHANGELOG.md)
- [Commits](https://github.com/eslint/eslint/compare/v8.54.0...v8.55.0)

---
updated-dependencies:
- dependency-name: eslint
  dependency-type: direct:development
  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>
2023-12-01 23:11:44 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2353c90cc1 chore(deps): bump the patches group with 3 updates (#619)
Bumps the patches group with 3 updates: [@react-email/components](https://github.com/resendlabs/react-email/tree/HEAD/packages/components), [@react-email/render](https://github.com/resendlabs/react-email/tree/HEAD/packages/render) and [@types/react](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react).


Updates `@react-email/components` from 0.0.11 to 0.0.12
- [Release notes](https://github.com/resendlabs/react-email/releases)
- [Changelog](https://github.com/resendlabs/react-email/blob/canary/docs/changelog.mdx)
- [Commits](https://github.com/resendlabs/react-email/commits/v0.0.12/packages/components)

Updates `@react-email/render` from 0.0.9 to 0.0.10
- [Release notes](https://github.com/resendlabs/react-email/releases)
- [Changelog](https://github.com/resendlabs/react-email/blob/canary/docs/changelog.mdx)
- [Commits](https://github.com/resendlabs/react-email/commits/HEAD/packages/render)

Updates `@types/react` from 18.2.39 to 18.2.40
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react)

---
updated-dependencies:
- dependency-name: "@react-email/components"
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: patches
- dependency-name: "@react-email/render"
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: patches
- dependency-name: "@types/react"
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: patches
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-02 00:07:41 +01:00
Marc Klingen df6c0cfa8a chore: release v1.11.0
CI/CD / push-docker-image (push) Blocked by required conditions
CI/CD / tests (18) (push) Failing after 1m17s
CI/CD / tests (20) (push) Failing after 1m7s
CI/CD / e2e-tests (push) Failing after 57s
CI/CD / lint (push) Successful in 4m21s
CI/CD / all-ci-passed (push) Waiting to run
release.yml / release (push) Failing after 4s
2023-12-01 23:48:00 +01:00
Marc KlingenandGitHub 2e8a9062f4 feat: improved rbac error message (#618) 2023-12-01 22:22:50 +00:00
7cc52b3c8e feat(ui): keep logo static on large screens while scrolling the main menu (#571)
---------

Co-authored-by: Marc Klingen <git@marcklingen.com>
2023-12-01 22:45:23 +01:00
Marc KlingenandGitHub 92d2def19f docs: update CONTRIBUTING.md 2023-12-01 21:16:46 +01:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
3cd7f6b3a9 chore(deps-dev): bump @adobe/css-tools from 4.3.1 to 4.3.2 (#614)
Bumps [@adobe/css-tools](https://github.com/adobe/css-tools) from 4.3.1 to 4.3.2.
- [Changelog](https://github.com/adobe/css-tools/blob/main/History.md)
- [Commits](https://github.com/adobe/css-tools/commits)

---
updated-dependencies:
- dependency-name: "@adobe/css-tools"
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-01 19:54:22 +00:00
Marc KlingenandGitHub 65c4fbf9e8 fix: improved state during trace deletion (#615) 2023-12-01 17:24:28 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
5c5accf4f8 chore(deps-dev): bump the patches group with 2 updates (#609)
Bumps the patches group with 2 updates: [@testing-library/jest-dom](https://github.com/testing-library/jest-dom) and [@types/eslint](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/eslint).


Updates `@testing-library/jest-dom` from 6.1.4 to 6.1.5
- [Release notes](https://github.com/testing-library/jest-dom/releases)
- [Changelog](https://github.com/testing-library/jest-dom/blob/main/CHANGELOG.md)
- [Commits](https://github.com/testing-library/jest-dom/compare/v6.1.4...v6.1.5)

Updates `@types/eslint` from 8.44.7 to 8.44.8
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/eslint)

---
updated-dependencies:
- dependency-name: "@testing-library/jest-dom"
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: patches
- dependency-name: "@types/eslint"
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: patches
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-01 17:11:36 +00:00
5bfb02b48a feat(auth): Add access to default project upon sign-up (#611)
* Add access to default project upon sign-up

* refactor: createProjectMembershipsOnSignup

---------

Co-authored-by: Marc Klingen <git@marcklingen.com>
2023-12-01 18:00:48 +01:00
Marc KlingenandGitHub e0650469c8 chore: remove next-swagger-doc (#612) 2023-12-01 01:54:50 +00:00
b20d4da827 feat: add html email template for project invitations (#601)
---------

Co-authored-by: Marc Klingen <git@marcklingen.com>
2023-12-01 02:14:20 +01:00
Marc Klingen 029577bb0d chore: release v1.10.0
CI/CD / push-docker-image (push) Blocked by required conditions
CI/CD / tests (20) (push) Failing after 56s
CI/CD / tests (18) (push) Failing after 1m9s
CI/CD / e2e-tests (push) Failing after 56s
release.yml / release (push) Failing after 2s
CI/CD / lint (push) Successful in 4m30s
CI/CD / all-ci-passed (push) Waiting to run
2023-11-30 23:48:42 +01:00
Marc KlingenandGitHub 86851322c9 feat(api): upsert and get dataset items (#606) 2023-11-30 22:44:50 +00:00
Marc KlingenandGitHub cd665d8fd5 fix(ui): extend checks to not trigger keyboard shortcuts when editing inputs (#607) 2023-11-30 21:26:12 +00:00
Marc Klingen f547fd710c Revert "chore(deps): bump sentry"
This reverts commit da7699fa68.
2023-11-30 22:21:34 +01:00
Marc Klingen da7699fa68 chore(deps): bump sentry 2023-11-30 22:17:26 +01:00
Max Deichmann b4a4f5683e chore: release v1.9.2
CI/CD / push-docker-image (push) Blocked by required conditions
CI/CD / tests (18) (push) Failing after 50s
CI/CD / tests (20) (push) Failing after 38s
CI/CD / e2e-tests (push) Failing after 1m5s
CI/CD / lint (push) Successful in 4m1s
CI/CD / all-ci-passed (push) Waiting to run
release.yml / release (push) Failing after 2s
2023-11-30 13:51:28 +01:00
Max DeichmannandGitHub 6ed893de18 fix(core): add start time to patches of ingestion events (#605) 2023-11-30 13:50:33 +01:00
Max Deichmann 92e834e490 chore: release v1.9.1
CI/CD / push-docker-image (push) Blocked by required conditions
CI/CD / tests (20) (push) Failing after 36s
CI/CD / tests (18) (push) Failing after 50s
CI/CD / e2e-tests (push) Failing after 36s
CI/CD / lint (push) Successful in 3m56s
CI/CD / all-ci-passed (push) Waiting to run
release.yml / release (push) Failing after 3s
2023-11-30 11:48:01 +01:00
Max DeichmannandGitHub ed9d647d31 fix(core): add logging to trace router (#603) 2023-11-30 10:41:49 +00:00
Marc Klingen 58f807b7de feat(cloud): demo project link to q&a chatbot 2023-11-29 13:07:19 +01:00
Veniamin ViflyantsevandGitHub 2a623ba65b fix: NEXTAUTH_URL environment variable in docker compose files (#596) 2023-11-29 11:48:08 +01:00
Marc Klingen de7919b7ec fix: auth url on Langfuse Cloud (staging) 2023-11-29 04:15:33 +01:00
Marc Klingen 88f96a37c1 fix: auth url on Langfuse Cloud 2023-11-29 04:12:57 +01:00
Marc Klingen e4f58de9ba chore: release v1.9.0
CI/CD / push-docker-image (push) Blocked by required conditions
CI/CD / tests (18) (push) Failing after 48s
CI/CD / e2e-tests (push) Failing after 35s
CI/CD / tests (20) (push) Failing after 42s
CI/CD / lint (push) Successful in 3m57s
CI/CD / all-ci-passed (push) Waiting to run
release.yml / release (push) Failing after 3s
2023-11-29 03:50:43 +01:00
Marc Klingen c00c1833dd chore(deps): bump 2023-11-29 03:47:46 +01:00
Marc Klingen 8657c61bb8 fix: remove user invite alert 2023-11-29 03:33:15 +01:00
144aada5ec feat(auth): invite users to a project that do not have an account yet (#574)
---------

Co-authored-by: Marc Klingen <git@marcklingen.com>
2023-11-29 03:17:25 +01:00
Marc Klingen 7e6ce12ca8 chore: release v1.8.2
CI/CD / push-docker-image (push) Blocked by required conditions
CI/CD / tests (18) (push) Failing after 36s
CI/CD / tests (20) (push) Failing after 34s
CI/CD / e2e-tests (push) Failing after 39s
CI/CD / lint (push) Successful in 4m18s
CI/CD / all-ci-passed (push) Waiting to run
release.yml / release (push) Failing after 3s
2023-11-29 01:48:35 +01:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
951b89c08a chore(deps): bump the patches group with 3 updates (#590)
Bumps the patches group with 3 updates: [posthog-js](https://github.com/PostHog/posthog-js), [@playwright/test](https://github.com/microsoft/playwright) and [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin).


Updates `posthog-js` from 1.93.2 to 1.93.3
- [Release notes](https://github.com/PostHog/posthog-js/releases)
- [Changelog](https://github.com/PostHog/posthog-js/blob/master/CHANGELOG.md)
- [Commits](https://github.com/PostHog/posthog-js/compare/v1.93.2...v1.93.3)

Updates `@playwright/test` from 1.40.0 to 1.40.1
- [Release notes](https://github.com/microsoft/playwright/releases)
- [Commits](https://github.com/microsoft/playwright/compare/v1.40.0...v1.40.1)

Updates `@typescript-eslint/eslint-plugin` from 6.13.0 to 6.13.1
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v6.13.1/packages/eslint-plugin)

---
updated-dependencies:
- dependency-name: posthog-js
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: patches
- dependency-name: "@playwright/test"
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: patches
- dependency-name: "@typescript-eslint/eslint-plugin"
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: patches
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-11-29 00:34:57 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
70f9f3b50a chore(deps-dev): bump @typescript-eslint/parser from 6.12.0 to 6.13.1 (#592)
Bumps [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) from 6.12.0 to 6.13.1.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/parser/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v6.13.1/packages/parser)

---
updated-dependencies:
- dependency-name: "@typescript-eslint/parser"
  dependency-type: direct:development
  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>
2023-11-29 00:25:54 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2a90e548a6 chore(deps): bump @sentry/nextjs from 7.82.0 to 7.83.0 (#593)
Bumps [@sentry/nextjs](https://github.com/getsentry/sentry-javascript) from 7.82.0 to 7.83.0.
- [Release notes](https://github.com/getsentry/sentry-javascript/releases)
- [Changelog](https://github.com/getsentry/sentry-javascript/blob/develop/CHANGELOG.md)
- [Commits](https://github.com/getsentry/sentry-javascript/compare/7.82.0...7.83.0)

---
updated-dependencies:
- dependency-name: "@sentry/nextjs"
  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>
2023-11-29 00:21:31 +00:00
Marc Klingen 7797982b52 fix(ui): dom nesting of DocPopup 2023-11-29 01:16:55 +01:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
9858c139bc chore(deps): bump lucide-react from 0.293.0 to 0.294.0 (#591)
Bumps [lucide-react](https://github.com/lucide-icons/lucide/tree/HEAD/packages/lucide-react) from 0.293.0 to 0.294.0.
- [Release notes](https://github.com/lucide-icons/lucide/releases)
- [Commits](https://github.com/lucide-icons/lucide/commits/0.294.0/packages/lucide-react)

---
updated-dependencies:
- dependency-name: lucide-react
  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>
2023-11-29 00:49:48 +01:00
Marc Klingen 05472524f3 docs: add note that staging env is sometimes reset 2023-11-29 00:01:17 +01:00
Marc Klingen 27e5eb5feb refactor: simplify DocPopup 2023-11-28 23:54:38 +01:00
Marc KlingenandGitHub 03062c3fd5 feat(ui): project name in breadcrumb (#589) 2023-11-28 22:42:02 +00:00
Marc Klingen fe40d8b139 chore: release v1.8.1
CI/CD / push-docker-image (push) Blocked by required conditions
CI/CD / tests (18) (push) Failing after 41s
CI/CD / tests (20) (push) Failing after 36s
CI/CD / e2e-tests (push) Failing after 37s
CI/CD / lint (push) Successful in 3m50s
CI/CD / all-ci-passed (push) Waiting to run
release.yml / release (push) Failing after 4s
2023-11-28 03:47:00 +01:00
Marc Klingen 81e9ac0cea feat: debug log for auth errors 2023-11-28 03:46:24 +01:00
Marc Klingen 9bb4a55a4c chore: release v1.8.0
CI/CD / push-docker-image (push) Blocked by required conditions
CI/CD / tests (18) (push) Failing after 44s
CI/CD / e2e-tests (push) Failing after 37s
CI/CD / tests (20) (push) Failing after 1m10s
CI/CD / lint (push) Successful in 3m51s
CI/CD / all-ci-passed (push) Waiting to run
release.yml / release (push) Failing after 2s
2023-11-28 02:54:32 +01:00
Marc KlingenandGitHub f4e7d5f3e9 feat: cache bookmark state in component (#586) 2023-11-28 01:52:07 +00:00
Marc Klingen cc0c0c9246 fix: hide demo banner on staging 2023-11-28 02:26:34 +01:00
93c080813e feat(ui): bookmark traces (#579)
---------

Co-authored-by: Marc Klingen <git@marcklingen.com>
2023-11-28 02:00:12 +01:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
926a5195c6 chore(deps): bump next-query-params from 4.3.0 to 5.0.0 (#583)
Bumps [next-query-params](https://github.com/amannn/next-query-params) from 4.3.0 to 5.0.0.
- [Changelog](https://github.com/amannn/next-query-params/blob/main/CHANGELOG.md)
- [Commits](https://github.com/amannn/next-query-params/compare/v4.3.0...v5.0.0)

---
updated-dependencies:
- dependency-name: next-query-params
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-11-27 23:54:21 +00:00
Marc KlingenandGitHub 4b8253f452 chore: remove dependabot-merge workflow 2023-11-28 00:50:32 +01:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
fe3e8eea5f chore(deps): bump @sentry/nextjs from 7.81.1 to 7.82.0 (#584)
Bumps [@sentry/nextjs](https://github.com/getsentry/sentry-javascript) from 7.81.1 to 7.82.0.
- [Release notes](https://github.com/getsentry/sentry-javascript/releases)
- [Changelog](https://github.com/getsentry/sentry-javascript/blob/develop/CHANGELOG.md)
- [Commits](https://github.com/getsentry/sentry-javascript/compare/7.81.1...7.82.0)

---
updated-dependencies:
- dependency-name: "@sentry/nextjs"
  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>
2023-11-27 23:46:37 +00:00
Marc KlingenandGitHub 26cc72488b refactor: use protectedProjectProcedure on most routes (#585) 2023-11-27 23:43:00 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
0e53210cd4 chore(deps): bump lucide-react from 0.292.0 to 0.293.0 (#570)
Bumps [lucide-react](https://github.com/lucide-icons/lucide/tree/HEAD/packages/lucide-react) from 0.292.0 to 0.293.0.
- [Release notes](https://github.com/lucide-icons/lucide/releases)
- [Commits](https://github.com/lucide-icons/lucide/commits/0.293.0/packages/lucide-react)

---
updated-dependencies:
- dependency-name: lucide-react
  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>
2023-11-27 23:39:58 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
47597077f6 chore(deps-dev): bump @typescript-eslint/eslint-plugin from 6.12.0 to 6.13.0 (#582)
chore(deps-dev): bump @typescript-eslint/eslint-plugin

Bumps [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) from 6.12.0 to 6.13.0.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v6.13.0/packages/eslint-plugin)

---
updated-dependencies:
- dependency-name: "@typescript-eslint/eslint-plugin"
  dependency-type: direct:development
  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>
2023-11-27 23:36:19 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
c21786c647 chore(deps-dev): bump the patches group with 1 update (#581)
Bumps the patches group with 1 update: [@types/react](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react).

- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react)

---
updated-dependencies:
- dependency-name: "@types/react"
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: patches
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-11-28 00:32:19 +01:00
Marc Klingen b579ac547c docs: typo 2023-11-27 20:28:43 +01:00
Marc Klingen ce0d1bfb0a docs: production environment to contributing.md 2023-11-27 20:21:02 +01:00
18491f3eec chore: license check for dependencies in GH action (#575)
---------

Co-authored-by: Marc Klingen <git@marcklingen.com>
2023-11-26 16:35:42 +01:00
Marc Klingen cb394a1bd9 chore: release v1.7.2
CI/CD / push-docker-image (push) Blocked by required conditions
CI/CD / tests (20) (push) Failing after 36s
CI/CD / tests (18) (push) Failing after 1m1s
CI/CD / e2e-tests (push) Failing after 39s
release.yml / release (push) Failing after 15s
CI/CD / lint (push) Successful in 4m39s
CI/CD / all-ci-passed (push) Waiting to run
2023-11-26 05:37:59 +01:00
Marc Klingen 41b44d6c09 fix: ci to release on semver bump 2023-11-26 05:37:42 +01:00
Marc Klingen 678dbaf3d0 fix: github access token 2023-11-26 05:33:42 +01:00
Marc Klingen 0b1b0aca11 chore: release v1.7.1
CI/CD / push-docker-image (push) Blocked by required conditions
CI/CD / tests (18) (push) Failing after 42s
CI/CD / tests (20) (push) Failing after 43s
CI/CD / lint (push) Successful in 3m49s
release.yml / release (push) Failing after 4s
CI/CD / e2e-tests (push) Failing after 15m49s
CI/CD / all-ci-passed (push) Waiting to run
2023-11-26 05:29:05 +01:00
Marc Klingen d5c117cf5d feat: support for staging environment 2023-11-26 05:11:47 +01:00
Marc KlingenandGitHub c380d16feb docs: staging environment in CONTRIBUTING.md (#576) 2023-11-26 04:58:09 +01:00
Marc Klingen 6b03507ca0 ci: use "protected branches" environment 2023-11-26 04:44:28 +01:00
Marc Klingen ff8a8b8ee6 ci: push 2023-11-26 04:32:51 +01:00
Marc Klingen d3981125a1 ci: next try 2023-11-26 04:15:48 +01:00
Marc Klingen b4965d386e ci: try GITHUB_TOKEN 2023-11-26 04:09:08 +01:00
Marc Klingen bda28bd385 ci: release tagged versions to production branch 2023-11-26 04:00:34 +01:00
Marc Klingen edfb90c6c7 feat(compliance): data accuracy notice on sign up 2023-11-25 20:23:27 +01:00
Marc Klingen a1e3762b9d docs: add CI/CD to CONTRIBUTING.md 2023-11-25 16:04:18 +01:00
Marc Klingen ea54da12ee fix(ci): wait for postgres to accept connections in e2e 2023-11-25 15:27:43 +01:00
Marc Klingen d552d61cfd fix(ci): wait for postgres to accept connections 2023-11-25 15:25:29 +01:00
Marc Klingen 6332418cb1 docs: CONTRIBUTING.md 2023-11-25 15:13:29 +01:00
Marc KlingenandGitHub 824c37c227 ci: add matrix tests for node 18 & 20 (#573)
Node 20 is curently in beta for serverless functions on Vercel (AWS Lambda)
2023-11-25 13:58:35 +00:00
1c116282d7 chore: upgrade to Node 20 (#553)
---------

Co-authored-by: Marc Klingen <git@marcklingen.com>
2023-11-25 14:49:42 +01:00
236 changed files with 13276 additions and 5086 deletions
+5 -1
View File
@@ -16,4 +16,8 @@ NEXTAUTH_SECRET="secret"
# Langfuse experimental features
LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES="true"
SALT="salt"
SALT="salt"
# Email
EMAIL_FROM_ADDRESS="" # Defines the email address to use as the from address.
SMTP_CONNECTION_URL="" # Defines the connection url for smtp server.
+6 -1
View File
@@ -13,4 +13,9 @@ NEXTAUTH_URL="http://localhost:3000"
# feature flag to enable experimental features locally
LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES="false"
SALT="salt"
SALT="salt"
# Email
EMAIL_FROM_ADDRESS="" # Defines the email address to use as the from address.
SMTP_CONNECTION_URL="" # Defines the connection url for smtp server.
+12
View File
@@ -7,6 +7,7 @@ NEXTAUTH_COOKIE_DOMAIN=".langfuse.com"
# https://www.prisma.io/docs/reference/database-reference/connection-urls#env
DIRECT_URL="postgresql://postgres:[pw]@db.[db_id].supabase.co:5432/postgres"
DATABASE_URL="postgres://postgres:[pw]@db.[db_id].supabase.co:6543/postgres?pgbouncer=true&connection_limit=1"
# SHADOW_DATABASE_URL=
# Next Auth
@@ -44,3 +45,14 @@ AUTH_GITHUB_CLIENT_ID=
AUTH_GITHUB_CLIENT_SECRET=
# AUTH_DOMAINS_WITH_SSO_ENFORCEMENT=domain1.com,domain2.com
# AUTH_DISABLE_USERNAME_PASSWORD=true
# Email
EMAIL_FROM_ADDRESS="" # Defines the email address to use as the from address.
SMTP_CONNECTION_URL="" # Defines the connection url for smtp server.
# S3 storage (eg for exports), necessary for serverless deployments
S3_ENDPOINT=""
S3_ACCESS_KEY_ID=""
S3_SECRET_ACCESS_KEY=""
S3_BUCKET_NAME=""
S3_REGION=""
+8 -2
View File
@@ -6,12 +6,17 @@ const config = {
overrides: [
{
extends: [
"plugin:@typescript-eslint/recommended-requiring-type-checking",
"plugin:@typescript-eslint/recommended",
"plugin:@typescript-eslint/strict-type-checked",
],
files: ["*.ts", "*.tsx"],
parserOptions: {
project: path.join(__dirname, "tsconfig.json"),
},
rules: {
"@typescript-eslint/no-non-null-assertion": "off",
"@typescript-eslint/no-confusing-void-expression": "off",
}
},
],
parser: "@typescript-eslint/parser",
@@ -19,7 +24,8 @@ const config = {
project: path.join(__dirname, "tsconfig.json"),
},
plugins: ["@typescript-eslint"],
extends: ["next/core-web-vitals", "plugin:@typescript-eslint/recommended"],
extends: ["next/core-web-vitals"],
ignorePatterns: ["generated/**/*"],
rules: {
"@typescript-eslint/consistent-type-imports": [
"warn",
+1 -1
View File
@@ -27,7 +27,7 @@ Fixes # (issue)
<!-- Remove bullet points below that don't apply to you -->
- I haven't read the [contributing guide](https://github.com/calcom/cal.com/blob/main/CONTRIBUTING.md)
- I haven't read the [contributing guide](https://github.com/langfuse/langfuse/blob/main/CONTRIBUTING.md)
- My code doesn't follow the style guidelines of this project (`npm run prettier`)
- I haven't commented my code, particularly in hard-to-understand areas
- I haven't checked if my PR needs changes to the documentation
+3
View File
@@ -18,6 +18,9 @@ updates:
- dependency-name: "@types/node"
- dependency-name: "@trpc/*"
groups:
sentry:
patterns:
- "@sentry/*"
prisma:
patterns:
- "prisma"
-26
View File
@@ -1,26 +0,0 @@
name: Dependabot auto-merge
on:
workflow_dispatch:
pull_request:
branches: [main]
permissions:
contents: write
pull-requests: write
jobs:
dependabot:
runs-on: ubuntu-latest
if: ${{ github.actor == 'dependabot[bot]' }}
steps:
- name: Dependabot metadata
id: metadata
uses: dependabot/fetch-metadata@v1
with:
github-token: "${{ secrets.GITHUB_TOKEN }}"
- name: Enable auto-merge for Dependabot PRs
if: ${{steps.metadata.outputs.update-type == 'version-update:semver-patch'}}
run: gh pr merge --auto --squash "$PR_URL"
env:
PR_URL: ${{github.event.pull_request.html_url}}
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
@@ -9,9 +9,10 @@ on:
jobs:
rebase-dependabot:
runs-on: ubuntu-latest
environment: "protected branches"
steps:
- name: "Rebase open Dependabot PR"
uses: orange-buffalo/dependabot-auto-rebase@v1
with:
api-token: ${{ secrets.DEP_REBASE_PAT }}
api-token: ${{ secrets.GH_ACCESS_TOKEN }}
repository: ${{ github.repository }}
+49
View File
@@ -0,0 +1,49 @@
name: License Compliance Check
on:
workflow_dispatch:
push:
branches:
- "main"
merge_group:
pull_request:
branches:
- "main"
jobs:
license_check:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Setup node
uses: actions/setup-node@v2
with:
node-version: 18
- name: Install license-checker
run: npm install -g license-checker
- name: Install yui-lint
run: npm install yui-lint
- name: Generate license-checker CSV file
run: license-checker --production --csv > npm-license-checker.csv
- name: Check license-checker CSV file without headers
id: license_check_report
uses: pilosus/action-pip-license-checker@v2
with:
external: "npm-license-checker.csv"
external-format: "csv"
external-options: "{:skip-header true}"
fail: "WeakCopyleft,StrongCopyleft,NetworkCopyleft"
fails-only: true
totals: true
verbose: 1
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Echo error
if: failure()
run: echo "::error::${{ steps.license_check_report.outputs.report }}"
- name: Delete license-checker CSV file
run: rm npm-license-checker.csv
+13 -5
View File
@@ -7,6 +7,7 @@ on:
- "main"
tags:
- "v*"
merge_group:
pull_request:
branches:
- "main"
@@ -18,7 +19,7 @@ jobs:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 18
node-version: 20
cache: "npm"
- name: install dependencies
run: |
@@ -31,11 +32,15 @@ jobs:
tests:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [18, 20]
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with:
node-version: 18
node-version: ${{ matrix.node-version }}
cache: "npm"
- name: install dependencies
@@ -49,6 +54,7 @@ jobs:
- name: Run, migrate, seed DB
run: |
docker-compose -f docker-compose.dev.yml up -d
sleep 5 # Wait for PostgreSQL to accept connections
npx --yes prisma migrate reset --force --skip-generate
- name: Build
@@ -66,7 +72,7 @@ jobs:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 18
node-version: 20
cache: "npm"
- name: install dependencies
@@ -80,6 +86,7 @@ jobs:
- name: Run, migrate, seed DB
run: |
docker-compose -f docker-compose.dev.yml up -d
sleep 5 # Wait for PostgreSQL to accept connections
npx --yes prisma migrate reset --force --skip-generate
- name: Build
@@ -107,6 +114,7 @@ jobs:
push-docker-image:
needs: all-ci-passed
if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/'))
environment: "protected branches"
runs-on: ubuntu-latest
env:
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/postgres
@@ -123,7 +131,7 @@ jobs:
- name: Setup node
uses: actions/setup-node@v3
with:
node-version: 18
node-version: 20
- name: Checkout
uses: actions/checkout@v3
+19
View File
@@ -0,0 +1,19 @@
on:
workflow_dispatch:
push:
# Pattern matched against refs/tags
tags:
- "v[0-9]+.[0-9]+.[0-9]+" # Semantic version tags
jobs:
release:
runs-on: ubuntu-latest
environment: "protected branches"
steps:
- uses: actions/checkout@v4
with:
ref: main # Always checkout main even for tagged releases
fetch-depth: 0
token: ${{ secrets.GH_ACCESS_TOKEN }}
- name: Push to production
run: git push origin +main:production
+3
View File
@@ -20,6 +20,9 @@ next-env.d.ts
# production
/build
# idea
.idea
# misc
.DS_Store
*.pem
+1 -1
View File
@@ -1 +1 @@
v18.18
v20
+3 -2
View File
@@ -1,6 +1,6 @@
{
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
"source.fixAll.eslint": "explicit"
},
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true,
@@ -28,5 +28,6 @@
"docwriter.style": "JSDoc",
"[prisma]": {
"editor.defaultFormatter": "Prisma.prisma"
}
},
"eslint.lintTask.enable": true
}
+163 -35
View File
@@ -5,67 +5,195 @@ First off, thanks for taking the time to contribute! ❤️
Langfuse is an open-source observability and analytics solution for LLM-based applications. We welcome contributions through GitHub pull requests. This document outlines our conventions regarding development workflow, commit message formatting, contact points, and other resources. Our goal is to simplify the process and ensure that your contributions are easily accepted.
We gratefully welcome improvements to documentation as well as to code.
<!-- The community looks forward to your contributions. 🎉 -->
The maintainers are available on [Discord](https://langfuse.com/discord) in case you have any questions.
> And if you like the project, but just don't have time to contribute, that's fine. There are other easy ways to support the project and show your appreciation, which we would also be very happy about:
>
> - Star the project;
> - Tweet about it;
> - Refer to this project in your project's readme;
> - Mention the project at local meetups and tell your friends/colleagues.
The maintainers are available on [Discord](https://langfuse.com/discord) in case you have any questions.
# How to contribute to Langfuse
## Making a change
_Before making any significant changes, please [open an issue](https://github.com/langfuse/langfuse/issues)._ Discussing your proposed changes ahead of time will make the contribution process smooth for everyone.
Once we've discussed your changes and you've got your code ready, make sure that tests are passing and open your pull request.
## Getting started
A good first step is to search for open [issues](https://github.com/langfuse/langfuse/issues). Issues are labeled, and some good issues to start with are labeled: [good first issue](https://github.com/langfuse/langfuse/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22).
## Setup
## Project Overview
First, the preferred version for development is Node.js >=18. Additionally, you should have npm and Docker installed
### Technologies we use
Then, to begin development fork repository and go to root folder by run `cd ./langfuse` .
- Application (this repository)
- NextJS 14, pages router
- NextAuth.js / Auth.js
- tRPC: Frontend APIs
- Prisma ORM
- Zod
- Tailwind CSS
- shadcn/ui tailwind components (using Radix and tanstack)
- Fern: generate OpenAPI spec and Pydantic models
- JS SDK ([langfuse/langfuse-js](https://github.com/langfuse/langfuse-js))
- openapi-typescript to generated types based on OpenAPI spec
- Python SDK ([langfuse/langfuse-python](https://github.com/langfuse/langfuse-python))
- Pydantic for input validation, models generated by fern
```bash
# Install dependencies
npm install
### Architecture Overview
# Run the db
docker-compose -f docker-compose.dev.yml up -d
```mermaid
flowchart TB
subgraph s4["Clients"]
subgraph s2["langfuse/langfuse-python"]
Python["Python SDK"]
OAI["OpenAI drop-in replacement"] -->|extends| Python
LCPYTHON["Langchain Python Integration"] -->|extends| Python
Langflow -->|uses| LCPYTHON
LiteLLM -->|uses| Python
end
subgraph s3["langfuse/langfuse-js"]
JS["JS SDK"]
LCJS["Langchain JS Integration"] -->|extends| JS
Flowise -->|uses| LCJS
end
end
# create an env file
cp .env.dev.example .env
# Migration
npm run db:migrate
# Optional: seed the database
# npm run db:seed
# npm run db:seed:examples
# Start the server
npm run dev
DB[Postgres Database]
subgraph s1["Application (langfuse/langfuse)"]
API[Public HTTP API]
G[TRPC API]
I[NextAuth]
H[React Frontend]
Prisma[Prisma ORM]
H --> G
H --> I
G --> I
G --- Prisma
API --- Prisma
I --- Prisma
end
Prisma --- DB
JS --- API
Python --- API
```
### Infrastructure & Network Overview
```mermaid
flowchart LR
Browser ---|Web UI & TRPC API| App
Integrations/SDKs ---|Public HTTP API| App
subgraph i1["Application Network"]
App["Langfuse Application (Docker or Serverless)"]
end
subgraph i2["Database Network"]
DB["Postgres Database"]
end
App --- DB
```
## Development Setup
Requirements
- Node.js 20 as specified in the [.nvmrc](.nvmrc)
- Docker to run the database locally
**Steps**
1. Fork the the repository and clone it locally
2. Install dependencies
```bash
npm install
```
3. Run the development database
```bash
docker-compose -f docker-compose.dev.yml up -d
```
4. Create an env file
```bash
cp .env.dev.example .env
```
5. Run the migrations
```bash
npm run db:migrate
# Optional: seed the database
# npm run db:seed
# npm run db:seed:examples
```
6. Start the development server
```bash
npm run dev
```
## Commit messages
On the main branch, we adhere to the best practices of [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/). All pull requests and branches are squash-merged to maintain a clean and readable history. This approach ensures the addition of a conventional commit message when merging contributions.
On the main branch, we adhere to the best practices of [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/). All pull requests and branches are squash-merged to maintain a clean and readable history. This approach ensures the addition of a conventional commit message when merging contributions.
## Test the public API
The API is tested using Jest. With the development server running, you can run the tests with:
Run all
```bash
npm run test
```
Run interactively in watch mode
```bash
npm run test:watch
```
These tests are also run in CI.
## CI/CD
We use GitHub Actions for CI/CD, the configuration is in [`.github/workflows/pipeline.yml`](.github/workflows/pipeline.yml)
CI on `main` and `pull_request`
- Check Linting
- E2E test of API using Jest
- E2E tests of UI using Playwright
CD on `main`
- Publish Docker image to GitHub Packages if CI passes. Done on every push to `main` branch. Only released versions are tagged with `latest`.
## Staging environment
We run a staging environment at [https://staging.langfuse.com](https://staging.langfuse.com) that is automatically deployed on every push to `main` branch.
The same environment is also used for preview deployments of pull requests. Limitations:
- SSO is not available as dynamic domains are not supported by most SSO providers.
- When making changes to the database, migrations to the staging database need to be applied manually by a maintainer. If you want to interactively test database changes in the staging environment, please reach out.
You can use the staging environment end-to-end with the Langfuse integrations or SDKs (host: `https://staging.langfuse.com`). However, please note that the staging environment is not intended for production use and may be reset at any time.
## Production environment
When a new release is tagged on the `main` branch (excluding prereleases), it triggers a production deployment. The deployment process consists of two steps:
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.
## License
Langfuse is MIT licensed, except for `ee/` folder. See [LICENSE](LICENSE) and [docs](https://langfuse.com/docs/open-source) for more details.
When contributing to the Langfuse codebase, you need to agree to the [Contributor License Agreement](https://cla-assistant.io/langfuse/langfuse). You only need to do this once and the CLA bot will remind you if you haven't signed it yet.
+1 -1
View File
@@ -1,5 +1,5 @@
# Base image
FROM node:18-alpine AS base
FROM node:20-alpine AS base
ARG DATABASE_URL
ARG NEXTAUTH_SECRET
ARG NEXTAUTH_URL
+1 -28
View File
@@ -218,34 +218,7 @@ To contribute, send us a PR, raise a GitHub issue, or email at contributing@lang
### Development setup
Requirements: Node.js >=18, npm, Docker
```bash
# Install dependencies
npm install
# Run the db
docker-compose -f docker-compose.dev.yml up -d
# create an env file
cp .env.dev.example .env
# Migration
npm run db:migrate
# Optional: seed the database
# npm run db:seed
# npm run db:seed:examples
# Start the server
npm run dev
```
Run tests
```bash
npm run test
```
See [CONTRIBUTING.md](CONTRIBUTING.md) for details on how to setup a development environment.
## License
+4
View File
@@ -0,0 +1,4 @@
## Security Policy
We strongly recommend using the latest version of Langfuse to receive all security updates.
For more information, please refer to the [Data Security & Privacy](https://langfuse.com/docs/data-security-privacy) page in the documentation or contact security@langfuse.com.
+2 -2
View File
@@ -8,7 +8,7 @@ services:
- DATABASE_URL=postgresql://postgres:postgres@db:5432/postgres
- NEXTAUTH_SECRET=mysecret
- SALT=mysalt
- NEXTAUTH_URL=http:localhost:3000
- NEXTAUTH_URL=http://localhost:3000
depends_on:
- db
ports:
@@ -18,7 +18,7 @@ services:
- DATABASE_URL=postgresql://postgres:postgres@db:5432/postgres
- NEXTAUTH_SECRET=mysecret
- SALT=mysalt
- NEXTAUTH_URL=http:localhost:3000
- NEXTAUTH_URL=http://localhost:3000
- TELEMETRY_ENABLED=${TELEMETRY_ENABLED:-true}
- NEXT_PUBLIC_SIGN_UP_DISABLED=${NEXT_PUBLIC_SIGN_UP_DISABLED:-false}
- LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES=${LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES:-false}
+1 -1
View File
@@ -12,7 +12,7 @@ services:
- DATABASE_URL=postgresql://postgres:postgres@db:5432/postgres
- NEXTAUTH_SECRET=mysecret
- SALT=mysalt
- NEXTAUTH_URL=http:localhost:3000
- NEXTAUTH_URL=http://localhost:3000
- TELEMETRY_ENABLED=${TELEMETRY_ENABLED:-true}
- NEXT_PUBLIC_SIGN_UP_DISABLED=${NEXT_PUBLIC_SIGN_UP_DISABLED:-false}
- LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES=${LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES:-false}
+17 -2
View File
@@ -1,7 +1,22 @@
#!/bin/sh
# Set DIRECT_URL to the value of DATABASE_URL, required for migrations
export DIRECT_URL=$DATABASE_URL
# Check if DATABASE_URL is not set
if [ -z "$DATABASE_URL" ]; then
# Check if all required variables are provided
if [ -n "$DATABASE_HOST" ] && [ -n "$DATABASE_USERNAME" ] && [ -n "$DATABASE_PASSWORD" ] && [ -n "$DATABASE_NAME" ]; then
# Construct DATABASE_URL from the provided variables
DATABASE_URL="postgresql://${DATABASE_USERNAME}:${DATABASE_PASSWORD}@${DATABASE_HOST}/${DATABASE_NAME}"
export DATABASE_URL
else
echo "Error: Required database environment variables are not set. Provide a postgres url for DATABASE_URL."
exit 1
fi
fi
# Set DIRECT_URL to the value of DATABASE_URL if it is not set, required for migrations
if [ -z "$DIRECT_URL" ]; then
export DIRECT_URL=$DATABASE_URL
fi
# Apply migrations
prisma migrate deploy
+6
View File
@@ -1,4 +1,10 @@
name: langfuse
docs: |
## Authentication
Authenticate with the API using Basic Auth, get API keys in the project settings:
- username: Langfuse Public Key
- password: Langfuse Secret Key
error-discrimination:
strategy: status-code
auth: basic
+27 -36
View File
@@ -1,32 +1,4 @@
types:
# Create Bodies
CreateEventRequest:
properties:
id: optional<string>
traceId: optional<string>
name: optional<string>
startTime: optional<datetime>
metadata: optional<unknown>
input: optional<unknown>
output: optional<unknown>
level: optional<ObservationLevel>
statusMessage: optional<string>
parentObservationId: optional<string>
version: optional<string>
CreateSpanRequest:
extends: CreateEventRequest
properties:
endTime: optional<datetime>
CreateGenerationRequest:
extends: CreateSpanRequest
properties:
completionStartTime: optional<datetime>
model: optional<string>
modelParameters: optional<map<string, MapValue>>
prompt: optional<unknown>
completion: optional<unknown>
usage: optional<LLMUsage>
# Objects
Trace:
properties:
@@ -35,10 +7,16 @@ types:
docs: The unique identifier of a trace
timestamp: datetime
name: optional<string>
input: optional<unknown>
output: optional<unknown>
sessionId: optional<string>
release: optional<string>
version: optional<string>
userId: optional<string>
metadata: optional<unknown>
public:
type: optional<boolean>
docs: Public traces are accessible via url without login
TraceWithDetails:
extends: Trace
properties:
@@ -53,6 +31,15 @@ types:
properties:
observations: list<Observation>
scores: list<Score>
Session:
properties:
id: string
createdAt: datetime
projectId: string
SessionWithTraces:
extends: Session
properties:
traces: list<Trace>
Observation:
properties:
id: string
@@ -68,12 +55,17 @@ types:
version: optional<string>
metadata: optional<unknown>
output: optional<unknown>
promptTokens: integer
completionTokens: integer
totalTokens: integer
usage: optional<Usage>
level: ObservationLevel
statusMessage: optional<string>
parentObservationId: optional<string>
promptId: optional<string>
Usage:
properties:
input: optional<integer>
output: optional<integer>
total: optional<integer>
unit: optional<ModelUsageUnit>
Score:
properties:
id: string
@@ -121,6 +113,10 @@ types:
datasetRunItems: list<DatasetRunItem>
# Utilities
ModelUsageUnit:
enum:
- CHARACTERS
- TOKENS
ObservationLevel:
enum:
- DEBUG
@@ -133,11 +129,6 @@ types:
- optional<string>
- optional<integer>
- optional<boolean>
LLMUsage:
properties:
promptTokens: optional<integer>
completionTokens: optional<integer>
totalTokens: optional<integer>
DatasetStatus:
enum:
- ACTIVE
+11 -1
View File
@@ -7,13 +7,23 @@ service:
endpoints:
create:
method: POST
docs: Create a dataset item
docs: Create a dataset item, upserts on id
path: /dataset-items
request: CreateDatasetItemRequest
response: commons.DatasetItem
get:
docs: Get a specific dataset item
method: GET
path: /dataset-items/{id}
path-parameters:
id:
type: string
response: commons.DatasetItem
types:
CreateDatasetItemRequest:
properties:
datasetName: string
input: unknown
expectedOutput: optional<unknown>
id: optional<string>
-13
View File
@@ -1,13 +0,0 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/fern-api/fern/main/fern.schema.json
imports:
commons: ./commons.yml
service:
auth: true
base-path: /api/public
endpoints:
create:
docs: Add an event to the database
method: POST
path: /events
request: commons.CreateEventRequest
response: commons.Observation
@@ -1,36 +0,0 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/fern-api/fern/main/fern.schema.json
imports:
commons: ./commons.yml
pagination: ./utils/pagination.yml
service:
auth: true
base-path: /api/public
endpoints:
log:
method: POST
path: /generations
request: commons.CreateGenerationRequest
response: commons.Observation
update:
method: PATCH
path: /generations
request: UpdateGenerationRequest
response: commons.Observation
types:
UpdateGenerationRequest:
properties:
generationId: string
traceId: optional<string>
name: optional<string>
endTime: optional<datetime>
completionStartTime: optional<datetime>
model: optional<string>
modelParameters: optional<map<string, commons.MapValue>>
prompt: optional<unknown>
version: optional<string>
metadata: optional<unknown>
completion: optional<unknown>
usage: optional<commons.LLMUsage>
level: optional<commons.ObservationLevel>
statusMessage: optional<string>
+13
View File
@@ -7,9 +7,22 @@ service:
docs: Check health of API and database
method: GET
path: /health
response: HealthResponse
errors:
- ServiceUnavailableError
types:
HealthResponse:
properties:
version:
type: string
docs: Langfuse server version
status: string
examples:
- value:
version: 1.25.0
status: OK
errors:
ServiceUnavailableError:
status-code: 503
+195 -16
View File
@@ -15,7 +15,7 @@ service:
body:
properties:
batch: list<IngestionEvent>
response: commons.Score
response: IngestionResponse # will be reportet as 200 response, but endpoint returns 207
types:
IngestionEvent:
@@ -23,26 +23,205 @@ types:
union:
trace-create: TraceEvent
score-create: ScoreEvent
observation-create: ObservationCreateEvent
observation-update: ObservationUpdateEvent
event-create: CreateEventEvent
generation-create: CreateGenerationEvent
generation-update: UpdateGenerationEvent
span-create: CreateSpanEvent
span-update: UpdateSpanEvent
sdk-log: SDKLogEvent
# both are legacy
observation-create: CreateObservationEvent
observation-update: UpdateObservationEvent
ObservationType:
enum:
- SPAN
- GENERATION
- EVENT
IngestionUsage:
discriminated: false
union:
- commons.Usage
- OpenAIUsage
OpenAIUsage:
properties:
promptTokens: optional<integer>
completionTokens: optional<integer>
totalTokens: optional<integer>
OptionalObservationBody:
properties:
traceId: optional<string>
name: optional<string>
startTime: optional<datetime>
metadata: optional<unknown>
input: optional<unknown>
output: optional<unknown>
level: optional<commons.ObservationLevel>
statusMessage: optional<string>
parentObservationId: optional<string>
version: optional<string>
CreateEventBody:
extends: OptionalObservationBody
properties:
id: optional<string>
UpdateEventBody:
extends: OptionalObservationBody
properties:
id: string
CreateSpanBody:
extends: CreateEventBody
properties:
endTime: optional<datetime>
UpdateSpanBody:
extends: UpdateEventBody
properties:
endTime: optional<datetime>
CreateGenerationBody:
extends: CreateSpanBody
properties:
completionStartTime: optional<datetime>
model: optional<string>
modelParameters: optional<map<string, commons.MapValue>>
usage: optional<IngestionUsage>
promptName: optional<string>
promptVersion: optional<integer>
UpdateGenerationBody:
extends: UpdateSpanBody
properties:
completionStartTime: optional<datetime>
model: optional<string>
modelParameters: optional<map<string, commons.MapValue>>
usage: optional<IngestionUsage>
promptName: optional<string>
promptVersion: optional<integer>
ObservationBody:
properties:
id: optional<string>
traceId: optional<string>
type: ObservationType
name: optional<string>
startTime: optional<datetime>
endTime: optional<datetime>
completionStartTime: optional<datetime>
model: optional<string>
modelParameters: optional<map<string, commons.MapValue>>
input: optional<unknown>
version: optional<string>
metadata: optional<unknown>
output: optional<unknown>
usage: optional<commons.Usage>
level: optional<commons.ObservationLevel>
statusMessage: optional<string>
parentObservationId: optional<string>
TraceBody:
properties:
id: optional<string>
name: optional<string>
userId: optional<string>
input: optional<unknown>
output: optional<unknown>
sessionId: optional<string>
release: optional<string>
version: optional<string>
metadata: optional<unknown>
public:
type: optional<boolean>
docs: Make trace publicly accessible via url
SDKLogBody:
properties:
log: unknown
ScoreBody:
properties:
id: optional<string>
traceId: string
name: string
value: double
observationId: optional<string>
comment: optional<string>
BaseEvent:
properties:
id: string
timestamp: string
metadata: unknown
TraceEvent:
extends: BaseEvent
properties:
id: string
timestamp: string
body: commons.Trace
ObservationCreateEvent:
body: TraceBody
CreateObservationEvent:
extends: BaseEvent
properties:
id: string
timestamp: string
body: commons.Observation
ObservationUpdateEvent:
body: ObservationBody
UpdateObservationEvent:
extends: BaseEvent
properties:
id: string
timestamp: string
body: commons.Observation
body: ObservationBody
ScoreEvent:
extends: BaseEvent
properties:
body: ScoreBody
SDKLogEvent:
extends: BaseEvent
properties:
body: SDKLogBody
CreateGenerationEvent:
extends: BaseEvent
properties:
body: CreateGenerationBody
UpdateGenerationEvent:
extends: BaseEvent
properties:
body: UpdateGenerationBody
CreateSpanEvent:
extends: BaseEvent
properties:
body: CreateSpanBody
UpdateSpanEvent:
extends: BaseEvent
properties:
body: UpdateSpanBody
CreateEventEvent:
extends: BaseEvent
properties:
body: CreateEventBody
IngestionSuccess:
properties:
id: string
timestamp: string
body: commons.Score
status: integer
IngestionError:
properties:
id: string
status: integer
message: optional<string>
error: optional<unknown>
IngestionResponse:
properties:
successes: list<IngestionSuccess>
errors: list<IngestionError>
+36
View File
@@ -0,0 +1,36 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/fern-api/fern/main/fern.schema.json
imports:
commons: ./commons.yml
pagination: ./utils/pagination.yml
service:
auth: true
base-path: /api/public
endpoints:
get:
docs: Get a specific prompt
method: GET
path: /prompts
request:
name: GetParameterRequest
query-parameters:
name: string
version: optional<integer>
response: Prompt
create:
docs: Create a specific prompt
method: POST
path: /prompts
request: CreatePromptRequest
response: Prompt
types:
CreatePromptRequest:
properties:
name: string
isActive: boolean
prompt: string
Prompt:
properties:
name: string
version: integer
prompt: string
+17
View File
@@ -0,0 +1,17 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/fern-api/fern/main/fern.schema.json
imports:
pagination: ./utils/pagination.yml
commons: ./commons.yml
service:
auth: true
base-path: /api/public
endpoints:
get:
docs: Get a session
method: GET
path: /sessions/{sessionId}
path-parameters:
sessionId:
type: string
docs: The unique id of a session
response: commons.SessionWithTraces
-33
View File
@@ -1,33 +0,0 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/fern-api/fern/main/fern.schema.json
imports:
commons: ./commons.yml
service:
auth: true
base-path: /api/public
endpoints:
create:
docs: Add a span to the database
method: POST
path: /spans
request: commons.CreateSpanRequest
response: commons.Observation
update:
docs: Update a span to the database
method: PATCH
path: /spans
request: UpdateSpanRequest
response: commons.Observation
types:
UpdateSpanRequest:
properties:
spanId: string
traceId: optional<string>
endTime: optional<datetime>
name: optional<string>
metadata: optional<unknown>
input: optional<unknown>
output: optional<unknown>
level: optional<commons.ObservationLevel>
version: optional<string>
statusMessage: optional<string>
-15
View File
@@ -6,12 +6,6 @@ service:
auth: true
base-path: /api/public
endpoints:
create:
docs: Add a trace to the database
method: POST
path: /traces
request: CreateTraceRequest
response: commons.Trace
get:
docs: Get a specific trace
method: GET
@@ -35,15 +29,6 @@ service:
response: Traces
types:
CreateTraceRequest:
properties:
id: optional<string>
name: optional<string>
userId: optional<string>
externalId: optional<string>
release: optional<string>
version: optional<string>
metadata: optional<unknown>
Traces:
properties:
data: list<commons.TraceWithDetails>
+4 -3
View File
@@ -11,13 +11,14 @@ groups:
namespaceExport: Langfuse
allowCustomFetcher: true
- name: fernapi/fern-python-sdk
version: 0.6.6
version: 0.7.3
output:
location: local-file-system
path: ../../../generated/python
config:
namespaceExport: Langfuse
allowCustomFetcher: true
client_class_name: FernLangfuse
pydantic_config:
require_optional_fields: false
- name: fernapi/fern-typescript-node-sdk
version: 0.7.1
output:
+1 -1
View File
@@ -1,4 +1,4 @@
{
"organization": "finto",
"version": "0.15.3"
"version": "0.16.7"
}
File diff suppressed because it is too large Load Diff
+127 -199
View File
@@ -2,7 +2,7 @@
"info": {
"name": "Langfuse",
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json",
"description": null
"description": "## Authentication\nAuthenticate with the API using Basic Auth, get API keys in the project settings:\n\n- username: Langfuse Public Key\n- password: Langfuse Secret Key\n"
},
"variable": [
{
@@ -46,7 +46,7 @@
"_type": "endpoint",
"name": "Create",
"request": {
"description": "Create a dataset item",
"description": "Create a dataset item, upserts on id",
"url": {
"raw": "{{baseUrl}}/api/public/dataset-items",
"host": [
@@ -65,7 +65,7 @@
"auth": null,
"body": {
"mode": "raw",
"raw": "{\n \"datasetName\": \"example\",\n \"input\": \"UNKNOWN\",\n \"expectedOutput\": \"UNKNOWN\"\n}",
"raw": "{\n \"datasetName\": \"example\",\n \"input\": \"UNKNOWN\",\n \"expectedOutput\": \"UNKNOWN\",\n \"id\": \"example\"\n}",
"options": {
"raw": {
"language": "json"
@@ -74,6 +74,38 @@
}
},
"response": []
},
{
"_type": "endpoint",
"name": "Get",
"request": {
"description": "Get a specific dataset item",
"url": {
"raw": "{{baseUrl}}/api/public/dataset-items/:id",
"host": [
"{{baseUrl}}"
],
"path": [
"api",
"public",
"dataset-items",
":id"
],
"query": [],
"variable": [
{
"key": "id",
"value": "",
"description": null
}
]
},
"header": [],
"method": "GET",
"auth": null,
"body": null
},
"response": []
}
]
},
@@ -228,119 +260,6 @@
}
]
},
{
"_type": "container",
"description": null,
"name": "Event",
"item": [
{
"_type": "endpoint",
"name": "Create",
"request": {
"description": "Add an event to the database",
"url": {
"raw": "{{baseUrl}}/api/public/events",
"host": [
"{{baseUrl}}"
],
"path": [
"api",
"public",
"events"
],
"query": [],
"variable": []
},
"header": [],
"method": "POST",
"auth": null,
"body": {
"mode": "raw",
"raw": "{\n \"id\": \"example\",\n \"traceId\": \"example\",\n \"name\": \"example\",\n \"startTime\": \"1994-11-05T13:15:30Z\",\n \"metadata\": \"UNKNOWN\",\n \"input\": \"UNKNOWN\",\n \"output\": \"UNKNOWN\",\n \"level\": \"DEBUG\",\n \"statusMessage\": \"example\",\n \"parentObservationId\": \"example\",\n \"version\": \"example\"\n}",
"options": {
"raw": {
"language": "json"
}
}
}
},
"response": []
}
]
},
{
"_type": "container",
"description": null,
"name": "Generations",
"item": [
{
"_type": "endpoint",
"name": "Log",
"request": {
"description": null,
"url": {
"raw": "{{baseUrl}}/api/public/generations",
"host": [
"{{baseUrl}}"
],
"path": [
"api",
"public",
"generations"
],
"query": [],
"variable": []
},
"header": [],
"method": "POST",
"auth": null,
"body": {
"mode": "raw",
"raw": "{\n \"completionStartTime\": \"1994-11-05T13:15:30Z\",\n \"model\": \"example\",\n \"modelParameters\": {\n \"example\": \"example\"\n },\n \"prompt\": \"UNKNOWN\",\n \"completion\": \"UNKNOWN\",\n \"usage\": {\n \"promptTokens\": 0,\n \"completionTokens\": 0,\n \"totalTokens\": 0\n },\n \"endTime\": \"1994-11-05T13:15:30Z\",\n \"id\": \"example\",\n \"traceId\": \"example\",\n \"name\": \"example\",\n \"startTime\": \"1994-11-05T13:15:30Z\",\n \"metadata\": \"UNKNOWN\",\n \"input\": \"UNKNOWN\",\n \"output\": \"UNKNOWN\",\n \"level\": \"DEBUG\",\n \"statusMessage\": \"example\",\n \"parentObservationId\": \"example\",\n \"version\": \"example\"\n}",
"options": {
"raw": {
"language": "json"
}
}
}
},
"response": []
},
{
"_type": "endpoint",
"name": "Update",
"request": {
"description": null,
"url": {
"raw": "{{baseUrl}}/api/public/generations",
"host": [
"{{baseUrl}}"
],
"path": [
"api",
"public",
"generations"
],
"query": [],
"variable": []
},
"header": [],
"method": "PATCH",
"auth": null,
"body": {
"mode": "raw",
"raw": "{\n \"generationId\": \"example\",\n \"traceId\": \"example\",\n \"name\": \"example\",\n \"endTime\": \"1994-11-05T13:15:30Z\",\n \"completionStartTime\": \"1994-11-05T13:15:30Z\",\n \"model\": \"example\",\n \"modelParameters\": {\n \"example\": \"example\"\n },\n \"prompt\": \"UNKNOWN\",\n \"version\": \"example\",\n \"metadata\": \"UNKNOWN\",\n \"completion\": \"UNKNOWN\",\n \"usage\": {\n \"promptTokens\": 0,\n \"completionTokens\": 0,\n \"totalTokens\": 0\n },\n \"level\": \"DEBUG\",\n \"statusMessage\": \"example\"\n}",
"options": {
"raw": {
"language": "json"
}
}
}
},
"response": []
}
]
},
{
"_type": "container",
"description": null,
@@ -401,7 +320,7 @@
"auth": null,
"body": {
"mode": "raw",
"raw": "{\n \"batch\": [\n {\n \"type\": \"trace-create\",\n \"id\": \"example\",\n \"timestamp\": \"example\",\n \"body\": {\n \"id\": \"example\",\n \"timestamp\": \"1994-11-05T13:15:30Z\",\n \"name\": \"example\",\n \"release\": \"example\",\n \"version\": \"example\",\n \"userId\": \"example\",\n \"metadata\": \"UNKNOWN\"\n }\n }\n ]\n}",
"raw": "{\n \"batch\": [\n {\n \"type\": \"trace-create\",\n \"body\": {\n \"id\": \"example\",\n \"name\": \"example\",\n \"userId\": \"example\",\n \"input\": \"UNKNOWN\",\n \"output\": \"UNKNOWN\",\n \"sessionId\": \"example\",\n \"release\": \"example\",\n \"version\": \"example\",\n \"metadata\": \"UNKNOWN\",\n \"public\": true\n },\n \"id\": \"example\",\n \"timestamp\": \"example\",\n \"metadata\": \"UNKNOWN\"\n }\n ]\n}",
"options": {
"raw": {
"language": "json"
@@ -545,6 +464,82 @@
}
]
},
{
"_type": "container",
"description": null,
"name": "Prompts",
"item": [
{
"_type": "endpoint",
"name": "Get",
"request": {
"description": "Get a specific prompt",
"url": {
"raw": "{{baseUrl}}/api/public/prompts?name=&version=",
"host": [
"{{baseUrl}}"
],
"path": [
"api",
"public",
"prompts"
],
"query": [
{
"key": "name",
"value": "",
"description": null
},
{
"key": "version",
"value": "",
"description": null
}
],
"variable": []
},
"header": [],
"method": "GET",
"auth": null,
"body": null
},
"response": []
},
{
"_type": "endpoint",
"name": "Create",
"request": {
"description": "Create a specific prompt",
"url": {
"raw": "{{baseUrl}}/api/public/prompts",
"host": [
"{{baseUrl}}"
],
"path": [
"api",
"public",
"prompts"
],
"query": [],
"variable": []
},
"header": [],
"method": "POST",
"auth": null,
"body": {
"mode": "raw",
"raw": "{\n \"name\": \"example\",\n \"isActive\": true,\n \"prompt\": \"example\"\n}",
"options": {
"raw": {
"language": "json"
}
}
}
},
"response": []
}
]
},
{
"_type": "container",
"description": null,
@@ -634,71 +629,37 @@
{
"_type": "container",
"description": null,
"name": "Span",
"name": "Sessions",
"item": [
{
"_type": "endpoint",
"name": "Create",
"name": "Get",
"request": {
"description": "Add a span to the database",
"description": "Get a session",
"url": {
"raw": "{{baseUrl}}/api/public/spans",
"raw": "{{baseUrl}}/api/public/sessions/:sessionId",
"host": [
"{{baseUrl}}"
],
"path": [
"api",
"public",
"spans"
"sessions",
":sessionId"
],
"query": [],
"variable": []
"variable": [
{
"key": "sessionId",
"value": "",
"description": "The unique id of a session"
}
]
},
"header": [],
"method": "POST",
"method": "GET",
"auth": null,
"body": {
"mode": "raw",
"raw": "{\n \"endTime\": \"1994-11-05T13:15:30Z\",\n \"id\": \"example\",\n \"traceId\": \"example\",\n \"name\": \"example\",\n \"startTime\": \"1994-11-05T13:15:30Z\",\n \"metadata\": \"UNKNOWN\",\n \"input\": \"UNKNOWN\",\n \"output\": \"UNKNOWN\",\n \"level\": \"DEBUG\",\n \"statusMessage\": \"example\",\n \"parentObservationId\": \"example\",\n \"version\": \"example\"\n}",
"options": {
"raw": {
"language": "json"
}
}
}
},
"response": []
},
{
"_type": "endpoint",
"name": "Update",
"request": {
"description": "Update a span to the database",
"url": {
"raw": "{{baseUrl}}/api/public/spans",
"host": [
"{{baseUrl}}"
],
"path": [
"api",
"public",
"spans"
],
"query": [],
"variable": []
},
"header": [],
"method": "PATCH",
"auth": null,
"body": {
"mode": "raw",
"raw": "{\n \"spanId\": \"example\",\n \"traceId\": \"example\",\n \"endTime\": \"1994-11-05T13:15:30Z\",\n \"name\": \"example\",\n \"metadata\": \"UNKNOWN\",\n \"input\": \"UNKNOWN\",\n \"output\": \"UNKNOWN\",\n \"level\": \"DEBUG\",\n \"version\": \"example\",\n \"statusMessage\": \"example\"\n}",
"options": {
"raw": {
"language": "json"
}
}
}
"body": null
},
"response": []
}
@@ -709,39 +670,6 @@
"description": null,
"name": "Trace",
"item": [
{
"_type": "endpoint",
"name": "Create",
"request": {
"description": "Add a trace to the database",
"url": {
"raw": "{{baseUrl}}/api/public/traces",
"host": [
"{{baseUrl}}"
],
"path": [
"api",
"public",
"traces"
],
"query": [],
"variable": []
},
"header": [],
"method": "POST",
"auth": null,
"body": {
"mode": "raw",
"raw": "{\n \"id\": \"example\",\n \"name\": \"example\",\n \"userId\": \"example\",\n \"externalId\": \"example\",\n \"release\": \"example\",\n \"version\": \"example\",\n \"metadata\": \"UNKNOWN\"\n}",
"options": {
"raw": {
"language": "json"
}
}
}
},
"response": []
},
{
"_type": "endpoint",
"name": "Get",
+6 -1
View File
@@ -18,6 +18,8 @@ const serverTestConfig = {
testEnvironment: "jest-environment-node",
};
// To avoid the "Cannot use import statement outside a module" errors while transforming ESM.
const esModules = ["superjson"];
// Add any custom config to be passed to Jest
/** @type {import('jest').Config} */
const config = {
@@ -26,7 +28,10 @@ const config = {
verbose: true,
projects: [
await createJestConfig(clientTestConfig)(),
await createJestConfig(serverTestConfig)(),
{
...(await createJestConfig(serverTestConfig)()),
transformIgnorePatterns: [`/node_modules/(?!(${esModules.join("|")})/)`],
},
],
};
+31 -21
View File
@@ -23,27 +23,37 @@ const nextConfig = {
output: "standalone",
async headers() {
// Required to check authentication status from langfuse.com
if (env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION !== undefined)
return [
{
source: "/api/auth/session",
headers: [
{
key: "Access-Control-Allow-Origin",
value: "https://langfuse.com",
},
{ key: "Access-Control-Allow-Credentials", value: "true" },
{ key: "Access-Control-Allow-Methods", value: "GET,POST" },
{
key: "Access-Control-Allow-Headers",
value: "Content-Type, Authorization",
},
],
},
];
return [];
return [
{
source: "/:path*",
headers: [
{
key: "x-frame-options",
value: "SAMEORIGIN",
},
],
},
// Required to check authentication status from langfuse.com
...(env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION !== undefined ?
[
{
source: "/api/auth/session",
headers: [
{
key: "Access-Control-Allow-Origin",
value: "https://langfuse.com",
},
{ key: "Access-Control-Allow-Credentials", value: "true" },
{ key: "Access-Control-Allow-Methods", value: "GET,POST" },
{
key: "Access-Control-Allow-Headers",
value: "Content-Type, Authorization",
},
]
},
] : []
)
]
},
// webassembly support for @dqbd/tiktoken
+3005 -1277
View File
File diff suppressed because it is too large Load Diff
+58 -38
View File
@@ -1,6 +1,6 @@
{
"name": "langfuse-core",
"version": "1.7.0",
"version": "1.27.1",
"private": true,
"scripts": {
"prebuild": "cp generated/openapi-client/openapi.yml public/openapi-client.yml && cp generated/openapi-server/openapi.yml public/openapi-server.yml",
@@ -8,6 +8,7 @@
"dev": "next dev",
"postinstall": "prisma generate",
"lint": "next lint",
"lint:fix": "next lint --fix",
"prettier": "prettier --write ./src *.{ts,js}",
"clean": "rm -rf node_modules",
"start": "next start",
@@ -24,15 +25,20 @@
},
"dependencies": {
"@anthropic-ai/tokenizer": "^0.0.4",
"@aws-sdk/client-s3": "^3.478.0",
"@aws-sdk/s3-request-presigner": "^3.478.0",
"@emotion/react": "^11.11.1",
"@emotion/styled": "^11.11.0",
"@formkit/auto-animate": "^0.8.1",
"@headlessui/react": "^1.7.17",
"@heroicons/react": "^2.0.18",
"@heroicons/react": "^2.1.1",
"@hookform/resolvers": "^3.3.2",
"@next-auth/prisma-adapter": "^1.0.7",
"@prisma/client": "^5.6.0",
"@prisma/client": "^5.7.1",
"@radix-ui/react-accordion": "^1.1.2",
"@radix-ui/react-alert-dialog": "^1.0.5",
"@radix-ui/react-avatar": "^1.0.4",
"@radix-ui/react-checkbox": "^1.0.4",
"@radix-ui/react-collapsible": "^1.0.3",
"@radix-ui/react-dialog": "^1.0.5",
"@radix-ui/react-dropdown-menu": "^2.0.6",
@@ -48,87 +54,96 @@
"@radix-ui/react-tabs": "^1.0.4",
"@radix-ui/react-toggle": "^1.0.3",
"@radix-ui/react-tooltip": "^1.0.7",
"@sentry/nextjs": "^7.81.1",
"@sentry/profiling-node": "^1.2.6",
"@react-email/components": "^0.0.12",
"@react-email/render": "^0.0.10",
"@sentry/nextjs": "^7.91.0",
"@sentry/profiling-node": "^1.3.2",
"@sentry/types": "^7.88.0",
"@t3-oss/env-nextjs": "^0.7.1",
"@tailwindcss/forms": "^0.5.7",
"@tailwindcss/line-clamp": "^0.4.4",
"@tanstack/react-query": "^4.36.1",
"@tanstack/react-table": "^8.10.7",
"@tanstack/react-table": "^8.11.2",
"@tremor/react": "^3.11.1",
"@trpc/client": "^10.44.1",
"@trpc/next": "^10.44.1",
"@trpc/react-query": "^10.44.1",
"@trpc/server": "^10.44.1",
"@vercel/edge-config": "^0.4.1",
"axios": "^1.6.2",
"axios": "^1.6.3",
"bcryptjs": "^2.4.3",
"class-variance-authority": "^0.7.0",
"clsx": "^2.0.0",
"cmdk": "^0.2.0",
"core-js": "^3.34.0",
"cors": "^2.8.5",
"date-fns": "^2.30.0",
"decimal.js": "^10.4.3",
"exponential-backoff": "^3.1.1",
"follow-redirects": "^1.15.4",
"js-yaml": "^4.1.0",
"lodash": "^4.17.21",
"lucide-react": "^0.292.0",
"next": "^14.0.3",
"lucide-react": "^0.302.0",
"mustache": "^4.2.0",
"next": "^14.0.4",
"next-auth": "^4.24.5",
"next-query-params": "^4.3.0",
"next-swagger-doc": "^0.4.0",
"posthog-js": "^1.93.2",
"posthog-node": "^3.1.3",
"next-query-params": "^5.0.0",
"nodemailer": "^6.9.7",
"posthog-js": "^1.96.1",
"posthog-node": "^3.2.1",
"react": "18.2.0",
"react-day-picker": "^8.9.1",
"react-day-picker": "^8.10.0",
"react-dom": "18.2.0",
"react-hook-form": "^7.48.2",
"react-hook-form": "^7.49.2",
"react-icons": "^4.12.0",
"react-responsive": "^9.0.2",
"react18-json-view": "^0.2.7-canary.1",
"react18-json-view": "^0.2.7-canary.2",
"superagent": "^8.1.2",
"superjson": "2.2.1",
"tailwind-merge": "^2.0.0",
"tailwind-merge": "^2.2.0",
"tailwindcss-animate": "^1.0.7",
"tiktoken": "^1.0.11",
"ts-pattern": "^5.0.5",
"ts-pattern": "^5.0.6",
"use-query-params": "^2.2.1",
"uuid": "^9.0.1",
"zod": "^3.22.4"
},
"devDependencies": {
"@playwright/test": "^1.40.0",
"@jedmao/location": "^3.0.0",
"@playwright/test": "^1.40.1",
"@release-it/bumper": "^6.0.1",
"@testing-library/jest-dom": "^6.1.4",
"@testing-library/jest-dom": "^6.1.5",
"@testing-library/react": "^14.1.2",
"@types/bcryptjs": "^2.4.6",
"@types/cors": "^2.8.17",
"@types/eslint": "^8.44.7",
"@types/jest": "^29.5.10",
"@types/eslint": "^8.56.0",
"@types/jest": "^29.5.11",
"@types/js-yaml": "^4.0.9",
"@types/lodash": "^4.14.202",
"@types/node": "18.16.0",
"@types/react": "^18.2.38",
"@types/react-dom": "^18.2.17",
"@types/node": "20.10.5",
"@types/nodemailer": "^6.4.14",
"@types/react": "^18.2.45",
"@types/react-dom": "^18.2.18",
"@types/swagger-ui-react": "^4.18.3",
"@types/uuid": "^9.0.7",
"@typescript-eslint/eslint-plugin": "^6.12.0",
"@typescript-eslint/parser": "^6.12.0",
"@typescript-eslint/eslint-plugin": "^6.16.0",
"@typescript-eslint/parser": "^6.16.0",
"autoprefixer": "^10.4.16",
"dotenv-cli": "^7.3.0",
"eslint": "^8.54.0",
"eslint-config-next": "^14.0.3",
"eslint": "^8.56.0",
"eslint-config-next": "^14.0.4",
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
"postcss": "^8.4.31",
"prettier": "^3.1.0",
"prettier-plugin-tailwindcss": "^0.5.7",
"prisma": "^5.6.0",
"release-it": "^17.0.0",
"postcss": "^8.4.32",
"prettier": "^3.1.1",
"prettier-plugin-tailwindcss": "^0.5.9",
"prisma": "^5.7.1",
"release-it": "^17.0.1",
"swagger-jsdoc": "^6.2.8",
"tailwindcss": "^3.3.5",
"ts-node": "^10.9.1",
"tailwindcss": "^3.4.0",
"ts-node": "^10.9.2",
"tsconfig-paths": "^4.2.0",
"typescript": "^5.3.2"
"typescript": "^5.3.3"
},
"ct3aMetadata": {
"initVersion": "7.13.0"
@@ -145,7 +160,12 @@
"release": true,
"web": true,
"autoGenerate": true,
"releaseName": "v${version}"
"releaseName": "v${version}",
"comments": {
"submit": true,
"issue": ":rocket: _This issue has been resolved in v${version}. See [${releaseName}](${releaseUrl}) for release notes._",
"pr": ":rocket: _This pull request is included in v${version}. See [${releaseName}](${releaseUrl}) for release notes._"
}
},
"plugins": {
"@release-it/bumper": {
@@ -0,0 +1,3 @@
-- AlterTable
ALTER TABLE "traces" ADD COLUMN "bookmarked" BOOLEAN NOT NULL DEFAULT false;
@@ -0,0 +1,27 @@
-- CreateTable
CREATE TABLE "membership_invitations" (
"id" TEXT NOT NULL,
"email" TEXT NOT NULL,
"role" "MembershipRole" NOT NULL,
"project_id" TEXT NOT NULL,
"sender_id" TEXT,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "membership_invitations_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "membership_invitations_id_key" ON "membership_invitations"("id");
-- CreateIndex
CREATE INDEX "membership_invitations_project_id_idx" ON "membership_invitations"("project_id");
-- CreateIndex
CREATE INDEX "membership_invitations_email_idx" ON "membership_invitations"("email");
-- AddForeignKey
ALTER TABLE "membership_invitations" ADD CONSTRAINT "membership_invitations_project_id_fkey" FOREIGN KEY ("project_id") REFERENCES "projects"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "membership_invitations" ADD CONSTRAINT "membership_invitations_sender_id_fkey" FOREIGN KEY ("sender_id") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE CASCADE;
@@ -0,0 +1,34 @@
-- AlterTable
ALTER TABLE "traces" ADD COLUMN "input" JSONB,
ADD COLUMN "output" JSONB,
ADD COLUMN "session_id" TEXT;
-- CreateTable
CREATE TABLE "trace_sessions" (
"id" TEXT NOT NULL,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"project_id" TEXT NOT NULL,
"bookmarked" BOOLEAN NOT NULL DEFAULT false,
"public" BOOLEAN NOT NULL DEFAULT false,
CONSTRAINT "trace_sessions_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE INDEX "trace_sessions_project_id_idx" ON "trace_sessions"("project_id");
-- CreateIndex
CREATE INDEX "trace_sessions_created_at_idx" ON "trace_sessions"("created_at");
-- CreateIndex
CREATE UNIQUE INDEX "trace_sessions_id_project_id_key" ON "trace_sessions"("id", "project_id");
-- CreateIndex
CREATE INDEX "traces_session_id_idx" ON "traces"("session_id");
-- AddForeignKey
ALTER TABLE "trace_sessions" ADD CONSTRAINT "trace_sessions_project_id_fkey" FOREIGN KEY ("project_id") REFERENCES "projects"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "traces" ADD CONSTRAINT "traces_session_id_fkey" FOREIGN KEY ("session_id") REFERENCES "trace_sessions"("id") ON DELETE SET NULL ON UPDATE CASCADE;
@@ -0,0 +1,4 @@
-- AlterTable
-- Adding this migration does not lock the postgres table. For non volaitile data, the default value is stored in the
-- table metadata and accessed on read query time. No table re-write is required (https://www.postgresql.org/docs/current/sql-altertable.html#:~:text=When%20a%20column%20is%20added,is%20specified%2C%20NULL%20is%20used.)
ALTER TABLE "observations" ADD COLUMN "unit" TEXT NOT NULL DEFAULT 'TOKENS';
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "projects" ADD COLUMN "cloud_config" JSONB;
@@ -0,0 +1,3 @@
-- AlterTable
ALTER TABLE "Account" ADD COLUMN "expires_in" INTEGER,
ADD COLUMN "ext_expires_in" INTEGER;
@@ -0,0 +1,29 @@
-- AlterTable
ALTER TABLE "observations" ADD COLUMN "prompt_id" TEXT;
-- CreateTable
CREATE TABLE "prompts" (
"id" TEXT NOT NULL,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"project_id" TEXT NOT NULL,
"created_by" TEXT NOT NULL,
"prompt" TEXT NOT NULL,
"name" TEXT NOT NULL,
"version" INTEGER NOT NULL,
"is_active" BOOLEAN NOT NULL,
CONSTRAINT "prompts_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE INDEX "prompts_project_id_name_version_idx" ON "prompts"("project_id", "name", "version");
-- CreateIndex
CREATE UNIQUE INDEX "prompts_project_id_name_version_key" ON "prompts"("project_id", "name", "version");
-- AddForeignKey
ALTER TABLE "observations" ADD CONSTRAINT "observations_prompt_id_fkey" FOREIGN KEY ("prompt_id") REFERENCES "prompts"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "prompts" ADD CONSTRAINT "prompts_project_id_fkey" FOREIGN KEY ("project_id") REFERENCES "projects"("id") ON DELETE CASCADE ON UPDATE CASCADE;
@@ -0,0 +1,30 @@
-- Migration to add gpt-3.5-turbo-1106 and gpt-3.5-turbo-instruct models
INSERT INTO pricings (
id,
model_name,
pricing_unit,
price,
currency,
token_type
)
VALUES
('clqqpc2pr000008l3hvy63gxy','gpt-3.5-turbo-1106', 'PER_1000_TOKENS', 0.001, 'USD', 'PROMPT'),
('clqqpcb6d000208l3atrfbmou','gpt-3.5-turbo-1106', 'PER_1000_TOKENS', 0.002, 'USD', 'COMPLETION'),
('clqqpdh45000008lfgrnx76cv','gpt-3.5-turbo-instruct', 'PER_1000_TOKENS', 0.0015, 'USD', 'PROMPT'),
('clqqpdjya000108lf3s4b4c4m','gpt-3.5-turbo-instruct', 'PER_1000_TOKENS', 0.002, 'USD', 'COMPLETION'),
('clnnuiuq6000008l4dxp43wc6', 'claude-instant-1.2', 'PER_1000_TOKENS', 0.00163, 'USD', 'PROMPT'),
('clnnujlmj000108l48wne4ii9', 'claude-instant-1.2', 'PER_1000_TOKENS', 0.00551, 'USD', 'COMPLETION'),
('clnnukutg000208l49qqt9lyr', 'claude-instant-1.1', 'PER_1000_TOKENS', 0.00163, 'USD', 'PROMPT'),
('clnnun3x3000408l490qz9uv0', 'claude-instant-1.1', 'PER_1000_TOKENS', 0.00551, 'USD', 'COMPLETION'),
('clnnuosvy000508l4gsjy2pp4', 'claude-2.0', 'PER_1000_TOKENS', 0.01102, 'USD', 'PROMPT'),
('clnnuqcns000608l40qaz8trt', 'claude-2.0', 'PER_1000_TOKENS', 0.03268, 'USD', 'COMPLETION'),
('clnnuuif8000808l4gal4fjq4', 'claude-1.0', 'PER_1000_TOKENS', 0.01102, 'USD', 'PROMPT'),
('clnnutptp000708l47r091vvd', 'claude-1.0', 'PER_1000_TOKENS', 0.03268, 'USD', 'COMPLETION'),
('clnon8riv000308mlgfr1agiv', 'text-embedding-ada-002', 'PER_1000_TOKENS', 0.0001, 'USD', 'PROMPT'),
('clnon9kfz000408ml1bg81o6z', 'text-embedding-ada-002', 'PER_1000_TOKENS', 0.0001, 'USD', 'COMPLETION'),
('clqwniv8a000d08l2frjl7mmw', 'codechat-bison-32k', 'PER_1000_CHARS', 0.0005, 'USD', 'PROMPT'),
('clqwnj044000e08l2dfjm5g90', 'chat-bison-32k', 'PER_1000_CHARS', 0.0005, 'USD', 'PROMPT'),
('clqwnj47r000f08l2a16fekls', 'chat-bison-32k', 'PER_1000_CHARS', 0.0005, 'USD', 'COMPLETION'),
('clqwnj863000g08l2bwxgdapm', 'codechat-bison-32k', 'PER_1000_CHARS', 0.0005, 'USD', 'COMPLETION')
ON CONFLICT (id) DO NOTHING;
+91 -20
View File
@@ -7,9 +7,10 @@ generator client {
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
directUrl = env("DIRECT_URL")
provider = "postgresql"
url = env("DATABASE_URL")
directUrl = env("DIRECT_URL")
shadowDatabaseUrl = env("SHADOW_DATABASE_URL")
}
model Example {
@@ -28,6 +29,8 @@ model Account {
refresh_token String? // @db.Text
access_token String? // @db.Text
expires_at Int?
expires_in Int?
ext_expires_in Int?
token_type String?
scope String?
id_token String? // @db.Text
@@ -47,19 +50,20 @@ model Session {
}
model User {
id String @id @default(cuid())
id String @id @default(cuid())
name String?
email String? @unique
emailVerified DateTime? @map("email_verified")
email String? @unique
emailVerified DateTime? @map("email_verified")
password String?
image String?
admin Boolean @default(false)
admin Boolean @default(false)
accounts Account[]
sessions Session[]
memberships Membership[]
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
featureFlags String[] @default([]) @map("feature_flags")
invitations MembershipInvitation[]
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
featureFlags String[] @default([]) @map("feature_flags")
@@map("users")
}
@@ -74,16 +78,20 @@ model VerificationToken {
}
model Project {
id String @id @default(cuid())
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
id String @id @default(cuid())
createdAt DateTime @default(now()) @map("created_at")
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[]
traces Trace[]
observations Observation[]
apiKeys ApiKey[]
dataset Dataset[]
RawEvents Events[]
invitations MembershipInvitation[]
sessions TraceSession[]
Prompt Prompt[]
@@map("projects")
}
@@ -122,6 +130,22 @@ model Membership {
@@map("memberships")
}
model MembershipInvitation {
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")
@@index([projectId])
@@index([email])
@@map("membership_invitations")
}
enum MembershipRole {
OWNER
ADMIN
@@ -129,23 +153,45 @@ enum MembershipRole {
VIEWER
}
model Trace {
model TraceSession {
id String @id @default(cuid())
externalId String? @map("external_id")
timestamp DateTime @default(now())
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
projectId String @map("project_id")
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
bookmarked Boolean @default(false)
public Boolean @default(false)
traces Trace[]
@@unique([id, projectId])
@@index([projectId])
@@index([createdAt])
@@map("trace_sessions")
}
model Trace {
id String @id @default(cuid())
externalId String? @map("external_id")
timestamp DateTime @default(now())
name String?
userId String? @map("user_id")
userId String? @map("user_id")
metadata Json?
release String?
version String?
projectId String @map("project_id")
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
public Boolean @default(false)
input Json?
output Json?
sessionId String? @map("session_id")
session TraceSession? @relation(fields: [sessionId], references: [id], onDelete: SetNull)
projectId String @map("project_id")
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
public Boolean @default(false)
bookmarked Boolean @default(false)
scores Score[]
@@unique([projectId, externalId])
@@index([projectId])
@@index([sessionId])
@@index([name])
@@index([userId])
@@index([externalId])
@@ -177,12 +223,16 @@ model Observation {
promptTokens Int @default(0) @map("prompt_tokens")
completionTokens Int @default(0) @map("completion_tokens")
totalTokens Int @default(0) @map("total_tokens")
unit String @default("TOKENS")
completionStartTime DateTime? @map("completion_start_time")
scores Score[]
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
derivedDatasetItems DatasetItem[]
datasetRunItems DatasetRunItems[]
promptId String? @map("prompt_id")
prompt Prompt? @relation(fields: [promptId], onDelete: SetNull, references: [id])
@@unique([id, projectId])
@@index([traceId])
@@index([type])
@@ -339,3 +389,24 @@ model Events {
@@index(projectId)
@@map("events")
}
model Prompt {
id String @id @default(cuid())
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
projectId String @map("project_id")
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
createdBy String @map("created_by")
prompt String
name String
version Int
isActive Boolean @map("is_active")
Observation Observation[]
@@unique([projectId, name, version])
@@index([projectId, name, version])
@@map("prompts")
}
+75 -27
View File
@@ -53,6 +53,25 @@ async function main() {
},
});
const prompt = await prisma.prompt.upsert({
where: {
projectId_name_version: {
projectId: seedProjectId,
name: "summary-prompt",
version: 1,
},
},
create: {
name: "summary-prompt",
project: { connect: { id: seedProjectId } },
prompt: "prompt {{variable}} {{anotherVariable}}",
isActive: true,
version: 1,
createdBy: "user-1",
},
update: {},
});
const seedApiKey = {
id: "seed-api-key",
secret: process.env.SEED_SECRET_KEY ?? "sk-lf-1234567890",
@@ -133,19 +152,42 @@ async function main() {
},
},
userId: `user-${i % 10}`,
session:
Math.random() > 0.3
? {
connectOrCreate: {
where: {
id: `session-${i % 10}`,
},
create: {
id: `session-${i % 10}`,
project: {
connect: { id: [project1.id, project2.id][i % 2] },
},
},
},
}
: undefined,
input:
Math.random() > 0.3
? "I'm looking for a React component"
: undefined,
output:
Math.random() > 0.3
? "What kind of component are you looking for?"
: undefined,
scores: {
createMany: {
data: [
{
name: "latency",
value: Math.floor(Math.random() * 20),
timestamp: traceTs,
},
{
name: "feedback",
value: Math.floor(Math.random() * 3) - 1,
timestamp: traceTs,
},
...(Math.random() > 0.5
? [
{
name: "feedback",
value: Math.floor(Math.random() * 3) - 1,
timestamp: traceTs,
},
]
: []),
...(Math.random() > 0.7
? [
{
@@ -303,25 +345,31 @@ async function main() {
totalTokens: promptTokens + completionTokens,
parentObservationId: span.id,
traceId: trace.id,
...{
...(Math.random() > 0.5 && trace.projectId === prompt.projectId
? { prompt: { connect: { id: prompt.id } } }
: {}),
},
},
});
await prisma.score.create({
data: {
name: "quality",
value: Math.random() * 2 - 1,
observationId: generation.id,
traceId: trace.id,
},
});
await prisma.score.create({
data: {
name: "conciseness",
value: Math.random() * 2 - 1,
observationId: generation.id,
traceId: trace.id,
},
});
if (Math.random() > 0.6)
await prisma.score.create({
data: {
name: "quality",
value: Math.random() * 2 - 1,
observationId: generation.id,
traceId: trace.id,
},
});
if (Math.random() > 0.6)
await prisma.score.create({
data: {
name: "conciseness",
value: Math.random() * 2 - 1,
observationId: generation.id,
traceId: trace.id,
},
});
generationIds.push(generation.id);
-11
View File
@@ -1,11 +0,0 @@
# Security Policy
## Supported Versions
We always recommend using the latest version of Langfuse to ensure you get all security updates.
## Reporting a Vulnerability
Please report security vulnerabilities to security@langfuse.com.
We currently do not operate a bug bounty program.
+2 -2
View File
@@ -12,9 +12,9 @@ if (process.env.NEXT_PUBLIC_SENTRY_DSN)
// Set tracesSampleRate to 1.0 to capture 100%
// of transactions for performance monitoring.
// We recommend adjusting this value in production
tracesSampleRate: 0.8,
tracesSampleRate: 0.5,
profilesSampleRate: 0.8, // Profiling sample rate is relative to tracesSampleRate
profilesSampleRate: 0.5, // Profiling sample rate is relative to tracesSampleRate
integrations: [
// Add profiling integration to list of integrations
new ProfilingIntegration(),
+187
View File
@@ -0,0 +1,187 @@
/** @jest-environment node */
import { prisma } from "@/src/server/db";
import { makeAPICall, pruneDatabase } from "@/src/__tests__/test-utils";
import { v4 } from "uuid";
describe("/api/public/datasets and /api/public/dataset-items API Endpoints", () => {
beforeEach(async () => await pruneDatabase());
afterEach(async () => await pruneDatabase());
it("should create and get a dataset", async () => {
await makeAPICall("POST", "/api/public/datasets", {
name: "dataset-name",
});
const dbDataset = await prisma.dataset.findMany({
where: {
name: "dataset-name",
},
});
expect(dbDataset.length).toBeGreaterThan(0);
const getDataset = await makeAPICall(
"GET",
`/api/public/datasets/dataset-name`,
);
expect(getDataset.status).toBe(200);
expect(getDataset.body).toMatchObject({
name: "dataset-name",
});
});
it("should create and get a dataset item (via datasets and individually)", async () => {
await makeAPICall("POST", "/api/public/datasets", {
name: "dataset-name",
});
await makeAPICall("POST", "/api/public/dataset-items", {
datasetName: "dataset-name",
input: { key: "value" },
expectedOutput: { key: "value" },
});
const dbDatasetItem = await prisma.datasetItem.findFirst({
where: {
dataset: {
name: "dataset-name",
},
},
});
expect(dbDatasetItem).not.toBeNull();
const getDataset = await makeAPICall(
"GET",
`/api/public/datasets/dataset-name`,
);
expect(getDataset.status).toBe(200);
expect(getDataset.body).toMatchObject({
name: "dataset-name",
items: [
{
id: dbDatasetItem!.id,
input: { key: "value" },
expectedOutput: { key: "value" },
},
],
});
const getDatasetItem = await makeAPICall(
"GET",
`/api/public/dataset-items/${dbDatasetItem!.id}`,
);
expect(getDatasetItem.status).toBe(200);
expect(getDatasetItem.body).toMatchObject({
id: dbDatasetItem!.id,
input: { key: "value" },
expectedOutput: { key: "value" },
});
});
it("should upsert a dataset item", async () => {
await makeAPICall("POST", "/api/public/datasets", {
name: "dataset-name",
});
const item1 = await makeAPICall("POST", "/api/public/dataset-items", {
id: "dataset-item-id",
datasetName: "dataset-name",
input: { key: "value" },
});
expect(item1.status).toBe(200);
expect(item1.body).toMatchObject({
id: "dataset-item-id",
});
const item2 = await makeAPICall("POST", "/api/public/dataset-items", {
id: "dataset-item-id",
datasetName: "dataset-name",
input: { key: "value2" },
});
expect(item2.status).toBe(200);
expect(item2.body).toMatchObject({
id: "dataset-item-id",
input: { key: "value2" },
});
const dbDatasetItem = await prisma.datasetItem.findFirst({
where: { id: "dataset-item-id" },
});
expect(dbDatasetItem).not.toBeNull();
expect(dbDatasetItem?.input).toMatchObject({ key: "value2" });
});
it("should create and get a dataset run", async () => {
const dataset = await makeAPICall("POST", "/api/public/datasets", {
name: "dataset-name",
});
expect(dataset.status).toBe(200);
expect(dataset.body).toMatchObject({
name: "dataset-name",
});
await makeAPICall("POST", "/api/public/dataset-items", {
datasetName: "dataset-name",
id: "dataset-item-id",
input: { key: "value" },
expectedOutput: { key: "value" },
});
const traceId = v4();
const observationId = v4();
const response = 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: "observation-create",
timestamp: new Date().toISOString(),
body: {
id: observationId,
traceId: traceId,
type: "GENERATION",
name: "generation-name",
startTime: "2021-01-01T00:00:00.000Z",
endTime: "2021-01-01T00:00:00.000Z",
modelParameters: { key: "value" },
input: { key: "value" },
metadata: { key: "value" },
version: "2.0.0",
},
},
],
});
expect(response.status).toBe(207);
const runItem = await makeAPICall("POST", "/api/public/dataset-run-items", {
datasetItemId: "dataset-item-id",
observationId: observationId,
runName: "run-name",
});
const dbRun = await prisma.datasetRuns.findFirst({
where: {
name: "run-name",
},
});
expect(dbRun).not.toBeNull();
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
expect(dbRun?.datasetId).toBe(dataset.body.id);
expect(runItem.status).toBe(200);
expect(runItem.body).toMatchObject({
datasetItemId: "dataset-item-id",
observationId: observationId,
datasetRunId: dbRun?.id,
});
});
});
+7 -7
View File
@@ -241,12 +241,12 @@ describe("/api/public/events API Endpoint", () => {
expect(createSpan.status).toBe(200);
expect(dbEvent?.id).toBe(spanId);
expect(dbEvent?.traceId).toBe(dbTrace[0]?.id);
expect(dbEvent?.name).toBe(generationName);
expect(dbEvent?.startTime).toEqual(new Date("2021-01-01T00:00:00.000Z"));
expect(dbEvent?.input).toEqual({ key: "value" });
expect(dbEvent?.metadata).toEqual({ key: "value" });
expect(dbEvent?.version).toBe("2.0.0");
expect(dbEvent.id).toBe(spanId);
expect(dbEvent.traceId).toBe(dbTrace[0]?.id);
expect(dbEvent.name).toBe(generationName);
expect(dbEvent.startTime).toEqual(new Date("2021-01-01T00:00:00.000Z"));
expect(dbEvent.input).toEqual({ key: "value" });
expect(dbEvent.metadata).toEqual({ key: "value" });
expect(dbEvent.version).toBe("2.0.0");
});
});
@@ -0,0 +1,110 @@
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/no-unsafe-call */
import { cleanup, render } from "@testing-library/react";
import { TestRouter } from "@/src/__tests__/fixtures/TestRouter";
import { useOrderByState } from "@/src/features/orderBy/hooks/useOrderByState";
import { LocationMock } from "@jedmao/location";
import NextAdapterPages from "next-query-params/pages";
import { useRouter } from "next/router";
import { QueryParamProvider } from "use-query-params";
afterEach(cleanup);
jest.mock("next/router", () => ({
useRouter: jest.fn(),
}));
const { location: savedLocation } = window;
// The test for the useOrderByState hook
describe("useOrderByState hook", () => {
let testRouter: TestRouter;
let locationMock: LocationMock;
beforeAll(() => {
// @ts-expect-error - TS only allows to delete optional params; but here we want to delete it
// as it will be set in beforeEach
delete window.location;
});
beforeEach(() => {
jest.resetAllMocks();
locationMock = new LocationMock("https://langfuse.com");
testRouter = new TestRouter(locationMock);
window.location = locationMock;
(useRouter as jest.Mock).mockReturnValue(testRouter);
});
afterAll(() => {
window.location = savedLocation;
});
test("orderBy takes the default value if no url param is given", () => {
const result = render(
<QueryParamProvider adapter={NextAdapterPages}>
<UseOrderByStateExample />
</QueryParamProvider>,
);
expect(result.queryByText(/column Column 1, order DESC/)).toBeTruthy();
});
test("setOrderBy updates orderBy query param", () => {
const result = render(
<QueryParamProvider adapter={NextAdapterPages}>
<UseOrderByStateExample />
</QueryParamProvider>,
);
result.queryByText(/Set Order/)!.click();
result.rerender(
<QueryParamProvider adapter={NextAdapterPages}>
<UseOrderByStateExample />
</QueryParamProvider>,
);
expect(result.queryByText(/column Column 2, order ASC/)).toBeTruthy();
expect(locationMock.searchParams.get("orderBy")).toBe(
"column-Column 2_order-ASC",
);
});
test("orderBy reads the given param from the url", () => {
locationMock.replace(
"https://langfuse.com?orderBy=column-Column 3_order-DESC",
);
const result = render(
<QueryParamProvider adapter={NextAdapterPages}>
<UseOrderByStateExample />
</QueryParamProvider>,
);
expect(result.queryByText(/column Column 3, order DESC/)).toBeTruthy();
});
});
const UseOrderByStateExample = () => {
const [orderBy, setOrderBy] = useOrderByState({
column: "Column 1",
order: "DESC",
});
return (
<>
<h1>{`column ${orderBy?.column}, order ${orderBy?.order}`}</h1>
<button
onClick={() => {
setOrderBy({
column: "Column 2",
order: "ASC",
});
}}
>
Set Order
</button>
</>
);
};
@@ -0,0 +1,36 @@
import { orderByToPrismaSql } from "@/src/features/orderBy/server/orderByToPrisma";
import { tracesTableCols } from "@/src/server/api/definitions/tracesTable";
import { Prisma } from "@prisma/client";
// The test for the orderByToPrisma function
describe("orderByToPrisma (Convert orderBy to Prisma.sql)", () => {
test("orderByToPrisma returns default sql when orderBy=null", () => {
expect(orderByToPrismaSql(null, tracesTableCols)).toStrictEqual(
Prisma.sql`ORDER BY t.timestamp DESC`,
);
});
test("orderByToPrisma returns correct clause for orderBy column included in column defs", () => {
expect(
orderByToPrismaSql(
{
column: "latency",
order: "ASC",
},
tracesTableCols,
),
).toStrictEqual(Prisma.sql`ORDER BY tl.latency ASC`);
});
test("orderByToPrisma throws error for orderBy column not included in column defs", () => {
expect(() =>
orderByToPrismaSql(
{
column: "InvalidCol",
order: "ASC",
},
tracesTableCols,
),
).toThrow(/Invalid filter column: InvalidCol/);
});
});
+56
View File
@@ -0,0 +1,56 @@
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-call */
import { type Router } from "next/router";
import type { UrlObject } from "url";
import { type LocationMock } from "@jedmao/location";
type PartialRouter = Partial<Router>;
export const BASE_URL = "https://langfuse.com";
/**
* A Router to be used for testing which provides the bare minimum needed
* for the useQueryParam(s) hook and NextAdapter to work.
*/
export class TestRouter implements PartialRouter {
isReady = true;
pathname = "/";
private currentUrl = "";
private history: string[] = [];
constructor(private locationMock: LocationMock) {}
replace = (url: string | UrlObject) => {
// eslint-disable-next-line @typescript-eslint/no-base-to-string, @typescript-eslint/restrict-template-expressions
this.locationMock.assign(`${BASE_URL}${url}`);
this.currentUrl = TestRouter.getURLString(url);
this.locationMock.assign(`${BASE_URL}${this.currentUrl}`);
return Promise.resolve(true);
};
push = (url: string | UrlObject) => {
this.history.push(this.currentUrl);
this.currentUrl = TestRouter.getURLString(url);
this.locationMock.assign(`${BASE_URL}${this.currentUrl}`);
return Promise.resolve(true);
};
setIsReady = (isReady: boolean) => {
this.isReady = isReady;
};
get asPath() {
return this.pathname;
}
static getURLString(url: string | UrlObject): string {
if (typeof url === "string") {
return url;
}
return `${url.pathname}${url.search}`;
}
getParams(): URLSearchParams {
return new URL(`${BASE_URL}${this.currentUrl}`).searchParams;
}
}
+152 -67
View File
@@ -6,69 +6,154 @@ import { v4 as uuidv4 } from "uuid";
describe("/api/public/generations API Endpoint", () => {
beforeEach(async () => await pruneDatabase());
afterEach(async () => await pruneDatabase());
it("should create generation after trace", async () => {
await pruneDatabase();
[
{
usage: {
input: 100,
output: 200,
total: 100,
unit: "CHARACTERS",
},
expectedUnit: "CHARACTERS",
expectedPromptTokens: 100,
expectedCompletionTokens: 200,
expectedTotalTokens: 100,
},
{
usage: {
total: 100,
unit: "CHARACTERS",
},
expectedUnit: "CHARACTERS",
expectedPromptTokens: 0,
expectedCompletionTokens: 0,
expectedTotalTokens: 100,
},
{
usage: {
total: 100,
},
expectedUnit: "TOKENS",
expectedPromptTokens: 0,
expectedCompletionTokens: 0,
expectedTotalTokens: 100,
},
{
usage: {
promptTokens: 100,
completionTokens: 200,
totalTokens: 100,
},
expectedPromptTokens: 100,
expectedCompletionTokens: 200,
expectedTotalTokens: 100,
expectedUnit: "TOKENS",
},
{
usage: {
totalTokens: 100,
},
expectedPromptTokens: 0,
expectedCompletionTokens: 0,
expectedTotalTokens: 100,
expectedUnit: "TOKENS",
},
{
usage: undefined,
expectedPromptTokens: 0,
expectedCompletionTokens: 0,
expectedTotalTokens: 0,
expectedUnit: "TOKENS",
},
{
usage: null,
expectedPromptTokens: 0,
expectedCompletionTokens: 0,
expectedTotalTokens: 0,
expectedUnit: "TOKENS",
},
{
usage: {},
expectedPromptTokens: 0,
expectedCompletionTokens: 0,
expectedTotalTokens: 0,
expectedUnit: "TOKENS",
},
].forEach((testConfig) => {
it(`should create generation after trace 1 ${JSON.stringify(
testConfig,
)}`, async () => {
await pruneDatabase();
const traceId = uuidv4();
const traceId = uuidv4();
await makeAPICall("POST", "/api/public/traces", {
id: traceId,
name: "trace-name",
userId: "user-1",
projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
metadata: { key: "value" },
release: "1.0.0",
version: "2.0.0",
});
const dbTrace = await prisma.trace.findMany({
where: {
await makeAPICall("POST", "/api/public/traces", {
id: traceId,
},
});
expect(dbTrace.length).toBeGreaterThan(0);
expect(dbTrace[0]?.id).toBe(traceId);
const generationId = uuidv4();
const createGeneration = await makeAPICall(
"POST",
"/api/public/generations",
{
id: generationId,
traceId: traceId,
name: "generation-name",
startTime: "2021-01-01T00:00:00.000Z",
endTime: "2021-01-01T00:00:00.000Z",
model: "model-name",
modelParameters: { key: "value" },
prompt: { key: "value" },
name: "trace-name",
userId: "user-1",
projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
metadata: { key: "value" },
release: "1.0.0",
version: "2.0.0",
},
);
});
expect(createGeneration.status).toBe(200);
const dbGeneration = await prisma.observation.findUnique({
where: {
id: generationId,
},
const dbTrace = await prisma.trace.findMany({
where: {
id: traceId,
},
});
expect(dbTrace.length).toBeGreaterThan(0);
expect(dbTrace[0]?.id).toBe(traceId);
const generationId = uuidv4();
const createGeneration = await makeAPICall(
"POST",
"/api/public/generations",
{
id: generationId,
traceId: traceId,
name: "generation-name",
startTime: "2021-01-01T00:00:00.000Z",
endTime: "2021-01-01T00:00:00.000Z",
model: "model-name",
modelParameters: { key: "value" },
prompt: { key: "value" },
metadata: { key: "value" },
version: "2.0.0",
usage: testConfig.usage,
},
);
expect(createGeneration.status).toBe(200);
const dbGeneration = await prisma.observation.findUnique({
where: {
id: generationId,
},
});
expect(dbGeneration?.id).toBe(generationId);
expect(dbGeneration?.traceId).toBe(traceId);
expect(dbGeneration?.name).toBe("generation-name");
expect(dbGeneration?.startTime).toEqual(
new Date("2021-01-01T00:00:00.000Z"),
);
expect(dbGeneration?.endTime).toEqual(
new Date("2021-01-01T00:00:00.000Z"),
);
expect(dbGeneration?.model).toBe("model-name");
expect(dbGeneration?.modelParameters).toEqual({ key: "value" });
expect(dbGeneration?.input).toEqual({ key: "value" });
expect(dbGeneration?.metadata).toEqual({ key: "value" });
expect(dbGeneration?.version).toBe("2.0.0");
expect(dbGeneration?.unit).toBe(testConfig.expectedUnit);
expect(dbGeneration?.promptTokens).toBe(testConfig.expectedPromptTokens);
expect(dbGeneration?.completionTokens).toBe(
testConfig.expectedCompletionTokens,
);
expect(dbGeneration?.totalTokens).toBe(testConfig.expectedTotalTokens);
});
expect(dbGeneration?.id).toBe(generationId);
expect(dbGeneration?.traceId).toBe(traceId);
expect(dbGeneration?.name).toBe("generation-name");
expect(dbGeneration?.startTime).toEqual(
new Date("2021-01-01T00:00:00.000Z"),
);
expect(dbGeneration?.endTime).toEqual(new Date("2021-01-01T00:00:00.000Z"));
expect(dbGeneration?.model).toBe("model-name");
expect(dbGeneration?.modelParameters).toEqual({ key: "value" });
expect(dbGeneration?.input).toEqual({ key: "value" });
expect(dbGeneration?.metadata).toEqual({ key: "value" });
expect(dbGeneration?.version).toBe("2.0.0");
});
it("should create generation before trace", async () => {
@@ -339,8 +424,8 @@ describe("/api/public/generations API Endpoint", () => {
name: generationName,
},
});
expect(dbGeneration?.id).toBe(generationId);
expect(dbGeneration?.traceId).toBe(externalTraceId);
expect(dbGeneration.id).toBe(generationId);
expect(dbGeneration.traceId).toBe(externalTraceId);
const dbTraces = await prisma.trace.findMany();
expect(dbTraces.length).toBe(0);
@@ -383,18 +468,18 @@ describe("/api/public/generations API Endpoint", () => {
expect(createGeneration.status).toBe(200);
expect(dbGeneration?.id).toBe(generationId);
expect(dbGeneration?.traceId).toBe(dbTrace[0]?.id);
expect(dbGeneration?.name).toBe(generationName);
expect(dbGeneration?.startTime).toEqual(
expect(dbGeneration.id).toBe(generationId);
expect(dbGeneration.traceId).toBe(dbTrace[0]?.id);
expect(dbGeneration.name).toBe(generationName);
expect(dbGeneration.startTime).toEqual(
new Date("2021-01-01T00:00:00.000Z"),
);
expect(dbGeneration?.endTime).toEqual(new Date("2021-01-01T00:00:00.000Z"));
expect(dbGeneration?.model).toBe("model-name");
expect(dbGeneration?.modelParameters).toEqual({ key: "value" });
expect(dbGeneration?.input).toEqual({ key: "value" });
expect(dbGeneration?.metadata).toEqual({ key: "value" });
expect(dbGeneration?.version).toBe("2.0.0");
expect(dbGeneration.endTime).toEqual(new Date("2021-01-01T00:00:00.000Z"));
expect(dbGeneration.model).toBe("model-name");
expect(dbGeneration.modelParameters).toEqual({ key: "value" });
expect(dbGeneration.input).toEqual({ key: "value" });
expect(dbGeneration.metadata).toEqual({ key: "value" });
expect(dbGeneration.version).toBe("2.0.0");
});
it("should update generation", async () => {
+400 -65
View File
@@ -1,18 +1,266 @@
/** @jest-environment node */
import { makeAPICall, pruneDatabase } from "@/src/__tests__/test-utils";
import { cleanEvent } from "@/src/pages/api/public/ingestion";
import { prisma } from "@/src/server/db";
import { v4 } from "uuid";
describe("/api/public/ingestion API Endpoint", () => {
beforeEach(async () => await pruneDatabase());
it("should create trace and generation", async () => {
[
{
usage: {
input: 100,
output: 200,
total: 100,
unit: "CHARACTERS",
},
expectedUnit: "CHARACTERS",
expectedPromptTokens: 100,
expectedCompletionTokens: 200,
expectedTotalTokens: 100,
},
{
usage: {
total: 100,
unit: "CHARACTERS",
},
expectedUnit: "CHARACTERS",
expectedPromptTokens: 0,
expectedCompletionTokens: 0,
expectedTotalTokens: 100,
},
{
usage: {
total: 100,
},
expectedUnit: "TOKENS",
expectedPromptTokens: 0,
expectedCompletionTokens: 0,
expectedTotalTokens: 100,
},
{
usage: {
promptTokens: 100,
completionTokens: 200,
totalTokens: 100,
},
expectedPromptTokens: 100,
expectedCompletionTokens: 200,
expectedTotalTokens: 100,
expectedUnit: "TOKENS",
},
{
usage: {
totalTokens: 100,
},
expectedPromptTokens: 0,
expectedCompletionTokens: 0,
expectedTotalTokens: 100,
expectedUnit: "TOKENS",
},
{
usage: undefined,
expectedPromptTokens: 0,
expectedCompletionTokens: 0,
expectedTotalTokens: 0,
expectedUnit: "TOKENS",
},
{
usage: null,
expectedPromptTokens: 0,
expectedCompletionTokens: 0,
expectedTotalTokens: 0,
expectedUnit: "TOKENS",
},
{
usage: {},
expectedPromptTokens: 0,
expectedCompletionTokens: 0,
expectedTotalTokens: 0,
expectedUnit: "TOKENS",
},
].forEach((testConfig) => {
it(`should create trace and generation ${JSON.stringify(
testConfig,
)}`, async () => {
const traceId = v4();
const generationId = v4();
const spanId = v4();
const scoreId = v4();
const response = await makeAPICall("POST", "/api/public/ingestion", {
metadata: {
sdk_verion: "1.0.0",
sdk_name: "python",
},
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: "observation-create",
timestamp: new Date().toISOString(),
body: {
id: generationId,
traceId: traceId,
type: "GENERATION",
name: "generation-name",
startTime: "2021-01-01T00:00:00.000Z",
endTime: "2021-01-01T00:00:00.000Z",
modelParameters: { key: "value" },
input: { key: "value" },
metadata: { key: "value" },
version: "2.0.0",
},
},
{
id: v4(),
type: "observation-update",
timestamp: new Date().toISOString(),
body: {
id: generationId,
type: "GENERATION",
output: { key: "this is a great gpt output" },
usage: testConfig.usage,
},
},
{
id: v4(),
type: "observation-create",
timestamp: new Date().toISOString(),
body: {
id: spanId,
traceId: traceId,
type: "SPAN",
name: "span-name",
startTime: "2021-01-01T00:00:00.000Z",
endTime: "2021-01-01T00:00:00.000Z",
input: { input: "value" },
metadata: { meta: "value" },
version: "2.0.0",
},
},
{
id: v4(),
type: "score-create",
timestamp: new Date().toISOString(),
body: {
id: scoreId,
name: "score-name",
value: 100.5,
traceId: traceId,
},
},
],
});
expect(response.status).toBe(207);
console.log("response body", response.body);
const dbTrace = await prisma.trace.findMany({
where: {
name: "trace-name",
},
});
expect(dbTrace.length).toBeGreaterThan(0);
expect(dbTrace[0]?.name).toBe("trace-name");
expect(dbTrace[0]?.release).toBe("1.0.0");
expect(dbTrace[0]?.externalId).toBeNull();
expect(dbTrace[0]?.version).toBe("2.0.0");
expect(dbTrace[0]?.projectId).toBe(
"7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
);
const dbGeneration = await prisma.observation.findUnique({
where: {
id: generationId,
},
});
expect(dbGeneration?.id).toBe(generationId);
expect(dbGeneration?.traceId).toBe(traceId);
expect(dbGeneration?.name).toBe("generation-name");
expect(dbGeneration?.startTime).toEqual(
new Date("2021-01-01T00:00:00.000Z"),
);
expect(dbGeneration?.endTime).toEqual(
new Date("2021-01-01T00:00:00.000Z"),
);
expect(dbGeneration?.model).toBeNull();
expect(dbGeneration?.modelParameters).toEqual({ key: "value" });
expect(dbGeneration?.input).toEqual({ key: "value" });
expect(dbGeneration?.metadata).toEqual({ key: "value" });
expect(dbGeneration?.version).toBe("2.0.0");
expect(dbGeneration?.promptTokens).toEqual(
testConfig.expectedPromptTokens,
);
expect(dbGeneration?.completionTokens).toEqual(
testConfig.expectedCompletionTokens,
);
expect(dbGeneration?.totalTokens).toEqual(testConfig.expectedTotalTokens);
expect(dbGeneration?.unit).toEqual(testConfig.expectedUnit);
expect(dbGeneration?.output).toEqual({
key: "this is a great gpt output",
});
const dbSpan = await prisma.observation.findUnique({
where: {
id: spanId,
},
});
expect(dbSpan?.id).toBe(spanId);
expect(dbSpan?.name).toBe("span-name");
expect(dbSpan?.startTime).toEqual(new Date("2021-01-01T00:00:00.000Z"));
expect(dbSpan?.endTime).toEqual(new Date("2021-01-:00:00.000Z"));
expect(dbSpan?.input).toEqual({ input: "value" });
expect(dbSpan?.metadata).toEqual({ meta: "value" });
expect(dbSpan?.version).toBe("2.0.0");
const dbScore = await prisma.score.findUnique({
where: {
id: scoreId,
},
});
expect(dbScore?.id).toBe(scoreId);
expect(dbScore?.traceId).toBe(traceId);
expect(dbScore?.name).toBe("score-name");
expect(dbScore?.value).toBe(100.5);
expect(dbScore?.observationId).toBeNull();
});
});
it("should create and update all events", async () => {
const traceId = v4();
const generationId = v4();
const spanId = v4();
const eventId = v4();
const scoreId = v4();
const exception = `
ERROR langfuse:callback.py:677 'model_name'
Traceback (most recent call last):
File "/Users/maximiliandeichmann/development/github.com/langfuse/langfuse-python/langfuse/callback.py", line 674, in __on_llm_action
model_name = kwargs["invocation_params"]["model_name"]
KeyError: 'model_name'
`;
const response = await makeAPICall("POST", "/api/public/ingestion", {
batch: [
{
@@ -21,55 +269,55 @@ describe("/api/public/ingestion API Endpoint", () => {
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: "observation-create",
type: "span-create",
timestamp: new Date().toISOString(),
body: {
id: generationId,
id: spanId,
traceId: traceId,
type: "GENERATION",
name: "generation-name",
startTime: "2021-01-01T00:00:00.000Z",
endTime: "2021-01-01T00:00:00.000Z",
modelParameters: { key: "value" },
input: { key: "value" },
metadata: { key: "value" },
version: "2.0.0",
},
},
{
id: v4(),
type: "observation-update",
timestamp: new Date().toISOString(),
body: {
id: generationId,
type: "GENERATION",
output: { key: "this is a great gpt output" },
usage: { promptTokens: 400, completionTokens: 1000 },
},
},
{
id: v4(),
type: "observation-create",
type: "span-update",
timestamp: new Date().toISOString(),
body: {
id: spanId,
traceId: traceId,
type: "SPAN",
name: "span-name",
startTime: "2021-01-01T00:00:00.000Z",
endTime: "2021-01-01T00:00:00.000Z",
input: { input: "value" },
metadata: { meta: "value" },
version: "2.0.0",
},
},
{
id: v4(),
type: "generation-create",
timestamp: new Date().toISOString(),
body: {
id: generationId,
traceId: traceId,
parentObservationId: spanId,
},
},
{
id: v4(),
type: "generation-update",
timestamp: new Date().toISOString(),
body: {
id: generationId,
name: "generation-name",
},
},
{
id: v4(),
type: "event-create",
timestamp: new Date().toISOString(),
body: {
id: eventId,
traceId: traceId,
name: "event-name",
parentObservationId: generationId,
},
},
{
@@ -79,8 +327,17 @@ describe("/api/public/ingestion API Endpoint", () => {
body: {
id: scoreId,
name: "score-name",
value: 100.5,
traceId: traceId,
value: 100.5,
observationId: generationId,
},
},
{
id: v4(),
type: "sdk-log",
timestamp: new Date().toISOString(),
body: {
log: exception,
},
},
],
@@ -90,17 +347,23 @@ describe("/api/public/ingestion API Endpoint", () => {
const dbTrace = await prisma.trace.findMany({
where: {
name: "trace-name",
id: traceId,
},
});
expect(dbTrace.length).toBeGreaterThan(0);
expect(dbTrace[0]?.name).toBe("trace-name");
expect(dbTrace[0]?.release).toBe("1.0.0");
expect(dbTrace[0]?.externalId).toBeNull();
expect(dbTrace[0]?.version).toBe("2.0.0");
expect(dbTrace[0]?.projectId).toBe("7a88fb47-b4e2-43b8-a06c-a5ce950dc53a");
const dbSpan = await prisma.observation.findUnique({
where: {
id: spanId,
},
});
expect(dbSpan?.id).toBe(spanId);
expect(dbSpan?.name).toBe("span-name");
expect(dbSpan?.traceId).toBe(traceId);
const dbGeneration = await prisma.observation.findUnique({
where: {
id: generationId,
@@ -110,34 +373,18 @@ describe("/api/public/ingestion API Endpoint", () => {
expect(dbGeneration?.id).toBe(generationId);
expect(dbGeneration?.traceId).toBe(traceId);
expect(dbGeneration?.name).toBe("generation-name");
expect(dbGeneration?.startTime).toEqual(
new Date("2021-01-01T00:00:00.000Z"),
);
expect(dbGeneration?.endTime).toEqual(new Date("2021-01-01T00:00:00.000Z"));
expect(dbGeneration?.model).toBeNull();
expect(dbGeneration?.modelParameters).toEqual({ key: "value" });
expect(dbGeneration?.input).toEqual({ key: "value" });
expect(dbGeneration?.metadata).toEqual({ key: "value" });
expect(dbGeneration?.version).toBe("2.0.0");
expect(dbGeneration?.promptTokens).toEqual(400);
expect(dbGeneration?.completionTokens).toEqual(1000);
expect(dbGeneration?.output).toEqual({
key: "this is a great gpt output",
});
expect(dbGeneration?.parentObservationId).toBe(spanId);
const dbSpan = await prisma.observation.findUnique({
const dbEvent = await prisma.observation.findUnique({
where: {
id: spanId,
id: eventId,
},
});
expect(dbSpan?.id).toBe(spanId);
expect(dbSpan?.name).toBe("span-name");
expect(dbSpan?.startTime).toEqual(new Date("2021-01-01T00:00:00.000Z"));
expect(dbSpan?.endTime).toEqual(new Date("2021-01-:00:00.000Z"));
expect(dbSpan?.input).toEqual({ input: "value" });
expect(dbSpan?.metadata).toEqual({ meta: "value" });
expect(dbSpan?.version).toBe("2.0.0");
expect(dbEvent?.id).toBe(eventId);
expect(dbEvent?.traceId).toBe(traceId);
expect(dbEvent?.name).toBe("event-name");
expect(dbEvent?.parentObservationId).toBe(generationId);
const dbScore = await prisma.score.findUnique({
where: {
@@ -147,9 +394,21 @@ describe("/api/public/ingestion API Endpoint", () => {
expect(dbScore?.id).toBe(scoreId);
expect(dbScore?.traceId).toBe(traceId);
expect(dbScore?.name).toBe("score-name");
expect(dbScore?.observationId).toBe(generationId);
expect(dbScore?.value).toBe(100.5);
expect(dbScore?.observationId).toBeNull();
const logEvent = await prisma.events.findFirst({
where: {
data: {
path: ["body", "log"],
string_contains: "ERROR",
},
},
});
expect(logEvent).toBeDefined();
expect(logEvent).not.toBeFalsy();
expect(JSON.stringify(logEvent?.data)).toContain("KeyError: 'model_name'");
});
it("should upsert threats", async () => {
@@ -686,4 +945,80 @@ describe("/api/public/ingestion API Endpoint", () => {
expect(dbGeneration).toBeTruthy();
});
it("filters out NULL characters", async () => {
const traceId = v4();
const generationId = 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",
},
},
{
id: v4(),
type: "observation-create",
timestamp: new Date().toISOString(),
body: {
id: generationId,
traceId: traceId,
type: "GENERATION",
name: "generation-name",
traceIdType: "LANGFUSE",
input: {
key: "IB\nibo.org Change site\nIB Home / . . . / News / News about the IB / Why ChatGPT is an opportunity for schools IB Home / News / News about the IB / Why ChatGPT is an opportunity for schools Why ChatGPT is an opportunity for schools Published: 06 March 2023 Last updated: 06 June 2023 Date published: 28 February 2023 Dr Matthew Glanville, Head of Assessment Principles and Practice Source: Why ChatGPT is an opportunity for schools | The Times Those of us who work in the schools or exam sector should not be terri \u0000 ed by ChatGPT and the rise of AI software we should be excited. We should embrace it as an extraordinary opportunity. Contrary to some stark warnings, it is not the end of exams, nor even a huge threat to coursework, but it does bring into very sharp focus the impact that arti \u0000 ",
},
output: {
key: "제점이 있었죠. 그중 하나는 일제가 한국의 신용체계를 망가뜨린 채 한국을 떠났다는 겁니다. 해방전 일제는 조선의 신용체계를 거의 독점적으로 소유한 상황이었습니다. 1945년 6월 기준 일제는 조선의 본점을 둔 전체은행 5개의 불입자본 총액의 89.7%를",
},
},
},
],
});
expect(responseOne.status).toBe(207);
const dbTrace = await prisma.trace.findMany({
where: {
id: traceId,
},
});
expect(dbTrace.length).toEqual(1);
const dbGeneration = await prisma.observation.findUnique({
where: {
id: generationId,
},
});
expect(dbGeneration?.input).toStrictEqual({
key: `IB
ibo.org Change site
IB Home / . . . / News / News about the IB / Why ChatGPT is an opportunity for schools IB Home / News / News about the IB / Why ChatGPT is an opportunity for schools Why ChatGPT is an opportunity for schools Published: 06 March 2023 Last updated: 06 June 2023 Date published: 28 February 2023 Dr Matthew Glanville, Head of Assessment Principles and Practice Source: Why ChatGPT is an opportunity for schools | The Times Those of us who work in the schools or exam sector should not be terri ed by ChatGPT and the rise of AI software we should be excited. We should embrace it as an extraordinary opportunity. Contrary to some stark warnings, it is not the end of exams, nor even a huge threat to coursework, but it does bring into very sharp focus the impact that arti `,
});
expect(dbGeneration?.output).toStrictEqual({
key: "제점이 있었죠. 그중 하나는 일제가 한국의 신용체계를 망가뜨린 채 한국을 떠났다는 겁니다. 해방전 일제는 조선의 신용체계를 거의 독점적으로 소유한 상황이었습니다. 1945년 6월 기준 일제는 조선의 본점을 둔 전체은행 5개의 불입자본 총액의 89.7%를",
});
expect(dbGeneration).toBeTruthy();
});
[
{ input: "A\u0000hallo", expected: "Ahallo" },
{ input: ["A\u0000hallo"], expected: ["Ahallo"] },
{ input: { obj: ["A\u0000hallo"] }, expected: { obj: ["Ahallo"] } },
].forEach(({ input, expected }) => {
it(`cleans events with null values ${JSON.stringify(
input,
)} ${JSON.stringify(expected)}`, () => {
const cleanedEvent = cleanEvent(input);
console.log(cleanedEvent);
expect(cleanedEvent).toStrictEqual(expected);
});
});
});
+346
View File
@@ -0,0 +1,346 @@
/** @jest-environment node */
import { prisma } from "@/src/server/db";
import { makeAPICall, pruneDatabase } from "@/src/__tests__/test-utils";
import { v4 as uuidv4, v4 } from "uuid";
import { type Prompt } from "@prisma/client";
describe("/api/public/prompts API Endpoint", () => {
beforeEach(async () => await pruneDatabase());
it("should fetch a prompt", async () => {
const promptId = uuidv4();
await prisma.prompt.create({
data: {
id: promptId,
name: "prompt-name",
prompt: "prompt",
isActive: true,
version: 1,
project: {
connect: { id: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a" },
},
createdBy: "user-1",
},
});
const fetchedObservations = await makeAPICall(
"GET",
"/api/public/prompts?name=prompt-name&version=1",
undefined,
);
expect(fetchedObservations.status).toBe(200);
if (!isPrompt(fetchedObservations.body)) {
throw new Error("Expected body to be a prompt");
}
expect(fetchedObservations.body.id).toBe(promptId);
expect(fetchedObservations.body.name).toBe("prompt-name");
expect(fetchedObservations.body.prompt).toBe("prompt");
expect(fetchedObservations.body.version).toBe(1);
expect(fetchedObservations.body.isActive).toBe(true);
expect(fetchedObservations.body.createdBy).toBe("user-1");
});
it("should fetch active prompt only if no prompt version is given", async () => {
const promptId = uuidv4();
await prisma.prompt.create({
data: {
id: promptId,
name: "prompt-name",
prompt: "prompt",
isActive: false,
version: 1,
project: {
connect: { id: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a" },
},
createdBy: "user-1",
},
});
const fetchedObservations = await makeAPICall(
"GET",
"/api/public/prompts?name=prompt-name",
undefined,
);
expect(fetchedObservations.status).toBe(404);
});
it("should fetch inactive prompt if prompt version is given", async () => {
const promptId = uuidv4();
const promptTwoId = uuidv4();
await prisma.prompt.create({
data: {
id: promptId,
name: "prompt-name",
prompt: "prompt-one",
isActive: false,
version: 1,
project: {
connect: { id: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a" },
},
createdBy: "user-1",
},
});
await prisma.prompt.create({
data: {
id: promptTwoId,
name: "prompt-name",
prompt: "prompt",
isActive: true,
version: 2,
project: {
connect: { id: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a" },
},
createdBy: "user-1",
},
});
const fetchedObservations = await makeAPICall(
"GET",
"/api/public/prompts?name=prompt-name&version=1",
undefined,
);
expect(fetchedObservations.status).toBe(200);
if (!isPrompt(fetchedObservations.body)) {
throw new Error("Expected body to be a prompt");
}
expect(fetchedObservations.body.id).toBe(promptId);
expect(fetchedObservations.body.name).toBe("prompt-name");
expect(fetchedObservations.body.prompt).toBe("prompt-one");
expect(fetchedObservations.body.version).toBe(1);
expect(fetchedObservations.body.isActive).toBe(false);
expect(fetchedObservations.body.createdBy).toBe("user-1");
});
it("should fetch active prompt when multiple exist", async () => {
const promptIdOne = uuidv4();
const promptIdTwo = uuidv4();
await prisma.prompt.create({
data: {
id: promptIdOne,
name: "prompt-name",
prompt: "prompt",
isActive: false,
version: 1,
project: {
connect: { id: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a" },
},
createdBy: "user-1",
},
});
await prisma.prompt.create({
data: {
id: promptIdTwo,
name: "prompt-name",
prompt: "prompt",
isActive: true,
version: 2,
project: {
connect: { id: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a" },
},
createdBy: "user-1",
},
});
const fetchedObservations = await makeAPICall(
"GET",
"/api/public/prompts?name=prompt-name",
undefined,
);
expect(fetchedObservations.status).toBe(200);
if (!isPrompt(fetchedObservations.body)) {
throw new Error("Expected body to be a prompt");
}
expect(fetchedObservations.body.id).toBe(promptIdTwo);
expect(fetchedObservations.body.name).toBe("prompt-name");
expect(fetchedObservations.body.prompt).toBe("prompt");
expect(fetchedObservations.body.version).toBe(2);
expect(fetchedObservations.body.isActive).toBe(true);
expect(fetchedObservations.body.createdBy).toBe("user-1");
});
it("should create and fetch a prompt", async () => {
await makeAPICall("POST", "/api/public/prompts", {
name: "prompt-name",
prompt: "prompt",
isActive: true,
projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
});
const fetchedObservations = await makeAPICall(
"GET",
"/api/public/prompts?name=prompt-name&version=1",
undefined,
);
expect(fetchedObservations.status).toBe(200);
if (!isPrompt(fetchedObservations.body)) {
throw new Error("Expected body to be an array of observations");
}
expect(fetchedObservations.body.name).toBe("prompt-name");
expect(fetchedObservations.body.prompt).toBe("prompt");
expect(fetchedObservations.body.version).toBe(1);
expect(fetchedObservations.body.isActive).toBe(true);
expect(fetchedObservations.body.createdBy).toBe("API");
});
it("should relate generation to prompt", async () => {
const traceId = v4();
const generationId = v4();
const promptId = uuidv4();
await prisma.prompt.create({
data: {
id: promptId,
name: "prompt-name",
prompt: "prompt",
isActive: true,
version: 1,
project: {
connect: { id: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a" },
},
createdBy: "user-1",
},
});
const response = await makeAPICall("POST", "/api/public/ingestion", {
metadata: {
sdk_verion: "1.0.0",
sdk_name: "python",
},
batch: [
{
id: v4(),
type: "trace-create",
timestamp: new Date().toISOString(),
body: {
id: traceId,
name: "trace-name",
},
},
{
id: v4(),
type: "generation-create",
timestamp: new Date().toISOString(),
body: {
id: generationId,
traceId: traceId,
type: "GENERATION",
name: "generation-name",
promptName: "prompt-name",
promptVersion: 1,
},
},
],
});
expect(response.status).toBe(207);
console.log("response body", response.body);
const dbGeneration = await prisma.observation.findUnique({
where: {
id: generationId,
},
});
expect(dbGeneration?.id).toBe(generationId);
expect(dbGeneration?.promptId).toBe(promptId);
});
it("should fail if prompt version is missing", async () => {
const traceId = v4();
const generationId = v4();
const promptId = uuidv4();
await prisma.prompt.create({
data: {
id: promptId,
name: "prompt-name",
prompt: "prompt",
isActive: true,
version: 1,
project: {
connect: { id: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a" },
},
createdBy: "user-1",
},
});
const response = await makeAPICall("POST", "/api/public/ingestion", {
metadata: {
sdk_verion: "1.0.0",
sdk_name: "python",
},
batch: [
{
id: v4(),
type: "trace-create",
timestamp: new Date().toISOString(),
body: {
id: traceId,
name: "trace-name",
},
},
{
id: v4(),
type: "generation-create",
timestamp: new Date().toISOString(),
body: {
id: generationId,
traceId: traceId,
type: "GENERATION",
name: "generation-name",
promptName: "prompt-name",
},
},
],
});
expect(response.status).toBe(207);
console.log("response body", response.body);
const dbGeneration = await prisma.observation.findUnique({
where: {
id: generationId,
},
});
expect(dbGeneration).toBeNull();
});
});
const isPrompt = (x: unknown): x is Prompt => {
if (typeof x !== "object" || x === null) return false;
const prompt = x as Prompt;
return (
typeof prompt.id === "string" &&
typeof prompt.name === "string" &&
typeof prompt.version === "number" &&
typeof prompt.prompt === "string" &&
typeof prompt.isActive === "boolean" &&
typeof prompt.projectId === "string" &&
typeof prompt.createdBy === "string"
);
};
@@ -0,0 +1,107 @@
/** @jest-environment node */
import { pruneDatabase } from "@/src/__tests__/test-utils";
import { appRouter } from "@/src/server/api/root";
import { createInnerTRPCContext } from "@/src/server/api/trpc";
import { prisma } from "@/src/server/db";
import type { Session } from "next-auth";
describe("Traces TRPC Router", () => {
beforeEach(async () => await pruneDatabase());
afterEach(async () => await pruneDatabase());
const session: Session = {
expires: "1",
user: {
id: "clgb17vnp000008jjere5g15i",
name: "John Doe",
projects: [
{
id: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
role: "ADMIN",
name: "test",
},
],
featureFlags: {
templateFlag: true,
},
admin: true,
},
};
const ctx = createInnerTRPCContext({ session });
const caller = appRouter.createCaller({ ...ctx, prisma });
test("traces.all RPC returns an array of traces", async () => {
const trace = {
name: "trace-name",
userId: "user-1",
metadata: { key: "value" },
release: "1.0.0",
version: "2.0.0",
};
await prisma.trace.create({
data: { ...trace, projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a" },
});
const traces = await caller.traces.all({
page: 0,
limit: 10,
// projectId from `seed.ts`
projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
filter: null,
searchQuery: "",
orderBy: null,
});
expect(traces).toBeDefined();
expect(traces).toMatchObject([trace]);
});
test("traces.all RPC orders traces by userId", async () => {
const traceTmpl = {
name: "trace-name",
userId: "user-1",
metadata: { key: "value" },
release: "1.0.0",
version: "2.0.0",
};
const trace1 = traceTmpl;
const trace2 = { ...traceTmpl, userId: "user-2" };
await prisma.trace.create({
data: { ...traceTmpl, projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a" },
});
await prisma.trace.create({
data: {
...trace2,
projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
},
});
const tracesASC = await caller.traces.all({
page: 0,
limit: 10,
// projectId from `seed.ts`
projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
filter: null,
searchQuery: "",
orderBy: {
column: "userId",
order: "ASC",
},
});
expect(tracesASC).toMatchObject([trace1, trace2]);
const tracesDESC = await caller.traces.all({
page: 0,
limit: 10,
// projectId from `seed.ts`
projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
filter: null,
searchQuery: "",
orderBy: {
column: "userId",
order: "DESC",
},
});
expect(tracesDESC).toMatchObject([trace2, trace1]);
});
});
+62
View File
@@ -0,0 +1,62 @@
/** @jest-environment node */
import { makeAPICall, pruneDatabase } from "@/src/__tests__/test-utils";
import { prisma } from "@/src/server/db";
describe("/api/public/traces API Endpoint", () => {
beforeEach(async () => await pruneDatabase());
afterEach(async () => await pruneDatabase());
it("should create a session via a trace", async () => {
await pruneDatabase();
await makeAPICall("POST", "/api/public/traces", {
name: "trace-name",
projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
sessionId: "session-id",
});
const dbSession = await prisma.traceSession.findFirst({
where: {
id: "session-id",
},
});
expect(dbSession).not.toBeNull();
expect(dbSession).toMatchObject({
id: "session-id",
projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
});
});
it("should get session including traces", async () => {
await pruneDatabase();
await makeAPICall("POST", "/api/public/traces", {
name: "trace-name",
id: "trace-id",
input: { hello: "world" },
output: "hi",
projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
sessionId: "session-id",
});
const response = await makeAPICall(
"GET",
"/api/public/sessions/session-id",
);
expect(response.status).toBe(200);
expect(response.body).toMatchObject({
id: "session-id",
projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
traces: [
{
id: "trace-id",
input: { hello: "world" },
output: "hi",
},
],
});
});
});
+10 -10
View File
@@ -140,8 +140,8 @@ describe("/api/public/spans API Endpoint", () => {
name: "spanName",
},
});
expect(dbSpan?.id).toBe(spanId);
expect(dbSpan?.traceId).toBe(traceId);
expect(dbSpan.id).toBe(spanId);
expect(dbSpan.traceId).toBe(traceId);
});
it("should create trace when creating span without existing trace", async () => {
@@ -248,14 +248,14 @@ describe("/api/public/spans API Endpoint", () => {
expect(createSpan.status).toBe(200);
expect(dbSpan?.id).toBe(spanId);
expect(dbSpan?.traceId).toBe(dbTrace[0]?.id);
expect(dbSpan?.name).toBe(generationName);
expect(dbSpan?.startTime).toEqual(new Date("2021-01-01T00:00:00.000Z"));
expect(dbSpan?.endTime).toEqual(new Date("2021-01-01T00:00:00.000Z"));
expect(dbSpan?.input).toEqual({ key: "value" });
expect(dbSpan?.metadata).toEqual({ key: "value" });
expect(dbSpan?.version).toBe("2.0.0");
expect(dbSpan.id).toBe(spanId);
expect(dbSpan.traceId).toBe(dbTrace[0]?.id);
expect(dbSpan.name).toBe(generationName);
expect(dbSpan.startTime).toEqual(new Date("2021-01-01T00:00:00.000Z"));
expect(dbSpan.endTime).toEqual(new Date("2021-01-01T00:00:00.000Z"));
expect(dbSpan.input).toEqual({ key: "value" });
expect(dbSpan.metadata).toEqual({ key: "value" });
expect(dbSpan.version).toBe("2.0.0");
});
it("should update span", async () => {
+9 -2
View File
@@ -9,6 +9,11 @@ export const pruneDatabase = async () => {
await prisma.score.deleteMany();
await prisma.observation.deleteMany();
await prisma.trace.deleteMany();
await prisma.datasetItem.deleteMany();
await prisma.dataset.deleteMany();
await prisma.datasetRuns.deleteMany();
await prisma.prompt.deleteMany();
await prisma.events.deleteMany();
};
export function createBasicAuthHeader(
@@ -24,7 +29,7 @@ export function createBasicAuthHeader(
export async function makeAPICall(
method: "POST" | "GET" | "PUT" | "DELETE" | "PATCH",
url: string,
body: unknown,
body?: unknown,
) {
const finalUrl = `http://localhost:3000/${url}`;
const options = {
@@ -37,7 +42,9 @@ export async function makeAPICall(
"sk-lf-1234567890",
),
},
body: JSON.stringify(body),
// Conditionally include the body property if the method is not "GET"
...(method !== "GET" &&
body !== undefined && { body: JSON.stringify(body) }),
};
const a = await fetch(finalUrl, options);
+28 -9
View File
@@ -44,29 +44,48 @@ describe("/api/public/traces API Endpoint", () => {
metadata: { key: "value" },
release: "1.0.0",
version: "2.0.0",
public: true,
});
const dbTrace1 = await prisma.trace.findFirst({
where: {
id: "trace-id",
},
});
expect(dbTrace1).not.toBeNull();
expect(dbTrace1).toMatchObject({
name: "trace-name",
release: "1.0.0",
externalId: null,
version: "2.0.0",
public: true,
userId: "user-1",
});
await makeAPICall("POST", "/api/public/traces", {
id: "trace-id",
name: "trace-name",
userId: "user-1",
projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
metadata: { key: "value" },
release: "1.0.0",
version: "5.0.0",
public: false,
});
const dbTrace = await prisma.trace.findMany({
const dbTrace2 = await prisma.trace.findFirst({
where: {
name: "trace-name",
},
});
expect(dbTrace.length).toBeGreaterThan(0);
expect(dbTrace[0]?.name).toBe("trace-name");
expect(dbTrace[0]?.release).toBe("1.0.0");
expect(dbTrace[0]?.externalId).toBeNull();
expect(dbTrace[0]?.version).toBe("5.0.0");
expect(dbTrace[0]?.projectId).toBe("7a88fb47-b4e2-43b8-a06c-a5ce950dc53a");
expect(dbTrace2).not.toBeNull();
expect(dbTrace2).toMatchObject({
name: "trace-name",
release: "1.0.0",
externalId: null,
version: "5.0.0",
public: false,
userId: "user-1",
});
});
});
+1
View File
@@ -8,6 +8,7 @@ describe("Token Count Functions", () => {
{ model: "gpt-4-vision-preview", tokens: 114 },
{ model: "claude", tokens: 118 },
{ model: "claude-instant-1.2", tokens: 118 },
{ model: "gpt-3.5-turbo-1106", tokens: 114 },
].forEach(({ model, tokens }) => {
it(`should return token count ${tokens} for ${model}`, () => {
const result = tokenCount({
+4
View File
@@ -1,3 +1,4 @@
import { VERSION } from "@/src/constants/VERSION";
import { cn } from "@/src/utils/tailwind";
export const LangfuseIcon = ({
@@ -20,9 +21,11 @@ export const LangfuseIcon = ({
export const LangfuseLogo = ({
className,
size = "sm",
version = false,
}: {
size?: "sm" | "xl";
className?: string;
version?: boolean;
}) => (
<div className={cn("flex items-center", className)}>
<LangfuseIcon size={size === "sm" ? 16 : 20} />
@@ -34,5 +37,6 @@ export const LangfuseLogo = ({
>
Langfuse
</span>
{version && <span className="ml-2 text-xs text-gray-400">{VERSION}</span>}
</div>
);
+1 -1
View File
@@ -191,7 +191,7 @@ export function DatePickerWithRange({
{DEFAULT_DATE_RANGE_SELECTION}
</SelectItem>
{dateTimeAggregationOptions.toReversed().map((item) => (
<SelectItem key={item} value={`${item}`}>
<SelectItem key={item} value={item}>
{item}
</SelectItem>
))}
+18 -8
View File
@@ -1,6 +1,6 @@
import { TrashIcon } from "lucide-react";
import { useRouter } from "next/router";
import { useState } from "react";
import { Button } from "@/src/components/ui/button";
import { api } from "@/src/utils/api";
import {
@@ -9,6 +9,7 @@ import {
PopoverTrigger,
} from "@/src/components/ui/popover";
import { useHasAccess } from "@/src/features/rbac/utils/checkAccess";
import { usePostHog } from "posthog-js/react";
export function DeleteTrace({
traceId,
@@ -19,13 +20,16 @@ export function DeleteTrace({
projectId: string;
isTableAction?: boolean;
}) {
const [isDeleted, setIsDeleted] = useState(false);
const router = useRouter();
const utils = api.useUtils();
const posthog = usePostHog();
const hasAccess = useHasAccess({ projectId, scope: "traces:delete" });
const mutDeleteTrace = api.traces.delete.useMutation({
const mutDeleteTraces = api.traces.deleteMany.useMutation({
onSuccess: () => {
setIsDeleted(true);
if (!isTableAction) {
void router
.push(`/project/${projectId}/traces`)
@@ -41,14 +45,14 @@ export function DeleteTrace({
}
return (
<Popover>
<Popover key={traceId}>
<PopoverTrigger asChild>
{isTableAction ? (
<Button variant="ghost" size="xs">
<TrashIcon className="h-4 w-4" />
</Button>
) : (
<Button variant="outline" type="button">
<Button variant="outline" type="button" size="sm">
<TrashIcon className="h-4 w-4" />
</Button>
)}
@@ -63,10 +67,16 @@ export function DeleteTrace({
<Button
type="button"
variant="destructive"
loading={mutDeleteTrace.isLoading}
onClick={() =>
void mutDeleteTrace.mutateAsync({ traceId, projectId })
}
loading={mutDeleteTraces.isLoading || isDeleted}
onClick={() => {
void mutDeleteTraces.mutateAsync({
traceIds: [traceId],
projectId,
});
posthog.capture("trace:delete", {
source: isTableAction ? "table-single-row" : "trace",
});
}}
>
Delete trace
</Button>
+36 -6
View File
@@ -1,8 +1,16 @@
import { Badge } from "@/src/components/ui/badge";
import {
HoverCard,
HoverCardContent,
HoverCardTrigger,
} from "@/src/components/ui/hover-card";
import { cn } from "@/src/utils/tailwind";
import { MessageCircle } from "lucide-react";
type ScoreSimplified = {
name: string;
value: number;
comment?: string | null;
};
export const GroupedScoreBadges = ({
@@ -12,7 +20,7 @@ export const GroupedScoreBadges = ({
scores: ScoreSimplified[];
variant?: "badge" | "headings";
}) => {
const groupedScores = scores.reduce(
const groupedScores = scores.reduce<Record<string, ScoreSimplified[]>>(
(acc, score) => {
if (!acc[score.name] || !Array.isArray(acc[score.name])) {
acc[score.name] = [score];
@@ -21,7 +29,31 @@ export const GroupedScoreBadges = ({
}
return acc;
},
{} as Record<string, ScoreSimplified[]>,
{},
);
const ScoresOfGroup = (props: {
scores: ScoreSimplified[];
className?: string;
}) => (
<div className={cn("text-xs", props.className)}>
{props.scores.map((s, i) => (
<span key={i} className="group/score ml-1 first:ml-0">
{s.value.toFixed(2)}
{s.comment && (
<HoverCard>
<HoverCardTrigger className="ml-1 inline-block cursor-pointer">
<MessageCircle size={12} />
</HoverCardTrigger>
<HoverCardContent>
<p>{s.comment}</p>
</HoverCardContent>
</HoverCard>
)}
<span className="group-last/score:hidden">,</span>
</span>
))}
</div>
);
if (variant === "headings")
@@ -32,9 +64,7 @@ export const GroupedScoreBadges = ({
.map(([name, scores]) => (
<div key={name}>
<div className="text-xs text-gray-500">{name}</div>
<div className="text-xs">
{scores.map((s) => s.value.toFixed(2)).join(", ")}
</div>
<ScoresOfGroup scores={scores} />
</div>
))}
</div>
@@ -50,7 +80,7 @@ export const GroupedScoreBadges = ({
key={name}
className="break-all font-normal"
>
{name}: {scores.map((s) => s.value.toFixed(2)).join(", ")}
{name}: <ScoresOfGroup scores={scores} className="ml-2" />
</Badge>
))}
</>
+29 -20
View File
@@ -5,34 +5,43 @@ import {
} from "@/src/components/ui/hover-card";
import { HelpCircle } from "lucide-react";
import Link from "next/link";
import { usePostHog } from "posthog-js/react";
export type DocPopupProps = {
description: React.ReactNode;
link: string;
href: string;
size?: "sm" | "md" | "lg";
};
export default function DocPopup({ description, link, size }: DocPopupProps) {
let sizeClass = "w-4 h-4";
switch (size) {
case "sm": {
sizeClass = "w-4 h-4";
}
case "md": {
sizeClass = "w-6 h-6";
}
case "lg": {
sizeClass = "w-8 h-8";
}
}
export default function DocPopup({
description,
href,
size = "sm",
}: DocPopupProps) {
const sizes = {
sm: "w-4 h-4",
md: "w-6 h-6",
lg: "w-8 h-8",
};
const posthog = usePostHog();
return (
<HoverCard openDelay={200}>
<HoverCardTrigger className="mx-2 cursor-pointer">
<Link href={link} rel="noopener" target="_blank">
<div className="whitespace-nowrap text-gray-500 sm:pl-0">
<HelpCircle className={sizeClass} />
</div>
<HoverCard
openDelay={200}
onOpenChange={(open) => {
if (open) {
posthog.capture("help-popup:opened", { href, description });
}
}}
>
<HoverCardTrigger className="mx-1 cursor-pointer" asChild>
<Link
href={href}
rel="noopener"
target="_blank"
className="inline-block whitespace-nowrap text-gray-500 sm:pl-0"
>
<HelpCircle className={sizes[size]} />
</Link>
</HoverCardTrigger>
<HoverCardContent>
+30 -21
View File
@@ -2,11 +2,14 @@ import { ChevronLeftIcon, ChevronRightIcon } from "@heroicons/react/20/solid";
import Link from "next/link";
import { useRouter } from "next/router";
import { useSession } from "next-auth/react";
import DocPopup from "@/src/components/layouts/doc-popup";
import { type Status, StatusBadge } from "./status-badge";
export default function Header(props: {
title: string;
breadcrumb?: { name: string; href?: string }[];
live?: boolean;
status?: Status;
help?: { description: string; href: string };
actionButtons?: React.ReactNode;
}) {
const router = useRouter();
@@ -16,6 +19,17 @@ export default function Header(props: {
const projectId = router.query.projectId;
const project = session.data?.user?.projects.find((p) => p.id === projectId);
const breadcrumb = [
...(project && projectId && currentPath !== "/project/[projectId]"
? [
{
name: project.name,
href: `/project/${projectId as string}`,
},
]
: []),
...(props.breadcrumb ?? []),
];
const backHref =
props.breadcrumb &&
[...props.breadcrumb.map((i) => i.href).filter(Boolean)].pop();
@@ -37,10 +51,10 @@ export default function Header(props: {
</Link>
</nav>
) : null}
{props.breadcrumb ? (
{breadcrumb.length ? (
<nav className="hidden sm:flex" aria-label="Breadcrumb">
<ol role="list" className="flex items-center space-x-4">
{props.breadcrumb.map(({ name, href }, index) => (
{breadcrumb.map(({ name, href }, index) => (
<li key={index}>
<div className="flex items-center">
{index !== 0 && (
@@ -68,30 +82,25 @@ export default function Header(props: {
</nav>
) : null}
</div>
<div className="mt-2 flex flex-wrap items-center justify-between gap-5">
<div className="mt-2 flex flex-wrap items-center justify-between gap-2">
<div className="flex items-center gap-3 md:gap-5">
<div className="min-w-0">
{project && projectId && currentPath !== "/project/[projectId]" ? (
<div className="text-sm font-medium text-gray-500">
{project.name}
</div>
) : null}
<div className="flex min-w-0 flex-row">
<h2 className="text-2xl font-bold leading-7 text-gray-900 sm:truncate sm:text-3xl sm:tracking-tight">
{props.title}
</h2>
{props.help ? (
<DocPopup
description={props.help.description}
href={props.help.href}
size="sm"
/>
) : null}
</div>
{props.live ? (
<div className="flex items-center gap-2 rounded-sm bg-green-100 px-3 text-green-600">
<span className="relative flex h-2 w-2 ">
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-green-500 opacity-75"></span>
<span className="relative inline-flex h-2 w-2 rounded-full bg-green-600"></span>
</span>
Live
</div>
) : null}
{props.status && <StatusBadge type={props.status} />}
</div>
<div className="flex items-center gap-3">
{props.actionButtons ?? null}
</div>
<div className="md:flex-1" />
{props.actionButtons ?? null}
</div>
</div>
);
+114 -70
View File
@@ -23,6 +23,7 @@ import Head from "next/head";
import { env } from "@/src/env.mjs";
import { LangfuseLogo } from "@/src/components/LangfuseLogo";
import { Spinner } from "@/src/components/layouts/spinner";
import { hasAccess } from "@/src/features/rbac/utils/checkAccess";
const userNavigation = [
{
@@ -36,6 +37,10 @@ const userNavigation = [
const pathsWithoutNavigation: string[] = [];
const unauthenticatedPaths = ["/auth/sign-in", "/auth/sign-up"];
const publishablePaths = [
"/project/[projectId]/sessions/[sessionId]",
"/project/[projectId]/traces/[traceId]",
];
export default function Layout(props: PropsWithChildren) {
const [sidebarOpen, setSidebarOpen] = useState(false);
@@ -54,6 +59,11 @@ export default function Layout(props: PropsWithChildren) {
enableExperimentalFeatures ||
session.data?.user?.featureFlags[featureFlag],
)
.filter(
({ rbacScope }) =>
rbacScope === undefined ||
(projectId && hasAccess({ projectId, scope: rbacScope, session })),
)
.map(({ pathname, ...rest }) => ({
pathname,
href: pathname.replace("[projectId]", projectId ?? ""),
@@ -76,6 +86,7 @@ export default function Layout(props: PropsWithChildren) {
session.data &&
session.data.user === null &&
!unauthenticatedPaths.includes(router.pathname) &&
!publishablePaths.includes(router.pathname) &&
!router.pathname.startsWith("/public/")
) {
void signOut({
@@ -87,9 +98,10 @@ export default function Layout(props: PropsWithChildren) {
if (
session.status === "unauthenticated" &&
!unauthenticatedPaths.includes(router.pathname) &&
!publishablePaths.includes(router.pathname) &&
!router.pathname.startsWith("/public/")
) {
void router.push("/auth/sign-in");
void router.replace("/auth/sign-in");
return <Spinner message="Redirecting" />;
}
@@ -97,7 +109,7 @@ export default function Layout(props: PropsWithChildren) {
session.status === "authenticated" &&
unauthenticatedPaths.includes(router.pathname)
) {
void router.push("/");
void router.replace("/");
return <Spinner message="Redirecting" />;
}
@@ -192,13 +204,13 @@ export default function Layout(props: PropsWithChildren) {
</Transition.Child>
{/* Sidebar component, swap this element with another sidebar if you like */}
<div className="flex grow flex-col gap-y-5 overflow-y-auto bg-white px-6 py-4">
<LangfuseLogo size="xl" />
<LangfuseLogo version size="xl" />
<nav className="flex flex-1 flex-col">
<ul role="list" className="flex flex-1 flex-col gap-y-7">
<li>
<ul role="list" className="-mx-2 space-y-1">
{navigation.map((item) => (
<li key={item.name}>
<li key={item.name} onClick={() => setSidebarOpen(false)}>
<Link
href={item.href}
className={clsx(
@@ -218,6 +230,18 @@ export default function Layout(props: PropsWithChildren) {
aria-hidden="true"
/>
{item.name}
{item.label && (
<span
className={cn(
"self-center whitespace-nowrap break-keep rounded-sm border px-1 py-0.5 text-xs",
item.current
? "border-indigo-600 text-indigo-600"
: "border-gray-200 text-gray-400 group-hover:border-indigo-600 group-hover:text-indigo-600",
)}
>
{item.label}
</span>
)}
</Link>
</li>
))}
@@ -267,7 +291,7 @@ export default function Layout(props: PropsWithChildren) {
{project.role === "VIEWER" ? (
<span
className={cn(
"whitespace-nowrap break-keep rounded-sm border p-1 text-xs",
"self-center whitespace-nowrap break-keep rounded-sm border px-1 py-0.5 text-xs",
projectId === project.id
? "border-indigo-600 text-indigo-600"
: "border-gray-200 text-gray-400 group-hover:border-indigo-600 group-hover:text-indigo-600",
@@ -293,10 +317,10 @@ export default function Layout(props: PropsWithChildren) {
{/* Static sidebar for desktop */}
<div className="hidden xl:fixed xl:inset-y-0 xl:z-50 xl:flex xl:w-72 xl:flex-col">
{/* Sidebar component, swap this element with another sidebar if you like */}
<div className="flex grow flex-col gap-y-5 overflow-y-auto border-r border-gray-200 bg-white px-6 pb-3 pt-7">
<LangfuseLogo size="xl" className="mb-4" />
<nav className="flex flex-1 flex-col">
<ul role="list" className="flex flex-1 flex-col gap-y-4">
<div className="flex h-screen grow flex-col gap-y-5 border-r border-gray-200 bg-white pt-7">
<LangfuseLogo version size="xl" className="mb-2 px-6" />
<nav className="flex h-full flex-1 flex-col overflow-y-auto px-6 pb-3">
<ul role="list" className="flex h-full flex-col gap-y-4">
<li>
<ul role="list" className="-mx-2 space-y-1">
{navigation.map((item) => (
@@ -320,6 +344,18 @@ export default function Layout(props: PropsWithChildren) {
aria-hidden="true"
/>
{item.name}
{item.label && (
<span
className={cn(
"self-center whitespace-nowrap break-keep rounded-sm border px-1 py-0.5 text-xs",
item.current
? "border-indigo-600 text-indigo-600"
: "border-gray-200 text-gray-400 group-hover:border-indigo-600 group-hover:text-indigo-600",
)}
>
{item.label}
</span>
)}
</Link>
</li>
))}
@@ -373,7 +409,7 @@ export default function Layout(props: PropsWithChildren) {
{project.role === "VIEWER" ? (
<span
className={cn(
"whitespace-nowrap break-keep rounded-sm border p-1 text-xs",
"self-center whitespace-nowrap break-keep rounded-sm border px-1 py-0.5 text-xs",
projectId === project.id
? "border-indigo-600 text-indigo-600"
: "border-gray-200 text-gray-400 group-hover:border-indigo-600 group-hover:text-indigo-600",
@@ -387,64 +423,60 @@ export default function Layout(props: PropsWithChildren) {
))}
</ul>
</li>
<li className="-mx-6 ">
<Menu as="div" className="relative">
<Menu.Button className="flex w-full items-center gap-x-4 p-1.5 px-6 py-3 text-sm font-semibold leading-6 text-gray-900 hover:bg-gray-50">
<span className="sr-only">Open user menu</span>
<Avatar className="h-8 w-8">
<AvatarImage
src={session.data?.user?.image ?? undefined}
/>
<AvatarFallback>
{session.data?.user?.name
? session.data.user.name
.split(" ")
.map((word) => word[0])
.slice(0, 2)
.concat("")
: null}
</AvatarFallback>
</Avatar>
<span className="flex-shrink truncate text-sm font-semibold leading-6 text-gray-900">
{session.data?.user?.name}
</span>
<ChevronDownIcon
className="h-5 w-5 text-gray-400"
aria-hidden="true"
/>
</Menu.Button>
<Transition
as={Fragment}
enter="transition ease-out duration-100"
enterFrom="transform opacity-0 scale-95"
enterTo="transform opacity-100 scale-100"
leave="transition ease-in duration-75"
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
<Menu.Items className="absolute -top-full right-0 z-10 mt-2.5 w-32 rounded-md bg-white py-2 shadow-lg ring-1 ring-gray-900/5 focus:outline-none">
{userNavigation.map((item) => (
<Menu.Item key={item.name}>
{({ active }) => (
<a
onClick={() => void item.onClick()}
className={cn(
active ? "bg-gray-50" : "",
"block cursor-pointer px-3 py-1 text-sm leading-6 text-gray-900",
)}
>
{item.name}
</a>
)}
</Menu.Item>
))}
</Menu.Items>
</Transition>
</Menu>
</li>
</ul>
</nav>
<Menu as="div" className="relative left-1">
<Menu.Button className="flex w-full items-center gap-x-4 p-1.5 px-6 py-3 text-sm font-semibold leading-6 text-gray-900 hover:bg-gray-50">
<span className="sr-only">Open user menu</span>
<Avatar className="h-8 w-8">
<AvatarImage src={session.data?.user?.image ?? undefined} />
<AvatarFallback>
{session.data?.user?.name
? session.data.user.name
.split(" ")
.map((word) => word[0])
.slice(0, 2)
.concat("")
: null}
</AvatarFallback>
</Avatar>
<span className="flex-shrink truncate text-sm font-semibold leading-6 text-gray-900">
{session.data?.user?.name}
</span>
<ChevronDownIcon
className="h-5 w-5 text-gray-400"
aria-hidden="true"
/>
</Menu.Button>
<Transition
as={Fragment}
enter="transition ease-out duration-100"
enterFrom="transform opacity-0 scale-95"
enterTo="transform opacity-100 scale-100"
leave="transition ease-in duration-75"
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
<Menu.Items className="absolute -top-full right-0 z-10 mt-2.5 w-32 rounded-md bg-white py-2 shadow-lg ring-1 ring-gray-900/5 focus:outline-none">
{userNavigation.map((item) => (
<Menu.Item key={item.name}>
{({ active }) => (
<a
onClick={() => void item.onClick()}
className={cn(
active ? "bg-gray-50" : "",
"block cursor-pointer px-3 py-1 text-sm leading-6 text-gray-900",
)}
>
{item.name}
</a>
)}
</Menu.Item>
))}
</Menu.Items>
</Transition>
</Menu>
</div>
</div>
@@ -457,7 +489,7 @@ export default function Layout(props: PropsWithChildren) {
<span className="sr-only">Open sidebar</span>
<Bars3Icon className="h-6 w-6" aria-hidden="true" />
</button>
<LangfuseLogo className="flex-1" />
<LangfuseLogo version className="flex-1" />
<Menu as="div" className="relative">
<Menu.Button className="flex items-center gap-x-4 text-sm font-semibold leading-6 text-gray-900">
<span className="sr-only">Open user menu</span>
@@ -506,6 +538,8 @@ export default function Layout(props: PropsWithChildren) {
<div className="xl:pl-72">
{env.NEXT_PUBLIC_DEMO_PROJECT_ID &&
projectId === env.NEXT_PUBLIC_DEMO_PROJECT_ID &&
(env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION === "STAGING" ||
env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION === "EU") &&
!session.data?.user?.email?.endsWith("@langfuse.com") ? (
<div className="flex w-full items-center border-b border-yellow-500 bg-yellow-100 px-4 py-2 xl:sticky xl:top-0 xl:z-40">
<div className="flex flex-1 flex-wrap gap-1">
@@ -515,9 +549,19 @@ export default function Layout(props: PropsWithChildren) {
</div>
<div>Live data from the Langfuse Q&A Chatbot.</div>
</div>
<Button size="sm" variant="ghost" asChild className="ml-2">
<Link href="https://langfuse.com/docs/demo" target="_blank">
Learn more
<Button size="sm" asChild className="ml-2">
<Link
href={
env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION === "EU"
? "https://langfuse.com/docs/qa-chatbot"
: "https://docs-staging.langfuse.com/docs/qa-chatbot"
}
target="_blank"
>
{env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION === "EU"
? "Q&A Chatbot ↗"
: "Q&A Chatbot (staging) ↗"}
</Link>
</Button>
</div>
+17
View File
@@ -1,4 +1,5 @@
import { type Flag } from "@/src/features/feature-flags/types";
import { type Scope } from "@/src/features/rbac/constants/roleAccessRights";
import {
Database,
LayoutDashboard,
@@ -9,6 +10,8 @@ import {
Settings,
TextSelect,
UsersIcon,
Route,
PenSquareIcon,
} from "lucide-react";
export const ROUTES: Array<{
@@ -16,12 +19,19 @@ export const ROUTES: Array<{
pathname: string;
icon: LucideIcon;
featureFlag?: Flag;
label?: string;
rbacScope?: Scope;
}> = [
{
name: "Dashboard",
pathname: `/project/[projectId]`,
icon: LayoutDashboard,
},
{
name: "Sessions",
pathname: `/project/[projectId]/sessions`,
icon: Route,
},
{
name: "Traces",
pathname: `/project/[projectId]/traces`,
@@ -42,6 +52,13 @@ export const ROUTES: Array<{
pathname: `/project/[projectId]/users`,
icon: UsersIcon,
},
{
name: "Prompts",
pathname: "/project/[projectId]/prompts",
icon: PenSquareIcon,
label: "Beta",
rbacScope: "prompts:read",
},
{
name: "Datasets",
pathname: `/project/[projectId]/datasets`,
+43
View File
@@ -0,0 +1,43 @@
import { cn } from "@/src/utils/tailwind";
const statusCategories = {
active: ["production", "live"],
inactive: ["disabled", "inactive"],
};
export type Status =
(typeof statusCategories)[keyof typeof statusCategories][number];
export const StatusBadge = (props: { className?: string; type: Status }) => {
return (
<div
className={cn(
"inline-flex items-center gap-2 rounded-sm px-2 py-1 text-xs",
statusCategories.active.includes(props.type)
? " bg-green-100 text-green-600"
: "bg-gray-100 text-gray-800",
props.className,
)}
>
<span className="relative inline-flex h-2 w-2">
<span
className={cn(
"absolute inline-flex h-full w-full rounded-full opacity-75",
statusCategories.active.includes(props.type)
? "animate-ping bg-green-500"
: "bg-gray-500",
)}
></span>
<span
className={cn(
"relative inline-flex h-2 w-2 rounded-full ",
statusCategories.active.includes(props.type)
? "bg-green-600"
: "bg-gray-600",
)}
></span>
</span>
<span>{props.type}</span>
</div>
);
};
+6
View File
@@ -0,0 +1,6 @@
export const LevelColors = {
DEFAULT: { text: "", bg: "" },
DEBUG: { text: "text-gray-500", bg: "bg-gray-50" },
WARNING: { text: "text-yellow-800", bg: "bg-yellow-50" },
ERROR: { text: "text-red-800", bg: "bg-red-50" },
};
+22
View File
@@ -0,0 +1,22 @@
import { Button } from "@/src/components/ui/button";
import { AlertCircle } from "lucide-react";
import { useSession } from "next-auth/react";
import { useRouter } from "next/router";
export const NoAccessError = () => {
const session = useSession();
const router = useRouter();
return (
<div className="flex h-screen flex-col items-center justify-center">
<AlertCircle className="mb-4 h-12 w-12 text-red-500" />
<h1 className="mb-4 text-xl font-bold">Error</h1>
<p className="mb-8 text-center">
You do not have access to this resource.
</p>
{session.status === "unauthenticated" ? (
<Button onClick={() => void router.push("/")}>Sign In</Button>
) : null}
</div>
);
};
+120
View File
@@ -0,0 +1,120 @@
import { Switch } from "@/src/components/ui/switch";
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: {
traceId: string;
projectId: string;
isPublic: boolean;
}) => {
const posthog = usePostHog();
const hasAccess = useHasAccess({
projectId: props.projectId,
scope: "objects:publish",
});
const utils = api.useUtils();
const mut = api.traces.publish.useMutation({
onSuccess: () => utils.traces.invalidate(),
});
return (
<Base
id={props.traceId}
isPublic={props.isPublic}
onChange={(val) => {
mut.mutate({
projectId: props.projectId,
traceId: props.traceId,
public: val,
});
posthog.capture("trace_detail:publish_trace_button_click");
}}
isLoading={mut.isLoading}
disabled={!hasAccess}
/>
);
};
export const PublishSessionSwitch = (props: {
sessionId: string;
projectId: string;
isPublic: boolean;
}) => {
const posthog = usePostHog();
const hasAccess = useHasAccess({
projectId: props.projectId,
scope: "objects:publish",
});
const utils = api.useUtils();
const mut = api.sessions.publish.useMutation({
onSuccess: () => utils.sessions.invalidate(),
});
return (
<Base
id={props.sessionId}
isPublic={props.isPublic}
onChange={(val) => {
mut.mutate({
projectId: props.projectId,
sessionId: props.sessionId,
public: val,
});
posthog.capture("session_detail:publish_session_button_click");
}}
isLoading={mut.isLoading}
disabled={!hasAccess}
/>
);
};
const Base = (props: {
id: string;
onChange: (value: boolean) => void;
isLoading: boolean;
isPublic: boolean;
disabled?: boolean;
}) => {
const [isCopied, setIsCopied] = useState(false);
const copyUrl = () => {
setIsCopied(true);
void navigator.clipboard.writeText(window.location.href);
setTimeout(() => setIsCopied(false), 1500);
};
return (
<div className="flex items-center gap-3">
<div className="text-sm font-semibold">
{props.isLoading ? (
"Loading.."
) : props.isPublic ? (
<div
className="flex cursor-pointer items-center gap-1 text-green-800"
onClick={() => copyUrl()}
>
{isCopied ? "Link copied ..." : "Public"}
<Link size={16} />
</div>
) : (
<div className="flex items-center gap-1">
Private
<LockIcon size={16} />
</div>
)}
</div>
<Switch
id="publish-trace"
checked={props.isPublic}
onCheckedChange={() => {
if (props.isLoading) return;
props.onChange(!props.isPublic);
}}
disabled={props.disabled}
/>
</div>
);
};
+119
View File
@@ -0,0 +1,119 @@
import { GroupedScoreBadges } from "@/src/components/grouped-score-badge";
import Header from "@/src/components/layouts/header";
import { NoAccessError } from "@/src/components/no-access";
import { PublishSessionSwitch } from "@/src/components/publish-object-switch";
import { StarSessionToggle } from "@/src/components/star-toggle";
import { IOPreview } from "@/src/components/trace/IOPreview";
import { Badge } from "@/src/components/ui/badge";
import { Card } from "@/src/components/ui/card";
import { ManualScoreButton } from "@/src/features/manual-scoring/components/ManualScoreButton";
import { DetailPageNav } from "@/src/features/navigate-detail-pages/DetailPageNav";
import { api } from "@/src/utils/api";
import Link from "next/link";
export const SessionPage: React.FC<{
sessionId: string;
projectId: string;
}> = ({ sessionId, projectId }) => {
const session = api.sessions.byId.useQuery(
{
sessionId,
projectId: projectId,
},
{
retry(failureCount, error) {
if (error.data?.code === "UNAUTHORIZED") return false;
return failureCount < 3;
},
},
);
if (session.error?.data?.code === "UNAUTHORIZED") return <NoAccessError />;
return (
<div className="flex flex-col overflow-hidden xl:container">
<Header
title="Session"
breadcrumb={[
{
name: "Sessions",
href: `/project/${projectId}/sessions`,
},
{ name: sessionId },
]}
actionButtons={[
<StarSessionToggle
key="star"
projectId={projectId}
sessionId={sessionId}
value={session.data?.bookmarked ?? false}
/>,
<PublishSessionSwitch
projectId={projectId}
sessionId={sessionId}
isPublic={session.data?.public ?? false}
key="publish"
/>,
<DetailPageNav
key="nav"
currentId={sessionId}
path={(id) => `/project/${projectId}/sessions/${id}`}
listKey="sessions"
/>,
]}
/>
<div className="flex flex-wrap gap-2">
{session.data?.users.map((userId) => (
<Link key={userId} href={`/project/${projectId}/users/${userId}`}>
<Badge>User ID: {userId}</Badge>
</Link>
))}
<Badge variant="outline">Traces: {session.data?.traces.length}</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}
>
<div className="col-span-2 flex flex-col gap-2 p-0">
{trace.input || trace.output ? (
<IOPreview
key={trace.id}
input={trace.input}
output={trace.output}
hideIfNull
/>
) : (
<div className="p-2 text-xs text-gray-500">
This trace has no input or output.
</div>
)}
</div>
<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>
))}
</div>
</div>
);
};
+122
View File
@@ -0,0 +1,122 @@
import { StarIcon } from "lucide-react";
import { Button } from "@/src/components/ui/button";
import { useEffect, useState } from "react";
import { api } from "@/src/utils/api";
import { useHasAccess } from "@/src/features/rbac/utils/checkAccess";
import { cn } from "@/src/utils/tailwind";
export function StarToggle({
value,
disabled = false,
onClick,
size = "sm",
}: {
value: boolean;
disabled?: boolean;
onClick: (value: boolean) => Promise<unknown>;
size?: "sm" | "xs";
}) {
const [cachedValue, setCachedValue] = useState<boolean | null>(null);
const [loading, setLoading] = useState(false);
const optimisticValue = cachedValue ?? value;
useEffect(() => {
setCachedValue(null);
}, [value]);
const handleBookmarkClick = async () => {
if (disabled) return;
setLoading(true);
setCachedValue(!optimisticValue);
await onClick(!optimisticValue);
setLoading(false);
};
return (
<Button
variant="ghost"
size={size}
onClick={() => void handleBookmarkClick()}
disabled={disabled}
loading={loading}
>
<StarIcon
className={cn(
"h-4 w-4",
optimisticValue ? "fill-current text-yellow-500" : "text-gray-500",
)}
/>
</Button>
);
}
export function StarTraceToggle({
projectId,
traceId,
value,
size = "sm",
}: {
projectId: string;
traceId: string;
value: boolean;
size?: "sm" | "xs";
}) {
const utils = api.useUtils();
const hasAccess = useHasAccess({ projectId, scope: "objects:bookmark" });
const mutBookmarkTrace = api.traces.bookmark.useMutation({
onSuccess: () => {
void utils.traces.invalidate();
},
});
return (
<StarToggle
value={value}
size={size}
disabled={!hasAccess}
onClick={(value) =>
mutBookmarkTrace.mutateAsync({
projectId,
traceId,
bookmarked: value,
})
}
/>
);
}
export function StarSessionToggle({
projectId,
sessionId,
value,
size = "sm",
}: {
projectId: string;
sessionId: string;
value: boolean;
size?: "sm" | "xs";
}) {
const utils = api.useUtils();
const hasAccess = useHasAccess({ projectId, scope: "objects:bookmark" });
const mutBookmarkSession = api.sessions.bookmark.useMutation({
onSuccess: () => {
void utils.sessions.invalidate();
},
});
return (
<StarToggle
value={value}
size={size}
disabled={!hasAccess}
onClick={(value) =>
mutBookmarkSession.mutateAsync({
projectId,
sessionId,
bookmarked: value,
})
}
/>
);
}
@@ -0,0 +1,78 @@
import React, {
useCallback,
type Dispatch,
type SetStateAction,
useState,
} from "react";
import { Button } from "@/src/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuTrigger,
DropdownMenuCheckboxItem,
} from "@/src/components/ui/dropdown-menu";
import { type VisibilityState } from "@tanstack/react-table";
import { ChevronDownIcon } from "lucide-react";
import { type LangfuseColumnDef } from "@/src/components/table/types";
interface DataTableColumnVisibilityFilterProps<TData, TValue> {
columns: LangfuseColumnDef<TData, TValue>[];
columnVisibility: VisibilityState;
setColumnVisibility: Dispatch<SetStateAction<VisibilityState>>;
}
export function DataTableColumnVisibilityFilter<TData, TValue>({
columns,
columnVisibility,
setColumnVisibility,
}: DataTableColumnVisibilityFilterProps<TData, TValue>) {
const [isOpen, setIsOpen] = useState(false);
const toggleColumn = useCallback(
(columnId: string) => {
setColumnVisibility((old) => ({
...old,
[columnId]: !old[columnId],
}));
},
[setColumnVisibility],
);
return (
<DropdownMenu open={isOpen}>
<DropdownMenuTrigger
onClick={() => {
setIsOpen(!isOpen);
}}
className="select-none"
asChild
>
<Button variant="outline" className="ml-auto">
Columns
<ChevronDownIcon className="ml-2 h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent
align="end"
onPointerDownOutside={() => setIsOpen(false)}
>
{columns.map(
(column, index) =>
"accessorKey" in column &&
column.enableHiding && (
<DropdownMenuCheckboxItem
key={index}
className="capitalize"
checked={columnVisibility[column.accessorKey]}
onCheckedChange={() =>
toggleColumn(column.accessorKey.toString())
}
>
{column.header?.toString() ?? column.accessorKey.toString()}
</DropdownMenuCheckboxItem>
),
)}
</DropdownMenuContent>
</DropdownMenu>
);
}
@@ -1,151 +0,0 @@
import * as React from "react";
import { Check, PlusCircle } from "lucide-react";
import { Badge } from "@/src/components/ui/badge";
import { Button } from "@/src/components/ui/button";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/src/components/ui/popover";
import { Separator } from "@/src/components/ui/separator";
import { type KeyValueFilter } from "@/src/utils/tanstack";
import { Input } from "@/src/components/ui/input";
import {
Command,
CommandEmpty,
CommandGroup,
CommandItem,
CommandList,
CommandSeparator,
} from "@/src/components/ui/command";
import { cn } from "@/src/utils/tailwind";
interface DataTableKeyValueFilter {
title?: string;
meta: KeyValueFilter;
}
export function DataTableKeyValueFilter({
title,
meta,
}: DataTableKeyValueFilter) {
const selectedValues = new Set(meta.values);
const [key, setKey] = React.useState("");
const [value, setValue] = React.useState("");
const clearFilter = () => {
meta.updateFunction(null);
};
const filter = () => {
meta.updateFunction({ key, value });
setKey("");
setValue("");
};
return (
<Popover>
<PopoverTrigger asChild>
<Button variant="outline" size="sm" className="h-8 border-dashed">
<PlusCircle className="mr-2 h-4 w-4" />
{title}
{selectedValues
? Array.from(selectedValues.values()).map((value) => (
<>
<Separator orientation="vertical" className="mx-2 h-4" />
<Badge
variant="secondary"
key={`${value.key}-${value.value}`}
className="rounded-sm px-1 font-normal"
>
{value.key}-{value.value}
</Badge>
</>
))
: undefined}
</Button>
</PopoverTrigger>
<PopoverContent className="w-[200px] p-0" align="start">
<Command>
<CommandList>
<CommandEmpty>No results found.</CommandEmpty>
<CommandGroup>
{Array.from(selectedValues.values()).map((selectedValue) => {
return (
<CommandItem
key={selectedValue.key}
onSelect={() => {
meta.removeSelectedValue({
key: selectedValue.key,
value: selectedValue.value,
});
}}
>
<div
className={
"mr-2 flex h-4 w-4 items-center justify-center rounded-sm border border-primary bg-primary text-primary-foreground"
}
>
<Check className={cn("h-4 w-4")} />
</div>
<span>
{selectedValue.key} - {selectedValue.value}
</span>
</CommandItem>
);
})}
</CommandGroup>
{selectedValues.size > 0 ? <Separator /> : undefined}
<div className="flex flex-row justify-end p-2">
<div className="flex flex-col space-y-1.5">
<Input
id="name-key"
placeholder="Key"
value={key}
onChange={(e) => setKey(e.target.value)}
/>
</div>
<div className="flex flex-col space-y-1.5">
<Input
id="name-value"
placeholder="Value"
value={value}
onChange={(e) => setValue(e.target.value)}
/>
</div>
</div>
<CommandSeparator />
<CommandGroup>
<CommandItem
onSelect={() => filter()}
className="justify-center text-center"
>
Filter
</CommandItem>
</CommandGroup>
{meta.values && (
<>
<CommandSeparator />
<CommandGroup>
<CommandItem
onSelect={() => clearFilter()}
className="justify-center text-center"
>
Clear filters
</CommandItem>
</CommandGroup>
</>
)}
</CommandList>
</Command>
</PopoverContent>
</Popover>
);
}
@@ -0,0 +1,114 @@
import { ChevronDown, Trash } from "lucide-react";
import {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
} from "@/src/components/ui/dropdown-menu";
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,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/src/components/ui/dialog";
import { useState } from "react";
export function TraceTableMultiSelectAction({
selectedTraceIds,
projectId,
onDeleteSuccess,
}: {
selectedTraceIds: string[];
projectId: string;
onDeleteSuccess: () => void;
}) {
const utils = api.useUtils();
const [open, setOpen] = useState(false);
const posthog = usePostHog();
const hasDeleteAccess = useHasAccess({ projectId, scope: "traces:delete" });
const mutDeleteTraces = api.traces.deleteMany.useMutation({
onSuccess: () => {
onDeleteSuccess();
void utils.traces.invalidate();
},
});
return (
<>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="outline"
className="bg-white p-2 font-medium text-black"
disabled={selectedTraceIds.length < 1}
>
Actions ({selectedTraceIds.length} selected)
<ChevronDown className="h-5 w-5" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem
disabled={!hasDeleteAccess}
onClick={() => {
setOpen(true);
}}
>
<Trash className="mr-2 h-4 w-4" />
<span>Delete</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<Dialog
open={open}
onOpenChange={(isOpen) => {
if (!isOpen) {
setOpen(false);
}
}}
>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle>Delete traces</DialogTitle>
<DialogDescription>
This action cannot be undone and removes all the data associated
with these traces.
</DialogDescription>
</DialogHeader>
<DialogFooter className="sm:justify-start">
<Button
type="button"
variant="destructive"
loading={mutDeleteTraces.isLoading}
disabled={mutDeleteTraces.isLoading}
onClick={() => {
void mutDeleteTraces
.mutateAsync({
traceIds: selectedTraceIds,
projectId,
})
.then(() => {
setOpen(false);
});
posthog.capture("trace:delete", {
count: selectedTraceIds.length,
source: "table-multi-select",
});
}}
>
Delete {selectedTraceIds.length} trace(s)
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</>
);
}
@@ -1,204 +0,0 @@
import * as React from "react";
import { Check, PlusCircle } from "lucide-react";
import { cn } from "@/src/utils/tailwind";
import { Badge } from "@/src/components/ui/badge";
import { Button } from "@/src/components/ui/button";
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
CommandSeparator,
} from "@/src/components/ui/command";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/src/components/ui/popover";
import { Separator } from "@/src/components/ui/separator";
import { type TableRowOptions } from "@/src/components/table/types";
import { type NumberComparisonFilter } from "@/src/utils/tanstack";
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/src/components/ui/select";
import { Input } from "@/src/components/ui/input";
interface DataTableNumberFilter {
title?: string;
meta: NumberComparisonFilter;
options: TableRowOptions;
}
export function DataTableNumberFilter({
title,
meta,
options,
}: DataTableNumberFilter) {
const clearFilter = () => {
meta.selectedValues = { name: null, operator: null, value: null };
meta.updateFunction(null);
};
const filter = () => {
if (
meta.selectedValues?.operator &&
meta.selectedValues?.value !== null &&
meta.selectedValues?.name
) {
meta.updateFunction({
name: meta.selectedValues?.name,
operator: meta.selectedValues.operator,
value: meta.selectedValues.value,
});
}
};
return (
<Popover>
<PopoverTrigger asChild>
<Button variant="outline" size="sm" className="h-8 border-dashed">
<PlusCircle className="mr-2 h-4 w-4" />
{title}
{meta.values ? (
<>
<Separator orientation="vertical" className="mx-2 h-4" />
<Badge
variant="secondary"
key={meta.values.name}
className="rounded-sm px-1 font-normal"
>
{meta.values.name}-{meta.values.operator}-{meta.values.value}
</Badge>
</>
) : undefined}
</Button>
</PopoverTrigger>
<PopoverContent className="w-[200px] p-0" align="start">
<Command>
<CommandInput placeholder={title} />
<CommandList>
<CommandEmpty>No results found.</CommandEmpty>
<CommandGroup>
{options.options.map((option) => {
const isSelected =
meta.selectedValues?.name &&
meta.selectedValues?.name === option.label;
return (
<CommandItem
key={option.label}
onSelect={() => {
if (isSelected) {
meta.selectedValues.name = null;
} else {
meta.updateSelectedScores({
...meta.selectedValues,
name: option.label,
});
}
}}
>
<div
className={cn(
"mr-2 flex h-4 w-4 items-center justify-center rounded-sm border border-primary",
isSelected
? "bg-primary text-primary-foreground"
: "opacity-50 [&_svg]:invisible",
)}
>
<Check className={cn("h-4 w-4")} />
</div>
{option.icon && (
<option.icon className="mr-2 h-4 w-4 text-muted-foreground" />
)}
<span>{option.label}</span>
{option.value && (
<span className="ml-auto flex h-4 w-4 items-center justify-center font-mono text-xs">
{option.value}
</span>
)}
</CommandItem>
);
})}
</CommandGroup>
<Separator />
<div className="flex flex-row justify-end p-2">
<Select
onValueChange={(operator) => {
if (
operator === "gt" ||
operator === "gte" ||
operator === "lt" ||
operator === "lte" ||
operator === "equals"
) {
meta.updateSelectedScores({
...meta.selectedValues,
operator: operator,
});
}
}}
value={meta.selectedValues.operator ?? undefined}
>
<SelectTrigger className="w-[180px]">
<SelectValue placeholder="Operator" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem value="equals">equals</SelectItem>
<SelectItem value="gt">gt</SelectItem>
<SelectItem value="lt">lt</SelectItem>
<SelectItem value="gte">gte</SelectItem>
<SelectItem value="lte">lte</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
<Input
type="number"
placeholder="Value"
value={meta.selectedValues.value ?? undefined}
onChange={(event) =>
meta.updateSelectedScores({
...meta.selectedValues,
value: Number(event.currentTarget.value),
})
}
/>
</div>
<>
<CommandSeparator />
<CommandGroup>
<CommandItem
onSelect={() => filter()}
className="justify-center text-center"
>
Filter
</CommandItem>
</CommandGroup>
</>
{meta.values && (
<>
<CommandSeparator />
<CommandGroup>
<CommandItem
onSelect={() => clearFilter()}
className="justify-center text-center"
>
Clear filters
</CommandItem>
</CommandGroup>
</>
)}
</CommandList>
</Command>
</PopoverContent>
</Popover>
);
}
@@ -1,144 +0,0 @@
import * as React from "react";
import { Check, PlusCircle } from "lucide-react";
import { cn } from "@/src/utils/tailwind";
import { Badge } from "@/src/components/ui/badge";
import { Button } from "@/src/components/ui/button";
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
CommandSeparator,
} from "@/src/components/ui/command";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/src/components/ui/popover";
import { Separator } from "@/src/components/ui/separator";
import { type TableRowOptions } from "@/src/components/table/types";
import { type SelectFilter } from "@/src/utils/tanstack";
interface DataTableSelectFilter {
title?: string;
meta: SelectFilter;
options: TableRowOptions;
}
export function DataTableSelectFilter({
title,
meta,
options,
}: DataTableSelectFilter) {
const selectedValues = new Set(meta.values ?? []);
return (
<Popover>
<PopoverTrigger asChild>
<Button variant="outline" size="sm" className="h-8 border-dashed">
<PlusCircle className="mr-2 h-4 w-4" />
{title}
{selectedValues?.size > 0 && (
<>
<Separator orientation="vertical" className="mx-2 h-4" />
<Badge
variant="secondary"
className="rounded-sm px-1 font-normal lg:hidden"
>
{selectedValues.size}
</Badge>
<div className="hidden space-x-1 lg:flex">
{selectedValues.size > 2 ? (
<Badge
variant="secondary"
className="rounded-sm px-1 font-normal"
>
{selectedValues.size} selected
</Badge>
) : (
options.options
.filter((option) => selectedValues.has(option.label))
.map((option) => (
<Badge
variant="secondary"
key={option.value}
className="rounded-sm px-1 font-normal"
>
{option.label}
</Badge>
))
)}
</div>
</>
)}
</Button>
</PopoverTrigger>
<PopoverContent className="w-[200px] p-0" align="start">
<Command>
<CommandInput placeholder={title} />
<CommandList>
<CommandEmpty>No results found.</CommandEmpty>
<CommandGroup>
{options.options.map((option) => {
const isSelected = selectedValues.has(option.label);
return (
<CommandItem
key={option.label}
onSelect={() => {
if (isSelected) {
selectedValues.delete(option.label);
} else {
selectedValues.add(option.label);
}
const filterValues = Array.from(selectedValues);
meta.updateFunction(
filterValues.length ? filterValues : null,
);
}}
>
<div
className={cn(
"mr-2 flex h-4 w-4 items-center justify-center rounded-sm border border-primary",
isSelected
? "bg-primary text-primary-foreground"
: "opacity-50 [&_svg]:invisible",
)}
>
<Check className={cn("h-4 w-4")} />
</div>
{option.icon && (
<option.icon className="mr-2 h-4 w-4 text-muted-foreground" />
)}
<span>{option.label}</span>
{option.value && (
<span className="ml-auto flex h-4 w-4 items-center justify-center font-mono text-xs">
{option.value}
</span>
)}
</CommandItem>
);
})}
</CommandGroup>
{selectedValues.size > 0 && (
<>
<CommandSeparator />
<CommandGroup>
<CommandItem
onSelect={() => meta.updateFunction(null)}
className="justify-center text-center"
>
Clear filters
</CommandItem>
</CommandGroup>
</>
)}
</CommandList>
</Command>
</PopoverContent>
</Popover>
);
}
+28 -10
View File
@@ -1,9 +1,12 @@
import { Button } from "@/src/components/ui/button";
import React, { type Dispatch, type SetStateAction, useState } from "react";
import { Input } from "@/src/components/ui/input";
import { DataTableColumnVisibilityFilter } from "@/src/components/table/data-table-column-visibility-filter";
import { type FilterState } from "@/src/features/filters/types";
import { FilterBuilder } from "@/src/features/filters/components/filter-builder";
import { type ColumnDefinition } from "@/src/server/api/interfaces/tableDefinition";
import { type VisibilityState } from "@tanstack/react-table";
import { type LangfuseColumnDef } from "@/src/components/table/types";
interface SearchConfig {
placeholder: string;
@@ -11,21 +14,27 @@ interface SearchConfig {
currentQuery?: string;
}
interface DataTableToolbarProps {
interface DataTableToolbarProps<TData, TValue> {
columns: LangfuseColumnDef<TData, TValue>[];
filterColumnDefinition: ColumnDefinition[];
searchConfig?: SearchConfig;
actionButtons?: React.ReactNode;
filterState: FilterState;
setFilterState: Dispatch<SetStateAction<FilterState>>;
columnVisibility?: VisibilityState;
setColumnVisibility?: Dispatch<SetStateAction<VisibilityState>>;
}
export function DataTableToolbar({
export function DataTableToolbar<TData, TValue>({
columns,
filterColumnDefinition,
searchConfig,
actionButtons,
filterState,
setFilterState,
}: DataTableToolbarProps) {
columnVisibility,
setColumnVisibility,
}: DataTableToolbarProps<TData, TValue>) {
const [searchString, setSearchString] = useState(
searchConfig?.currentQuery ?? "",
);
@@ -33,33 +42,42 @@ export function DataTableToolbar({
return (
<div className="my-2 flex max-w-full items-center justify-between overflow-x-auto">
<div className="flex flex-1 items-center space-x-2">
{searchConfig ? (
{searchConfig && (
<div className="flex max-w-md items-center space-x-2">
<Input
autoFocus
placeholder={searchConfig.placeholder}
value={searchString}
className="h-10 w-[200px] lg:w-[350px]"
onChange={(event) => {
setSearchString(event.currentTarget.value);
onChange={(event) => setSearchString(event.currentTarget.value)}
onKeyDown={(event) => {
if (event.key === "Enter") {
searchConfig.updateQuery(searchString);
}
}}
className="h-10 w-[200px] lg:w-[350px]"
/>
<Button
variant="outline"
type="submit"
onClick={() => searchConfig.updateQuery(searchString)}
>
Search
</Button>
</div>
) : undefined}
)}
<FilterBuilder
columns={filterColumnDefinition}
filterState={filterState}
onChange={setFilterState}
/>
<div className="flex-1" />
{actionButtons ? actionButtons : null}
{!!columnVisibility && !!setColumnVisibility && (
<DataTableColumnVisibilityFilter
columns={columns}
columnVisibility={columnVisibility}
setColumnVisibility={setColumnVisibility}
/>
)}
{actionButtons}
</div>
</div>
);
+108 -29
View File
@@ -1,15 +1,8 @@
"use client";
import {
type ColumnDef,
flexRender,
getCoreRowModel,
useReactTable,
type ColumnFiltersState,
getFilteredRowModel,
type OnChangeFn,
type PaginationState,
} from "@tanstack/react-table";
import DocPopup from "@/src/components/layouts/doc-popup";
import { DataTablePagination } from "@/src/components/table/data-table-pagination";
import { type LangfuseColumnDef } from "@/src/components/table/types";
import {
Table,
TableBody,
@@ -18,17 +11,36 @@ import {
TableHeader,
TableRow,
} from "@/src/components/ui/table";
import { type OrderByState } from "@/src/features/orderBy/types";
import { cn } from "@/src/utils/tailwind";
import {
flexRender,
getCoreRowModel,
getFilteredRowModel,
useReactTable,
type ColumnFiltersState,
type OnChangeFn,
type PaginationState,
type RowSelectionState,
type VisibilityState,
} from "@tanstack/react-table";
import { useState } from "react";
import { DataTablePagination } from "@/src/components/table/data-table-pagination";
interface DataTableProps<TData, TValue> {
columns: ColumnDef<TData, TValue>[];
columns: LangfuseColumnDef<TData, TValue>[];
data: AsyncTableData<TData[]>;
pagination?: {
pageCount: number;
onChange: OnChangeFn<PaginationState>;
state: PaginationState;
};
rowSelection?: RowSelectionState;
setRowSelection?: OnChangeFn<RowSelectionState>;
columnVisibility?: VisibilityState;
onColumnVisibilityChange?: OnChangeFn<VisibilityState>;
orderBy?: OrderByState;
setOrderBy?: (s: OrderByState) => void;
help?: { description: string; href: string };
}
export interface AsyncTableData<T> {
@@ -38,10 +50,17 @@ export interface AsyncTableData<T> {
error?: string;
}
export function DataTable<TData, TValue>({
export function DataTable<TData extends object, TValue>({
columns,
data,
pagination,
rowSelection,
setRowSelection,
columnVisibility,
onColumnVisibilityChange,
help,
orderBy,
setOrderBy,
}: DataTableProps<TData, TValue>) {
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
@@ -54,9 +73,20 @@ export function DataTable<TData, TValue>({
manualPagination: pagination !== undefined,
pageCount: pagination?.pageCount ?? 0,
onPaginationChange: pagination?.onChange,
onRowSelectionChange: setRowSelection,
onColumnVisibilityChange: onColumnVisibilityChange,
getRowId: (row, index) => {
if ("id" in row && typeof row.id === "string") {
return row.id;
} else {
return index.toString();
}
},
state: {
columnFilters,
pagination: pagination?.state,
columnVisibility,
rowSelection,
},
manualFiltering: true,
});
@@ -70,19 +100,56 @@ export function DataTable<TData, TValue>({
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => {
return (
const sortingEnabled =
header.column.columnDef.enableSorting;
return header.column.getIsVisible() ? (
<TableHead
key={header.id}
className="whitespace-nowrap p-2"
className={cn(
sortingEnabled ? "cursor-pointer" : null,
"whitespace-nowrap p-2",
)}
title={sortingEnabled ? "Sort by this column" : ""}
onClick={(event) => {
event.preventDefault(); // Add this line
if (
!setOrderBy ||
!header.column.columnDef.id ||
!sortingEnabled
) {
return;
}
if (orderBy?.column === header.column.columnDef.id) {
setOrderBy({
column: header.column.columnDef.id,
order: orderBy.order === "ASC" ? "DESC" : "ASC",
});
} else {
setOrderBy({
column: header.column.columnDef.id,
order: "DESC",
});
}
}}
>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext(),
)}
{header.isPlaceholder ? null : (
<>
<div className="select-none">
{flexRender(
header.column.columnDef.header,
header.getContext(),
)}
{orderBy?.column === header.column.columnDef.id
? renderOrderingIndicator(orderBy)
: null}
</div>
</>
)}
</TableHead>
);
) : null;
})}
</TableRow>
))}
@@ -97,16 +164,13 @@ export function DataTable<TData, TValue>({
Loading...
</TableCell>
</TableRow>
) : table.getRowModel().rows?.length ? (
) : table.getRowModel().rows.length ? (
table.getRowModel().rows.map((row) => (
<TableRow
key={row.id}
data-state={row.getIsSelected() && "selected"}
>
<TableRow key={row.id}>
{row.getVisibleCells().map((cell) => (
<TableCell
key={cell.id}
className="overflow-hidden whitespace-nowrap p-2 text-xs"
className="overflow-hidden whitespace-nowrap px-2 py-1 text-xs first:pl-2"
>
{flexRender(
cell.column.columnDef.cell,
@@ -122,7 +186,16 @@ export function DataTable<TData, TValue>({
colSpan={columns.length}
className="h-24 text-center"
>
No results.
<div>
No results.{" "}
{help && (
<DocPopup
description={help.description}
href={help.href}
size="sm"
/>
)}
</div>
</TableCell>
</TableRow>
)}
@@ -134,3 +207,9 @@ export function DataTable<TData, TValue>({
</>
);
}
function renderOrderingIndicator(orderBy?: OrderByState) {
if (!orderBy) return;
if (orderBy.order === "ASC") return <span className="ml-1"></span>;
else return <span className="ml-1"></span>;
}
+10 -16
View File
@@ -1,4 +1,4 @@
import { useRouter } from "next/router";
import Link from "next/link";
export type TableLinkProps = {
path: string;
@@ -11,21 +11,15 @@ export default function TableLink({
value,
truncateAt = 7,
}: TableLinkProps) {
const router = useRouter();
return (
<div>
<button
key="openTrace"
className="rounded bg-indigo-50 px-2 py-1 text-xs font-semibold text-blue-600 shadow-sm hover:bg-indigo-100"
onClick={() => {
void router.push(path);
}}
>
{value.length > truncateAt
? `...${value.substring(value.length - truncateAt)}`
: value}
</button>
</div>
<Link
className="inline-block rounded bg-indigo-50 px-2 py-1 text-xs font-semibold text-blue-600 shadow-sm hover:bg-indigo-100"
href={path}
title={value}
>
{value.length > truncateAt
? `...${value.substring(value.length - truncateAt)}`
: value}
</Link>
);
}
+6
View File
@@ -1,6 +1,12 @@
import { type RowData, type ColumnDef } from "@tanstack/react-table";
import { type LucideIcon } from "lucide-react";
export type TableRowOptions = {
columnId: string;
options: { label: string; value: number; icon?: LucideIcon }[];
};
export type LangfuseColumnDef<
TData extends RowData,
TValue = unknown,
> = ColumnDef<TData, TValue> & { defaultHidden?: boolean };
@@ -0,0 +1,405 @@
import { api, directApi } from "@/src/utils/api";
import { DataTable } from "@/src/components/table/data-table";
import TableLink from "@/src/components/table/table-link";
import { DataTableToolbar } from "@/src/components/table/data-table-toolbar";
import { useState } from "react";
import { TokenUsageBadge } from "@/src/components/token-usage-badge";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} 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,
useQueryParam,
useQueryParams,
withDefault,
} from "use-query-params";
import { useQueryFilterState } from "@/src/features/filters/hooks/useFilterState";
import { observationsTableColsWithOptions } from "@/src/server/api/definitions/observationsTable";
import { formatInterval, utcDateOffsetByDays } from "@/src/utils/dates";
import useColumnVisibility from "@/src/features/column-visibility/hooks/useColumnVisibility";
import { JSONView } from "@/src/components/ui/code";
import { type LangfuseColumnDef } from "@/src/components/table/types";
import { type ObservationLevel } from "@prisma/client";
import { cn } from "@/src/utils/tailwind";
import { LevelColors } from "@/src/components/level-colors";
import { usdFormatter } from "@/src/utils/numbers";
import {
exportOptions,
type ExportFileFormats,
} from "@/src/server/api/interfaces/exportTypes";
export type GenerationsTableRow = {
id: string;
traceId: string;
startTime: string;
level?: ObservationLevel;
statusMessage?: string;
endTime?: string;
latency?: number;
name?: string;
model?: string;
input?: unknown;
output?: unknown;
traceName?: string;
metadata?: string;
usage: {
promptTokens: number;
completionTokens: number;
totalTokens: number;
};
};
export type GenerationsTableProps = {
projectId: string;
};
export default function GenerationsTable({ projectId }: GenerationsTableProps) {
const posthog = usePostHog();
const [isExporting, setIsExporting] = useState(false);
const [searchQuery, setSearchQuery] = useQueryParam(
"search",
withDefault(StringParam, null),
);
const [paginationState, setPaginationState] = useQueryParams({
pageIndex: withDefault(NumberParam, 0),
pageSize: withDefault(NumberParam, 50),
});
const [filterState, setFilterState] = useQueryFilterState([
{
column: "start_time",
type: "datetime",
operator: ">",
value: utcDateOffsetByDays(-14),
},
]);
const generations = api.generations.all.useQuery({
page: paginationState.pageIndex,
limit: paginationState.pageSize,
projectId,
filter: filterState,
searchQuery,
});
const totalCount = generations.data?.slice(1)[0]?.totalCount ?? 0;
const filterOptions = api.generations.filterOptions.useQuery({
projectId,
});
const handleExport = async (fileFormat: ExportFileFormats) => {
if (isExporting) return;
setIsExporting(true);
posthog.capture("generations:export", { file_format: fileFormat });
try {
const fileData = await directApi.generations.export.query({
projectId,
fileFormat,
filter: filterState,
searchQuery,
});
let url: string;
if (fileData.type === "s3") {
url = fileData.url;
} else {
const file = new File([fileData.data], fileData.fileName, {
type: exportOptions[fileFormat].fileType,
});
// create url from file
url = URL.createObjectURL(file);
}
// Use a dynamically created anchor element to trigger the download
const a = document.createElement("a");
document.body.appendChild(a);
a.href = url;
a.download = fileData.fileName; // name of the downloaded file
a.click();
a.remove();
// Revoke the blob URL after using it
if (fileData.type === "data") {
setTimeout(() => URL.revokeObjectURL(url), 100);
}
setIsExporting(false);
} catch (e) {
console.error(e);
setIsExporting(false);
}
};
const columns: LangfuseColumnDef<GenerationsTableRow>[] = [
{
accessorKey: "id",
header: "ID",
cell: ({ row }) => {
const observationId = row.getValue("id");
const traceId = row.getValue("traceId");
return typeof observationId === "string" &&
typeof traceId === "string" ? (
<TableLink
path={`/project/${projectId}/traces/${traceId}?observation=${observationId}`}
value={observationId}
/>
) : null;
},
},
{
accessorKey: "name",
header: "name",
},
{
accessorKey: "traceId",
header: "Trace ID",
cell: ({ row }) => {
const value = row.getValue("traceId");
return typeof value === "string" ? (
<TableLink
path={`/project/${projectId}/traces/${value}`}
value={value}
/>
) : undefined;
},
},
{
accessorKey: "traceName",
header: "Trace Name",
enableHiding: true,
},
{
accessorKey: "startTime",
header: "Start Time",
enableHiding: true,
},
{
accessorKey: "latency",
header: "Latency",
cell: ({ row }) => {
const value: number | undefined = row.getValue("latency");
return value !== undefined ? (
<span>{formatInterval(value)}</span>
) : undefined;
},
enableHiding: true,
},
{
accessorKey: "cost",
header: "Cost",
cell: ({ row }) => {
const value: number | undefined = row.getValue("cost");
return value !== undefined ? (
<span>{usdFormatter(value)}</span>
) : undefined;
},
enableHiding: true,
},
{
accessorKey: "level",
header: "Level",
enableHiding: true,
cell({ row }) {
const value: ObservationLevel | undefined = row.getValue("level");
return value ? (
<span
className={cn(
"rounded-sm p-0.5 text-xs",
LevelColors[value].bg,
LevelColors[value].text,
)}
>
{value}
</span>
) : undefined;
},
},
{
accessorKey: "statusMessage",
header: "Status Message",
enableHiding: true,
defaultHidden: true,
},
{
accessorKey: "model",
header: "Model",
enableHiding: true,
},
{
accessorKey: "usage",
header: "Usage",
cell: ({ row }) => {
const value: {
promptTokens: number;
completionTokens: number;
totalTokens: number;
} = row.getValue("usage");
return (
<TokenUsageBadge
promptTokens={value.promptTokens}
completionTokens={value.completionTokens}
totalTokens={value.totalTokens}
inline
/>
);
},
enableHiding: true,
},
{
accessorKey: "input",
header: "Input",
cell: ({ row }) => {
const value: unknown = row.getValue("input");
return <JSONView json={value} className="w-[500px]" />;
},
enableHiding: true,
defaultHidden: true,
},
{
accessorKey: "output",
header: "Output",
cell: ({ row }) => {
const value: unknown = row.getValue("output");
return <JSONView json={value} className="w-[500px] bg-green-50" />;
},
enableHiding: true,
defaultHidden: true,
},
{
accessorKey: "metadata",
header: "Metadata",
cell: ({ row }) => {
const values: string | undefined = row.getValue("metadata");
return <div className="flex flex-wrap gap-x-3 gap-y-1">{values}</div>;
},
enableHiding: true,
defaultHidden: true,
},
{
accessorKey: "version",
header: "Version",
enableHiding: true,
},
];
const [columnVisibility, setColumnVisibility] =
useColumnVisibility<GenerationsTableRow>(
"generationsColumnVisibility",
columns,
);
const rows: GenerationsTableRow[] = generations.isSuccess
? generations.data.map((generation) => {
return {
id: generation.id,
traceId: generation.traceId,
traceName: generation.traceName,
startTime: generation.startTime.toLocaleString(),
endTime: generation.endTime?.toLocaleString() ?? undefined,
latency: generation.latency === null ? undefined : generation.latency,
cost: generation.cost,
name: generation.name ?? undefined,
version: generation.version ?? "",
model: generation.model ?? "",
input: generation.input,
output: generation.output,
level: generation.level,
metadata: generation.metadata
? JSON.stringify(generation.metadata)
: undefined,
statusMessage: generation.statusMessage ?? undefined,
usage: {
promptTokens: generation.promptTokens,
completionTokens: generation.completionTokens,
totalTokens: generation.totalTokens,
},
};
})
: [];
return (
<div>
<DataTableToolbar
columns={columns}
filterColumnDefinition={observationsTableColsWithOptions(
filterOptions.data,
)}
filterState={filterState}
setFilterState={setFilterState}
searchConfig={{
placeholder: "Search by id, name, traceName, model",
updateQuery: setSearchQuery,
currentQuery: searchQuery ?? undefined,
}}
columnVisibility={columnVisibility}
setColumnVisibility={setColumnVisibility}
actionButtons={
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="outline"
className="ml-auto whitespace-nowrap"
size="sm"
>
{filterState.length > 0 || searchQuery
? "Export selection"
: "Export all"}{" "}
{isExporting ? (
<Loader className="ml-2 h-4 w-4 animate-spin" />
) : (
<ChevronDownIcon className="ml-2 h-4 w-4" />
)}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
{Object.entries(exportOptions).map(([key, options]) => (
<DropdownMenuItem
key={key}
className="capitalize"
onClick={() => void handleExport(key as ExportFileFormats)}
>
as {options.label}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
}
/>
<DataTable
columns={columns}
data={
generations.isLoading
? { isLoading: true, isError: false }
: generations.isError
? {
isLoading: false,
isError: true,
error: generations.error.message,
}
: {
isLoading: false,
isError: false,
data: rows,
}
}
pagination={{
pageCount: Math.ceil(totalCount / paginationState.pageSize),
onChange: setPaginationState,
state: paginationState,
}}
columnVisibility={columnVisibility}
onColumnVisibilityChange={setColumnVisibility}
/>
</div>
);
}
+27 -14
View File
@@ -1,15 +1,16 @@
import { DataTable } from "@/src/components/table/data-table";
import { DataTableToolbar } from "@/src/components/table/data-table-toolbar";
import TableLink from "@/src/components/table/table-link";
import { type LangfuseColumnDef } from "@/src/components/table/types";
import useColumnVisibility from "@/src/features/column-visibility/hooks/useColumnVisibility";
import { useQueryFilterState } from "@/src/features/filters/hooks/useFilterState";
import { scoresTableColsWithOptions } from "@/src/server/api/definitions/scoresTable";
import { api } from "@/src/utils/api";
import { type RouterInput } from "@/src/utils/types";
import { type Score } from "@prisma/client";
import { type ColumnDef } from "@tanstack/react-table";
import { useQueryParams, withDefault, NumberParam } from "use-query-params";
type RowData = {
export type ScoresTableRow = {
id: string;
traceId: string;
timestamp: string;
@@ -60,7 +61,7 @@ export default function ScoresTable({
projectId,
});
const columns: ColumnDef<RowData>[] = [
const columns: LangfuseColumnDef<ScoresTableRow>[] = [
{
accessorKey: "traceId",
enableColumnFilter: true,
@@ -95,22 +96,29 @@ export default function ScoresTable({
{
accessorKey: "timestamp",
header: "Timestamp",
enableHiding: true,
},
{
accessorKey: "name",
header: "Name",
enableHiding: true,
},
{
accessorKey: "value",
header: "Value",
enableHiding: true,
},
{
accessorKey: "comment",
header: "Comment",
enableHiding: true,
},
];
const convertToTableRow = (score: Score): RowData => {
const [columnVisibility, setColumnVisibility] =
useColumnVisibility<ScoresTableRow>("scoresColumnVisibility", columns);
const convertToTableRow = (score: Score): ScoresTableRow => {
return {
id: score.id,
timestamp: score.timestamp.toLocaleString(),
@@ -125,9 +133,12 @@ export default function ScoresTable({
return (
<div>
<DataTableToolbar
columns={columns}
filterColumnDefinition={scoresTableColsWithOptions(filterOptions.data)}
filterState={userFilterState}
setFilterState={setUserFilterState}
columnVisibility={columnVisibility}
setColumnVisibility={setColumnVisibility}
/>
<DataTable
columns={columns}
@@ -135,22 +146,24 @@ export default function ScoresTable({
scores.isLoading
? { isLoading: true, isError: false }
: scores.isError
? {
isLoading: false,
isError: true,
error: scores.error.message,
}
: {
isLoading: false,
isError: false,
data: scores.data?.map((t) => convertToTableRow(t)),
}
? {
isLoading: false,
isError: true,
error: scores.error.message,
}
: {
isLoading: false,
isError: false,
data: scores.data.map((t) => convertToTableRow(t)),
}
}
pagination={{
pageCount: Math.ceil(totalCount / paginationState.pageSize),
onChange: setPaginationState,
state: paginationState,
}}
columnVisibility={columnVisibility}
onColumnVisibilityChange={setColumnVisibility}
/>
</div>
);
+217
View File
@@ -0,0 +1,217 @@
import { StarSessionToggle } from "@/src/components/star-toggle";
import { DataTable } from "@/src/components/table/data-table";
import { DataTableToolbar } from "@/src/components/table/data-table-toolbar";
import TableLink from "@/src/components/table/table-link";
import { type LangfuseColumnDef } from "@/src/components/table/types";
import useColumnVisibility from "@/src/features/column-visibility/hooks/useColumnVisibility";
import { useQueryFilterState } from "@/src/features/filters/hooks/useFilterState";
import { type FilterState } from "@/src/features/filters/types";
import { useDetailPageLists } from "@/src/features/navigate-detail-pages/context";
import { sessionsViewCols } from "@/src/server/api/definitions/sessionsView";
import { api } from "@/src/utils/api";
import { formatInterval, utcDateOffsetByDays } from "@/src/utils/dates";
import { type RouterOutput } from "@/src/utils/types";
import { useEffect } from "react";
import { NumberParam, useQueryParams, withDefault } from "use-query-params";
export type SessionTableRow = {
id: string;
createdAt: string;
userIds: string[];
countTraces: number;
bookmarked: boolean;
sessionDuration: number | null;
};
export type SessionTableProps = {
projectId: string;
userId?: string;
omittedFilter?: string[];
};
export default function SessionsTable({
projectId,
userId,
omittedFilter = [],
}: SessionTableProps) {
const { setDetailPageList } = useDetailPageLists();
const [userFilterState, setUserFilterState] = useQueryFilterState([
{
column: "createdAt",
type: "datetime",
operator: ">",
value: utcDateOffsetByDays(-14),
},
]);
const userIdFilter: FilterState = userId
? [
{
column: "userId",
type: "string",
operator: "=",
value: userId,
},
]
: [];
const filterState = userFilterState.concat(userIdFilter);
const [paginationState, setPaginationState] = useQueryParams({
pageIndex: withDefault(NumberParam, 0),
pageSize: withDefault(NumberParam, 50),
});
const sessions = api.sessions.all.useQuery({
page: paginationState.pageIndex,
limit: paginationState.pageSize,
projectId,
filter: filterState,
});
const totalCount = sessions.data?.slice(1)[0]?.totalCount ?? 0;
useEffect(() => {
if (sessions.isSuccess) {
setDetailPageList(
"sessions",
sessions.data.map((t) => t.id),
);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [sessions.isSuccess, sessions.data]);
const convertToTableRow = (
session: RouterOutput["sessions"]["all"][0],
): SessionTableRow => {
return {
id: session.id,
createdAt: session.createdAt.toLocaleString(),
userIds: session.userIds,
countTraces: session.countTraces,
bookmarked: session.bookmarked,
sessionDuration: session.sessionDuration,
};
};
const columns: LangfuseColumnDef<SessionTableRow>[] = [
{
accessorKey: "bookmarked",
header: undefined,
cell: ({ row }) => {
const bookmarked = row.getValue("bookmarked");
const sessionId = row.getValue("id");
return typeof sessionId === "string" &&
typeof bookmarked === "boolean" ? (
<StarSessionToggle
sessionId={sessionId}
projectId={projectId}
value={bookmarked}
size="xs"
/>
) : undefined;
},
},
{
accessorKey: "id",
header: "ID",
cell: ({ row }) => {
const value = row.getValue("id");
return value && typeof value === "string" ? (
<TableLink
path={`/project/${projectId}/sessions/${value}`}
value={value}
/>
) : undefined;
},
},
{
accessorKey: "createdAt",
header: "Created At",
enableHiding: true,
},
{
accessorKey: "sessionDuration",
header: "Duration",
enableHiding: true,
cell: ({ row }) => {
const value = row.getValue("sessionDuration");
return value && typeof value === "number"
? formatInterval(value)
: undefined;
},
},
{
accessorKey: "userIds",
enableColumnFilter: !omittedFilter.find((f) => f === "userIds"),
header: "User ID",
enableHiding: true,
cell: ({ row }) => {
const value = row.getValue("userIds");
return value && Array.isArray(value) ? (
<div className="flex gap-1">
{(value as string[]).map((user) => (
<TableLink
key={user}
path={`/project/${projectId}/users/${user}`}
value={user}
truncateAt={40}
/>
))}
</div>
) : undefined;
},
},
{
accessorKey: "countTraces",
header: "Traces",
enableHiding: true,
},
];
const [columnVisibility, setColumnVisibility] =
useColumnVisibility<SessionTableRow>("sessionsColumnVisibility", columns);
return (
<div>
<DataTableToolbar
filterColumnDefinition={sessionsViewCols}
filterState={userFilterState}
setFilterState={setUserFilterState}
columns={columns}
columnVisibility={columnVisibility}
setColumnVisibility={setColumnVisibility}
/>
<DataTable
columns={columns}
data={
sessions.isLoading
? { isLoading: true, isError: false }
: sessions.isError
? {
isLoading: false,
isError: true,
error: sessions.error.message,
}
: {
isLoading: false,
isError: false,
data: sessions.data.map((t) => convertToTableRow(t)),
}
}
pagination={{
pageCount: Math.ceil(totalCount / paginationState.pageSize),
onChange: setPaginationState,
state: paginationState,
}}
columnVisibility={columnVisibility}
onColumnVisibilityChange={setColumnVisibility}
help={{
description:
"A session is a collection of related traces, such as a conversation or thread. To begin, add a sessionId to the trace.",
href: "https://langfuse.com/docs/sessions",
}}
/>
</div>
);
}
+163 -13
View File
@@ -1,19 +1,26 @@
import { DeleteTrace } from "@/src/components/delete-trace";
import { GroupedScoreBadges } from "@/src/components/grouped-score-badge";
import { StarTraceToggle } from "@/src/components/star-toggle";
import { DataTable } from "@/src/components/table/data-table";
import { TraceTableMultiSelectAction } from "@/src/components/table/data-table-multi-select-actions/trace-table-multi-select-action";
import { DataTableToolbar } from "@/src/components/table/data-table-toolbar";
import TableLink from "@/src/components/table/table-link";
import { type LangfuseColumnDef } from "@/src/components/table/types";
import { TokenUsageBadge } from "@/src/components/token-usage-badge";
import { Checkbox } from "@/src/components/ui/checkbox";
import { JSONView } from "@/src/components/ui/code";
import useColumnVisibility from "@/src/features/column-visibility/hooks/useColumnVisibility";
import { useQueryFilterState } from "@/src/features/filters/hooks/useFilterState";
import { type FilterState } from "@/src/features/filters/types";
import { useDetailPageLists } from "@/src/features/navigate-detail-pages/context";
import { useOrderByState } from "@/src/features/orderBy/hooks/useOrderByState";
import { tracesTableColsWithOptions } from "@/src/server/api/definitions/tracesTable";
import { api } from "@/src/utils/api";
import { utcDateOffsetByDays } from "@/src/utils/dates";
import { formatInterval, utcDateOffsetByDays } from "@/src/utils/dates";
import { type RouterInput, type RouterOutput } from "@/src/utils/types";
import { type Score } from "@prisma/client";
import { type ColumnDef } from "@tanstack/react-table";
import { useEffect } from "react";
import { type RowSelectionState } from "@tanstack/react-table";
import { useEffect, useState } from "react";
import {
NumberParam,
StringParam,
@@ -22,7 +29,8 @@ import {
withDefault,
} from "use-query-params";
export type TraceTableRow = {
export type TracesTableRow = {
bookmarked: boolean;
id: string;
timestamp: string;
name: string;
@@ -31,6 +39,9 @@ export type TraceTableRow = {
latency?: number;
release?: string;
version?: string;
input?: unknown;
output?: unknown;
sessionId?: string;
scores: Score[];
usage: {
promptTokens: number;
@@ -39,7 +50,7 @@ export type TraceTableRow = {
};
};
export type TraceTableProps = {
export type TracesTableProps = {
projectId: string;
userId?: string;
omittedFilter?: string[];
@@ -51,13 +62,13 @@ export default function TracesTable({
projectId,
userId,
omittedFilter = [],
}: TraceTableProps) {
}: TracesTableProps) {
const [selectedRows, setSelectedRows] = useState<RowSelectionState>({});
const { setDetailPageList } = useDetailPageLists();
const [searchQuery, setSearchQuery] = useQueryParam(
"search",
withDefault(StringParam, null),
);
const [userFilterState, setUserFilterState] = useQueryFilterState([
{
column: "timestamp",
@@ -66,6 +77,10 @@ export default function TracesTable({
value: utcDateOffsetByDays(-14),
},
]);
const [orderByState, setOrderByState] = useOrderByState({
column: "timestamp",
order: "DESC",
});
const userIdFilter: FilterState = userId
? [
@@ -79,7 +94,6 @@ export default function TracesTable({
: [];
const filterState = userFilterState.concat(userIdFilter);
const [paginationState, setPaginationState] = useQueryParams({
pageIndex: withDefault(NumberParam, 0),
pageSize: withDefault(NumberParam, 50),
@@ -91,10 +105,11 @@ export default function TracesTable({
projectId,
filter: filterState,
searchQuery,
orderBy: orderByState,
});
const totalCount = traces.data?.slice(1)[0]?.totalCount ?? 0;
useEffect(() => {
if (traces.isSuccess && traces.data) {
if (traces.isSuccess) {
setDetailPageList(
"traces",
traces.data.map((t) => t.id),
@@ -109,8 +124,9 @@ export default function TracesTable({
const convertToTableRow = (
trace: RouterOutput["traces"]["all"][0],
): TraceTableRow => {
): TracesTableRow => {
return {
bookmarked: trace.bookmarked,
id: trace.id,
timestamp: trace.timestamp.toLocaleString(),
name: trace.name ?? "",
@@ -119,6 +135,9 @@ export default function TracesTable({
version: trace.version ?? undefined,
userId: trace.userId ?? "",
scores: trace.scores,
sessionId: trace.sessionId ?? undefined,
input: trace.input,
output: trace.output,
latency: trace.latency === null ? undefined : trace.latency,
usage: {
promptTokens: trace.promptTokens,
@@ -128,10 +147,58 @@ export default function TracesTable({
};
};
const columns: ColumnDef<TraceTableRow>[] = [
const columns: LangfuseColumnDef<TracesTableRow>[] = [
{
id: "select",
header: ({ table }) => (
<Checkbox
checked={
table.getIsAllPageRowsSelected()
? true
: table.getIsSomePageRowsSelected()
? "indeterminate"
: false
}
onCheckedChange={(value) => {
table.toggleAllPageRowsSelected(!!value);
if (!value) {
setSelectedRows({});
}
}}
aria-label="Select all"
/>
),
cell: ({ row }) => (
<Checkbox
checked={row.getIsSelected()}
onCheckedChange={(value) => row.toggleSelected(!!value)}
aria-label="Select row"
/>
),
},
{
accessorKey: "bookmarked",
header: undefined,
id: "bookmarked",
cell: ({ row }) => {
const bookmarked = row.getValue("bookmarked");
const traceId = row.getValue("id");
return typeof traceId === "string" &&
typeof bookmarked === "boolean" ? (
<StarTraceToggle
traceId={traceId}
projectId={projectId}
value={bookmarked}
size="xs"
/>
) : undefined;
},
},
{
accessorKey: "id",
header: "ID",
id: "id",
cell: ({ row }) => {
const value = row.getValue("id");
return value && typeof value === "string" ? (
@@ -141,19 +208,26 @@ export default function TracesTable({
/>
) : undefined;
},
enableSorting: true,
},
{
accessorKey: "timestamp",
header: "Timestamp",
id: "timestamp",
enableHiding: true,
enableSorting: true,
},
{
accessorKey: "name",
header: "Name",
id: "name",
enableHiding: true,
},
{
accessorKey: "userId",
enableColumnFilter: !omittedFilter.find((f) => f === "userId"),
header: "User ID",
id: "userId",
cell: ({ row }) => {
const value = row.getValue("userId");
return value && typeof value === "string" ? (
@@ -164,17 +238,39 @@ export default function TracesTable({
/>
) : undefined;
},
enableHiding: true,
enableSorting: true,
},
{
accessorKey: "sessionId",
enableColumnFilter: !omittedFilter.find((f) => f === "sessionId"),
header: "Session ID",
cell: ({ row }) => {
const value = row.getValue("sessionId");
return value && typeof value === "string" ? (
<TableLink
path={`/project/${projectId}/sessions/${value}`}
value={value}
truncateAt={40}
/>
) : undefined;
},
enableHiding: true,
},
{
accessorKey: "latency",
id: "latency",
header: "Latency",
// add seconds to the end of the latency
cell: ({ row }) => {
const value: number | undefined = row.getValue("latency");
return value !== undefined ? `${value.toFixed(2)} sec` : undefined;
return value !== undefined ? formatInterval(value) : undefined;
},
enableHiding: true,
enableSorting: true,
},
{
// TODO: Enable Ordering By Usage (not covered by API yet)
accessorKey: "usage",
header: "Usage",
cell: ({ row }) => {
@@ -192,15 +288,38 @@ export default function TracesTable({
/>
);
},
enableHiding: true,
},
{
accessorKey: "scores",
id: "scores",
header: "Scores",
enableColumnFilter: !omittedFilter.find((f) => f === "scores"),
cell: ({ row }) => {
const values: Score[] = row.getValue("scores");
return <GroupedScoreBadges scores={values} variant="headings" />;
},
enableHiding: true,
},
{
accessorKey: "input",
header: "Input",
cell: ({ row }) => {
const value: unknown = row.getValue("input");
return <JSONView json={value} className="w-[500px]" />;
},
enableHiding: true,
defaultHidden: true,
},
{
accessorKey: "output",
header: "Output",
cell: ({ row }) => {
const value: unknown = row.getValue("output");
return <JSONView json={value} className="w-[500px] bg-green-50" />;
},
enableHiding: true,
defaultHidden: true,
},
{
accessorKey: "metadata",
@@ -209,14 +328,21 @@ export default function TracesTable({
const values: string = row.getValue("metadata");
return <div className="flex flex-wrap gap-x-3 gap-y-1">{values}</div>;
},
enableHiding: true,
},
{
accessorKey: "version",
id: "version",
header: "Version",
enableHiding: true,
enableSorting: true,
},
{
accessorKey: "release",
id: "release",
header: "Release",
enableHiding: true,
enableSorting: true,
},
{
accessorKey: "action",
@@ -234,9 +360,13 @@ export default function TracesTable({
},
];
const [columnVisibility, setColumnVisibility] =
useColumnVisibility<TracesTableRow>("tracesColumnVisibility", columns);
return (
<div>
<DataTableToolbar
columns={columns}
filterColumnDefinition={tracesTableColsWithOptions(
traceFilterOptions.data,
)}
@@ -247,6 +377,20 @@ export default function TracesTable({
}}
filterState={userFilterState}
setFilterState={setUserFilterState}
actionButtons={
<TraceTableMultiSelectAction
// Exclude traces that are not in the current page
selectedTraceIds={Object.keys(selectedRows).filter(
(traceId) => traces.data?.map((t) => t.id).includes(traceId),
)}
projectId={projectId}
onDeleteSuccess={() => {
setSelectedRows({});
}}
/>
}
columnVisibility={columnVisibility}
setColumnVisibility={setColumnVisibility}
/>
<DataTable
columns={columns}
@@ -262,7 +406,7 @@ export default function TracesTable({
: {
isLoading: false,
isError: false,
data: traces.data?.map((t) => convertToTableRow(t)),
data: traces.data.map((t) => convertToTableRow(t)),
}
}
pagination={{
@@ -270,6 +414,12 @@ export default function TracesTable({
onChange: setPaginationState,
state: paginationState,
}}
setOrderBy={setOrderByState}
orderBy={orderByState}
rowSelection={selectedRows}
setRowSelection={setSelectedRows}
columnVisibility={columnVisibility}
onColumnVisibilityChange={setColumnVisibility}
/>
</div>
);
+155
View File
@@ -0,0 +1,155 @@
import { JSONView } from "@/src/components/ui/code";
import { z } from "zod";
import { deepParseJson } from "@/src/utils/json";
import { cn } from "@/src/utils/tailwind";
import { useState } from "react";
import { Button } from "@/src/components/ui/button";
import { Tabs, TabsList, TabsTrigger } from "@/src/components/ui/tabs";
import { Fragment } from "react";
export const IOPreview: React.FC<{
input?: unknown;
output?: unknown;
isLoading?: boolean;
hideIfNull?: boolean;
}> = ({ isLoading = false, hideIfNull = false, ...props }) => {
const [currentView, setCurrentView] = useState<"pretty" | "json">("pretty");
const input = deepParseJson(props.input);
const output = deepParseJson(props.output);
// parse old completions: { completion: string } -> string
const outLegacyCompletionSchema = z
.object({
completion: z.string(),
})
.refine((value) => Object.keys(value).length === 1);
const outLegacyCompletionSchemaParsed =
outLegacyCompletionSchema.safeParse(output);
const outputClean = outLegacyCompletionSchemaParsed.success
? outLegacyCompletionSchemaParsed.data
: props.output ?? null;
// OpenAI messages
let inOpenAiMessageArray = OpenAiMessageArraySchema.safeParse(input);
if (!inOpenAiMessageArray.success) {
// check if input is an array of length 1 including an array of OpenAiMessageSchema
// this is the case for some integrations
const inputArray = z.array(OpenAiMessageArraySchema).safeParse(input);
if (inputArray.success && inputArray.data.length === 1) {
inOpenAiMessageArray = OpenAiMessageArraySchema.safeParse(
inputArray.data[0],
);
}
}
const outOpenAiMessage = OpenAiMessageSchema.safeParse(output);
// Pretty view available
const isPrettyViewAvailable = inOpenAiMessageArray.success;
// default I/O
return (
<>
{isPrettyViewAvailable ? (
<Tabs
value={currentView}
onValueChange={(v) => setCurrentView(v as "pretty" | "json")}
>
<TabsList>
<TabsTrigger value="pretty">Pretty </TabsTrigger>
<TabsTrigger value="json">JSON</TabsTrigger>
</TabsList>
</Tabs>
) : null}
{isPrettyViewAvailable && currentView === "pretty" ? (
<OpenAiMessageView
messages={inOpenAiMessageArray.data.concat(
outOpenAiMessage.success
? {
...outOpenAiMessage.data,
role: outOpenAiMessage.data.role ?? "assistant",
}
: {
role: "assistant",
content: outputClean ? JSON.stringify(outputClean) : null,
},
)}
/>
) : null}
{currentView === "json" || !isPrettyViewAvailable ? (
<>
{!(hideIfNull && !input) ? (
<JSONView
title="Input"
json={input ?? null}
isLoading={isLoading}
className="flex-1"
/>
) : null}
{!(hideIfNull && !output) ? (
<JSONView
title="Output"
json={outputClean}
isLoading={isLoading}
className="flex-1 bg-green-50"
/>
) : null}
</>
) : null}
</>
);
};
const OpenAiMessageSchema = z
.object({
role: z.enum(["system", "user", "assistant"]).optional(),
name: z.string().optional(),
content: z.union([z.record(z.any()), z.string()]).nullable(),
})
.strict() // no additional properties
.refine((value) => value.content !== null || value.role !== undefined);
const OpenAiMessageArraySchema = z.array(OpenAiMessageSchema).min(1);
const OpenAiMessageView: React.FC<{
messages: z.infer<typeof OpenAiMessageArraySchema>;
}> = ({ messages }) => {
const COLLAPSE_THRESHOLD = 3;
const [isCollapsed, setCollapsed] = useState(
messages.length > COLLAPSE_THRESHOLD ? true : null,
);
return (
<div className="flex flex-col gap-2 rounded-md border p-3">
{messages
.filter(
(_, i) =>
// show all if not collapsed or null; show first and last n if collapsed
!isCollapsed || i == 0 || i > messages.length - COLLAPSE_THRESHOLD,
)
.map((message, index) => (
<Fragment key={index}>
<JSONView
title={message.name ?? message.role}
json={message.content}
className={cn(
message.role === "system" && "bg-gray-100",
message.role === "assistant" && "bg-green-50",
)}
/>
{isCollapsed !== null && index === 0 ? (
<Button
variant="ghost"
size="xs"
onClick={() => setCollapsed((v) => !v)}
>
{isCollapsed
? `Show ${messages.length - COLLAPSE_THRESHOLD} more ...`
: "Hide history"}
</Button>
) : null}
</Fragment>
))}
</div>
);
};
+35 -156
View File
@@ -16,16 +16,13 @@ import {
TableHeader,
TableRow,
} from "@/src/components/ui/table";
import { ManualScoreButton } from "@/src/features/manual-scoring/components";
import { ManualScoreButton } from "@/src/features/manual-scoring/components/ManualScoreButton";
import { NewDatasetItemFromObservationButton } from "@/src/features/datasets/components/NewDatasetItemFromObservationButton";
import { type ObservationReturnType } from "@/src/server/api/routers/traces";
import { api } from "@/src/utils/api";
import { z } from "zod";
import { deepParseJson } from "@/src/utils/json";
import { cn } from "@/src/utils/tailwind";
import { useState } from "react";
import { Button } from "@/src/components/ui/button";
import { Tabs, TabsList, TabsTrigger } from "@/src/components/ui/tabs";
import { IOPreview } from "@/src/components/trace/IOPreview";
import { formatInterval } from "@/src/utils/dates";
import Link from "next/link";
export const ObservationPreview = (props: {
observations: Array<ObservationReturnType>;
@@ -58,20 +55,28 @@ export const ObservationPreview = (props: {
{preloadedObservation.startTime.toLocaleString()}
</CardDescription>
<div className="flex flex-wrap gap-2">
{preloadedObservation.promptId ? (
<PromptBadge
promptId={preloadedObservation.promptId}
projectId={preloadedObservation.projectId}
/>
) : undefined}
{preloadedObservation.endTime ? (
<Badge variant="outline">
{`${(
{formatInterval(
(preloadedObservation.endTime.getTime() -
preloadedObservation.startTime.getTime()) /
1000
).toFixed(2)} sec`}
1000,
)}
</Badge>
) : null}
<Badge variant="outline">
{preloadedObservation.promptTokens} prompt {" "}
{preloadedObservation.completionTokens} completion ({" "}
{preloadedObservation.totalTokens})
</Badge>
{preloadedObservation.type === "GENERATION" && (
<Badge variant="outline">
{preloadedObservation.promptTokens} prompt {" "}
{preloadedObservation.completionTokens} completion ({" "}
{preloadedObservation.totalTokens})
</Badge>
)}
{preloadedObservation.version ? (
<Badge variant="outline">
Version: {preloadedObservation.version}
@@ -85,6 +90,7 @@ export const ObservationPreview = (props: {
{preloadedObservation.price.toString()} USD
</Badge>
) : undefined}
{preloadedObservation.modelParameters &&
typeof preloadedObservation.modelParameters === "object"
? Object.entries(preloadedObservation.modelParameters)
@@ -116,14 +122,12 @@ export const ObservationPreview = (props: {
</div>
</CardHeader>
<CardContent className="flex flex-col gap-4">
<ObservationIO
<IOPreview
key={preloadedObservation.id + "-input"}
observationType={preloadedObservation.type}
input={observationWithInputAndOutput.data?.input ?? undefined}
output={observationWithInputAndOutput.data?.output ?? undefined}
isLoading={observationWithInputAndOutput.isLoading}
/>
{preloadedObservation.statusMessage ? (
<JSONView
key={preloadedObservation.id + "-status"}
@@ -178,146 +182,21 @@ export const ObservationPreview = (props: {
);
};
const ObservationIO: React.FC<{
observationType: string;
input?: unknown;
output?: unknown;
isLoading: boolean;
}> = ({ isLoading, ...props }) => {
const [currentView, setCurrentView] = useState<"pretty" | "json">("pretty");
const PromptBadge = (props: { promptId: string; projectId: string }) => {
const prompt = api.prompts.byId.useQuery({
id: props.promptId,
projectId: props.projectId,
});
const input = deepParseJson(props.input);
const output = deepParseJson(props.output);
// parse old completions: { completion: string } -> string
const outLegacyCompletionSchema = z
.object({
completion: z.string(),
})
.refine((value) => Object.keys(value).length === 1);
const outLegacyCompletionSchemaParsed =
outLegacyCompletionSchema.safeParse(output);
const outputClean = outLegacyCompletionSchemaParsed.success
? outLegacyCompletionSchemaParsed.data
: props.output ?? null;
// OpenAI messages
let inOpenAiMessageArray = OpenAiMessageArraySchema.safeParse(input);
if (!inOpenAiMessageArray.success) {
// check if input is an array of length 1 including an array of OpenAiMessageSchema
// this is the case for some integrations
const inputArray = z.array(OpenAiMessageArraySchema).safeParse(input);
if (inputArray.success && inputArray.data.length === 1) {
inOpenAiMessageArray = OpenAiMessageArraySchema.safeParse(
inputArray.data[0],
);
}
}
const outOpenAiMessage = OpenAiMessageSchema.safeParse(output);
// Pretty view available
const isPrettyViewAvailable = inOpenAiMessageArray.success;
// default I/O
return (
<>
{isPrettyViewAvailable ? (
<Tabs
value={currentView}
onValueChange={(v) => setCurrentView(v as "pretty" | "json")}
>
<TabsList>
<TabsTrigger value="pretty">Pretty </TabsTrigger>
<TabsTrigger value="json">JSON</TabsTrigger>
</TabsList>
</Tabs>
) : null}
{isPrettyViewAvailable && currentView === "pretty" ? (
inOpenAiMessageArray.success ? (
<OpenAiMessageView
messages={inOpenAiMessageArray.data.concat(
outOpenAiMessage.success
? {
...outOpenAiMessage.data,
role: outOpenAiMessage.data.role ?? "assistant",
}
: {
role: "assistant",
content: JSON.stringify(outputClean) ?? null,
},
)}
/>
) : null
) : null}
{currentView === "json" || !isPrettyViewAvailable ? (
<>
<JSONView
title="Input"
json={input ?? null}
isLoading={isLoading}
className="flex-1"
/>
<JSONView
title="Output"
json={outputClean}
isLoading={isLoading}
className="flex-1"
/>
</>
) : null}
</>
);
};
const OpenAiMessageSchema = z
.object({
role: z.enum(["system", "user", "assistant"]).optional(),
content: z.string().nullable(),
})
.refine((value) => value.content !== null || value.role !== undefined);
const OpenAiMessageArraySchema = z.array(OpenAiMessageSchema).min(1);
const OpenAiMessageView: React.FC<{
messages: z.infer<typeof OpenAiMessageArraySchema>;
}> = ({ messages }) => {
const COLLAPSE_THRESHOLD = 3;
const [isCollapsed, setCollapsed] = useState(
messages.length > COLLAPSE_THRESHOLD ? true : null,
);
if (prompt.isLoading) return null;
return (
<div className="flex flex-col gap-2 rounded-md border p-3">
{messages
.filter(
(_, i) =>
// show all if not collapsed or null; show first and last n if collapsed
!isCollapsed || i == 0 || i > messages.length - COLLAPSE_THRESHOLD,
)
.map((message, index) => (
<>
<JSONView
title={message.role}
json={message.content}
key={index}
className={cn(
message.role === "system" && "bg-gray-100",
message.role === "assistant" && "bg-green-50",
)}
/>
{isCollapsed !== null && index === 0 ? (
<Button
variant="ghost"
size="xs"
onClick={() => setCollapsed((v) => !v)}
>
{isCollapsed
? `Show ${messages.length - COLLAPSE_THRESHOLD} more ...`
: "Hide history"}
</Button>
) : null}
</>
))}
</div>
<Link href={`/project/${props.projectId}/prompts/${props.promptId}`}>
<Badge>
Prompt: {prompt.data?.name}
{" - "}
{prompt.data?.version}
</Badge>
</Link>
);
};
+9 -15
View File
@@ -4,6 +4,8 @@ import { type Trace, type Score } from "@prisma/client";
import { GroupedScoreBadges } from "@/src/components/grouped-score-badge";
import { Fragment } from "react";
import { type ObservationReturnType } from "@/src/server/api/routers/traces";
import { LevelColors } from "@/src/components/level-colors";
import { formatInterval } from "@/src/utils/dates";
export const ObservationTree = (props: {
observations: ObservationReturnType[];
@@ -55,7 +57,7 @@ const ObservationTreeTraceNode = (props: {
{props.trace.latency ? (
<div className="flex gap-2">
<span className="text-xs text-gray-500">
{props.trace.latency.toFixed(2)} sec
{formatInterval(props.trace.latency)}
</span>
</div>
) : null}
@@ -106,12 +108,11 @@ const ObservationTreeNode = (props: {
<div className="flex gap-2">
{observation.endTime ? (
<span className="text-xs text-gray-500">
{(
{formatInterval(
(observation.endTime.getTime() -
observation.startTime.getTime()) /
1000
).toFixed(2)}{" "}
sec
1000,
)}
</span>
) : null}
{observation.promptTokens ||
@@ -127,9 +128,9 @@ const ObservationTreeNode = (props: {
<div className="flex">
<span
className={cn(
"rounded-sm text-xs",
LevelColor[observation.level].bg,
LevelColor[observation.level].text,
"rounded-sm p-0.5 text-xs",
LevelColors[observation.level].bg,
LevelColors[observation.level].text,
)}
>
{observation.level}
@@ -159,13 +160,6 @@ const ObservationTreeNode = (props: {
</>
);
const LevelColor = {
DEFAULT: { text: "", bg: "" },
DEBUG: { text: "text-gray-500", bg: "bg-gray-50" },
WARNING: { text: "text-yellow-800", bg: "bg-yellow-50" },
ERROR: { text: "text-red-800", bg: "bg-red-50" },
};
export function nestObservations(
list: ObservationReturnType[],
): NestedObservation[] {
+17 -7
View File
@@ -17,16 +17,18 @@ import {
TableRow,
} from "@/src/components/ui/table";
import { TraceAggUsageBadge } from "@/src/components/token-usage-badge";
import { ManualScoreButton } from "@/src/features/manual-scoring/components";
import { ManualScoreButton } from "@/src/features/manual-scoring/components/ManualScoreButton";
import { Badge } from "@/src/components/ui/badge";
import { type ObservationReturnType } from "@/src/server/api/routers/traces";
import { IOPreview } from "@/src/components/trace/IOPreview";
import { formatInterval } from "@/src/utils/dates";
export const TracePreview = ({
trace,
observations,
scores,
}: {
trace: Trace;
trace: Trace & { latency?: number };
observations: ObservationReturnType[];
scores: Score[];
}) => {
@@ -42,13 +44,16 @@ export const TracePreview = ({
</CardTitle>
<CardDescription>{trace.timestamp.toLocaleString()}</CardDescription>
<div className="flex flex-wrap gap-2">
{!!trace.latency && (
<Badge variant="outline">{formatInterval(trace.latency)}</Badge>
)}
<TraceAggUsageBadge observations={observations} />
{trace.release ? (
{!!trace.release && (
<Badge variant="outline">Release: {trace.release}</Badge>
) : undefined}
{trace.version ? (
)}
{!!trace.version && (
<Badge variant="outline">Version: {trace.version}</Badge>
) : undefined}
)}
</div>
</div>
<ManualScoreButton
@@ -57,7 +62,12 @@ export const TracePreview = ({
scores={scores}
/>
</CardHeader>
<CardContent>
<CardContent className="flex flex-col gap-4">
<IOPreview
key={trace.id + "-io"}
input={trace.input ?? undefined}
output={trace.output ?? undefined}
/>
<JSONView
key={trace.id + "-metadata"}
title="Metadata"
+50 -22
View File
@@ -8,12 +8,15 @@ import { Badge } from "@/src/components/ui/badge";
import { TraceAggUsageBadge } from "@/src/components/token-usage-badge";
import Decimal from "decimal.js";
import { StringParam, useQueryParam } from "use-query-params";
import { PublishTraceSwitch } from "@/src/features/public-traces/components/PublishTraceSwitch";
import { PublishTraceSwitch } from "@/src/components/publish-object-switch";
import { DetailPageNav } from "@/src/features/navigate-detail-pages/DetailPageNav";
import { useRouter } from "next/router";
import { type ObservationReturnType } from "@/src/server/api/routers/traces";
import { api } from "@/src/utils/api";
import { DeleteTrace } from "@/src/components/delete-trace";
import { StarTraceToggle } from "@/src/components/star-toggle";
import Link from "next/link";
import { NoAccessError } from "@/src/components/no-access";
export function Trace(props: {
observations: Array<ObservationReturnType>;
@@ -28,16 +31,7 @@ export function Trace(props: {
return (
<div className="grid h-full gap-4 md:grid-cols-3">
<div className="col-span-1 lg:hidden">
<ObservationTree
observations={props.observations}
trace={props.trace}
scores={props.scores}
currentObservationId={currentObservationId ?? undefined}
setCurrentObservationId={setCurrentObservationId}
/>
</div>
<div className="col-span-2 h-full overflow-y-auto">
<div className="md:col-span-2 md:h-full md:overflow-y-auto">
{currentObservationId === undefined ||
currentObservationId === "" ||
currentObservationId === null ? (
@@ -56,7 +50,7 @@ export function Trace(props: {
/>
)}
</div>
<div className="col-span-1 hidden h-full overflow-y-auto lg:block">
<div className="md:h-full md:overflow-y-auto">
<ObservationTree
observations={props.observations}
trace={props.trace}
@@ -71,26 +65,45 @@ export function Trace(props: {
export function TracePage({ traceId }: { traceId: string }) {
const router = useRouter();
const trace = api.traces.byId.useQuery({ traceId });
const trace = api.traces.byId.useQuery(
{ traceId },
{
retry(failureCount, error) {
if (error.data?.code === "UNAUTHORIZED") return false;
return failureCount < 3;
},
},
);
const totalCost = trace.data?.observations.reduce(
(acc, o) => {
if (!o.price) return acc;
return acc
? acc.plus(o.price ? o.price : new Decimal(0))
: new Decimal(0).plus(o.price ? o.price : new Decimal(0));
return acc ? acc.plus(o.price) : new Decimal(0).plus(o.price);
},
undefined as Decimal | undefined,
);
if (trace.error?.data?.code === "UNAUTHORIZED") return <NoAccessError />;
if (!trace.data) return <div>loading...</div>;
return (
<div className="flex flex-col overflow-hidden xl:container lg:h-[calc(100vh-100px)] xl:h-[calc(100vh-50px)]">
<div className="flex flex-col overflow-hidden xl:container md:h-[calc(100vh-100px)] xl:h-[calc(100vh-40px)]">
<Header
title="Trace Detail"
breadcrumb={[
{
name: "Traces",
href: `/project/${router.query.projectId as string}/traces`,
},
{ name: traceId },
]}
actionButtons={
<>
<StarTraceToggle
traceId={trace.data.id}
projectId={trace.data.projectId}
value={trace.data.bookmarked}
/>
<PublishTraceSwitch
traceId={trace.data.id}
projectId={trace.data.projectId}
@@ -110,11 +123,26 @@ export function TracePage({ traceId }: { traceId: string }) {
</>
}
/>
<div className="flex gap-2">
{trace.data.userId ? (
<Badge variant="outline">User ID: {trace.data.userId}</Badge>
<div className="flex flex-wrap gap-2">
{trace.data.sessionId ? (
<Link
href={`/project/${router.query.projectId as string}/sessions/${
trace.data.sessionId
}`}
>
<Badge>Session: {trace.data.sessionId}</Badge>
</Link>
) : null}
<TraceAggUsageBadge observations={trace.data.observations ?? []} />
{trace.data.userId ? (
<Link
href={`/project/${router.query.projectId as string}/users/${
trace.data.userId
}`}
>
<Badge>User ID: {trace.data.userId}</Badge>
</Link>
) : null}
<TraceAggUsageBadge observations={trace.data.observations} />
{totalCost ? (
<Badge variant="outline">
Total cost: {totalCost.toString()} USD
@@ -127,7 +155,7 @@ export function TracePage({ traceId }: { traceId: string }) {
trace={trace.data}
scores={trace.data.scores}
projectId={trace.data.projectId}
observations={trace.data.observations ?? []}
observations={trace.data.observations}
/>
</div>
</div>
+1 -1
View File
@@ -4,7 +4,7 @@ import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/src/utils/tailwind";
const alertVariants = cva(
"relative w-full rounded-lg border p-4 [&:has(svg)]:pl-11 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground",
"relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground",
{
variants: {
variant: {
+1
View File
@@ -2,6 +2,7 @@
import * as React from "react";
import * as AvatarPrimitive from "@radix-ui/react-avatar";
import { cn } from "@/src/utils/tailwind";
const Avatar = React.forwardRef<

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