Compare commits

...
96 Commits
Author SHA1 Message Date
Marc Klingen ebabf0e100 chore: release v1.34.1
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 55s
release.yml / release (push) Failing after 3s
CI/CD / e2e-tests (push) Failing after 45s
CI/CD / test-docker-build (push) Successful in 3m12s
CI/CD / lint (push) Successful in 4m0s
CI/CD / all-ci-passed (push) Waiting to run
2024-01-24 01:12:22 +01:00
Marc KlingenandGitHub e05d4693d2 fix(ui): encodeURIComponent for session and user ids (#940)
Before, special characters in those ids would break the UI as the ids are used as URI components
2024-01-24 01:11:37 +01:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
77d49adc00 chore(deps): bump react-icons from 4.12.0 to 5.0.1 (#935)
Bumps [react-icons](https://github.com/react-icons/react-icons) from 4.12.0 to 5.0.1.
- [Release notes](https://github.com/react-icons/react-icons/releases)
- [Commits](https://github.com/react-icons/react-icons/compare/v4.12.0...v5.0.1)

---
updated-dependencies:
- dependency-name: react-icons
  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>
2024-01-23 23:55:10 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
7dc45f0074 chore(deps): bump lucide-react from 0.306.0 to 0.314.0 (#937)
Bumps [lucide-react](https://github.com/lucide-icons/lucide/tree/HEAD/packages/lucide-react) from 0.306.0 to 0.314.0.
- [Release notes](https://github.com/lucide-icons/lucide/releases)
- [Commits](https://github.com/lucide-icons/lucide/commits/0.314.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>
2024-01-24 00:49:42 +01:00
44d1da3c9c fix: fix playwright test timeouts (#868)
Co-authored-by: gitstart_bot <gitstart_bot@users.noreply.github.com>
Co-authored-by: gitstart-langfuse <gitstart-langfuse@users.noreply.github.com>
Co-authored-by: Max Deichmann <m.deichmann@tum.de>
2024-01-23 23:47:53 +01:00
jhlopenandGitHub 564ba4bd68 chore: mitigate type truncation in VS Code tooltip (#932) 2024-01-23 23:38:30 +01:00
Marc Klingen 63df788908 chore: release v1.34.0
CI/CD / push-docker-image (push) Blocked by required conditions
CI/CD / tests (18) (push) Failing after 47s
CI/CD / tests (20) (push) Failing after 44s
release.yml / release (push) Failing after 2s
CI/CD / e2e-tests (push) Failing after 41s
CI/CD / test-docker-build (push) Successful in 3m14s
CI/CD / lint (push) Successful in 5m59s
CI/CD / all-ci-passed (push) Waiting to run
2024-01-23 22:59:17 +01:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
fb463cbf98 chore(deps): bump the sentry group with 2 updates (#931)
Bumps the sentry group with 2 updates: [@sentry/nextjs](https://github.com/getsentry/sentry-javascript) and [@sentry/profiling-node](https://github.com/getsentry/profiling-node).


Updates `@sentry/nextjs` from 7.92.0 to 7.95.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.92.0...7.95.0)

Updates `@sentry/profiling-node` from 1.3.2 to 1.3.5
- [Release notes](https://github.com/getsentry/profiling-node/releases)
- [Changelog](https://github.com/getsentry/profiling-node/blob/main/CHANGES)
- [Commits](https://github.com/getsentry/profiling-node/compare/v1.3.2...v1.3.5)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-23 21:56:41 +00:00
Marc KlingenandGitHub cfd99efd48 feat(ui): add ordering to sessions table (#930) 2024-01-23 21:33:12 +00:00
Marc KlingenandGitHub 3b2c5f8bf3 feat(ui): add ordering to generations table (#929) 2024-01-23 21:23:47 +00:00
Marc KlingenandGitHub d08a276b67 perf: revert splitting of generations trpc call (#928) 2024-01-23 21:03:17 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
23bc2cd39c chore(deps): bump the next group with 2 updates (#924)
Bumps the next group with 2 updates: [next](https://github.com/vercel/next.js) and [eslint-config-next](https://github.com/vercel/next.js/tree/HEAD/packages/eslint-config-next).


Updates `next` from 14.0.4 to 14.1.0
- [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.4...v14.1.0)

Updates `eslint-config-next` from 14.0.4 to 14.1.0
- [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.1.0/packages/eslint-config-next)

---
updated-dependencies:
- dependency-name: next
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: next
- dependency-name: eslint-config-next
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: next
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-23 14:57:01 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
a31eda8c68 chore(deps): bump date-fns from 3.1.0 to 3.3.1 (#926)
Bumps [date-fns](https://github.com/date-fns/date-fns) from 3.1.0 to 3.3.1.
- [Release notes](https://github.com/date-fns/date-fns/releases)
- [Changelog](https://github.com/date-fns/date-fns/blob/main/CHANGELOG.md)
- [Commits](https://github.com/date-fns/date-fns/compare/v3.1.0...v3.3.1)

---
updated-dependencies:
- dependency-name: date-fns
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-23 15:52:08 +01:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
d044ae51d1 chore(deps-dev): bump @typescript-eslint/parser from 6.16.0 to 6.19.1 (#922)
Bumps [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) from 6.16.0 to 6.19.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.19.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>
2024-01-22 22:11:22 +00:00
Marc KlingenandGitHub 552578afc3 fix(ui): improve ui of tag manager (#921) 2024-01-22 21:41:52 +00:00
Marc Klingen 00997db325 chore: release v1.33.10
CI/CD / push-docker-image (push) Blocked by required conditions
CI/CD / tests (18) (push) Failing after 2m38s
CI/CD / tests (20) (push) Failing after 2m2s
CI/CD / test-docker-build (push) Successful in 5m10s
CI/CD / lint (push) Successful in 5m33s
CI/CD / e2e-tests (push) Failing after 2m17s
CI/CD / all-ci-passed (push) Waiting to run
release.yml / release (push) Failing after 3s
2024-01-22 22:17:00 +01:00
Marc KlingenandGitHub 550464cf0b perf: native db upserts for TraceProcessor and ObservationProcessor (#920) 2024-01-22 22:05:48 +01:00
Max DeichmannandGitHub e43568a3d2 fix: send error only in custom sentry alert (#919) 2024-01-22 18:21:17 +00:00
Max DeichmannandGitHub 4869e8a093 fix: remove sentry instrumentation (#918) 2024-01-22 18:16:27 +00:00
Max Deichmann d9b307ec87 chore: release v1.33.9
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 42s
release.yml / release (push) Failing after 3s
CI/CD / e2e-tests (push) Failing after 1m1s
CI/CD / test-docker-build (push) Successful in 3m52s
CI/CD / lint (push) Successful in 4m15s
CI/CD / all-ci-passed (push) Waiting to run
2024-01-22 13:11:40 +01:00
Max Deichmann 3abfbef4c3 refactor: update openapi docs 2024-01-22 13:08:06 +01:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Max Deichmann
86aa1eb34f chore(deps): bump posthog-js from 1.96.1 to 1.100.0 (#885)
Bumps [posthog-js](https://github.com/PostHog/posthog-js) from 1.96.1 to 1.100.0.
- [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.96.1...v1.100.0)

---
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>
Co-authored-by: Max Deichmann <m.deichmann@tum.de>
2024-01-22 11:35:24 +01:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Max Deichmann
59edc9851a chore(deps): bump the prisma group with 2 updates (#884)
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.7.1 to 5.8.1
- [Release notes](https://github.com/prisma/prisma/releases)
- [Commits](https://github.com/prisma/prisma/commits/5.8.1/packages/client)

Updates `prisma` from 5.7.1 to 5.8.1
- [Release notes](https://github.com/prisma/prisma/releases)
- [Commits](https://github.com/prisma/prisma/commits/5.8.1/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>
Co-authored-by: Max Deichmann <m.deichmann@tum.de>
2024-01-22 10:17:09 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
0e31dcef99 chore(deps-dev): bump @typescript-eslint/eslint-plugin from 6.17.0 to 6.19.0 (#887)
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.17.0 to 6.19.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.19.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>
2024-01-22 11:12:11 +01:00
Max DeichmannandGitHub a67235ab8a feat: support ordering traces on the GET API (#913) 2024-01-19 13:37:31 +00:00
Max Deichmann eb88c9bdf8 chore: release v1.33.8
CI/CD / push-docker-image (push) Blocked by required conditions
CI/CD / tests (18) (push) Failing after 51s
CI/CD / tests (20) (push) Failing after 44s
release.yml / release (push) Failing after 3s
CI/CD / e2e-tests (push) Failing after 48s
CI/CD / lint (push) Successful in 4m17s
CI/CD / test-docker-build (push) Successful in 4m5s
CI/CD / all-ci-passed (push) Waiting to run
2024-01-19 11:58:20 +01:00
Richard KrümmelandGitHub 77e4791cbe feat(ui): add option to keep tracing side menu open (#912) 2024-01-19 11:54:44 +01:00
Max DeichmannandGitHub 5d922dfae7 fix: filter generations metadata correctly (#911) 2024-01-18 15:35:41 +00:00
Max DeichmannandGitHub c704c84451 fix: fix jsonl export to correctly append output (#909) 2024-01-18 13:14:37 +00:00
Max DeichmannandGitHub 6f23488438 fix: sentry reports only unknown errors (#908) 2024-01-18 12:14:54 +00:00
Max Deichmann ca50abb290 chore: release v1.33.7
CI/CD / push-docker-image (push) Blocked by required conditions
CI/CD / tests (18) (push) Failing after 46s
CI/CD / tests (20) (push) Failing after 42s
CI/CD / e2e-tests (push) Failing after 42s
release.yml / release (push) Failing after 2s
CI/CD / test-docker-build (push) Successful in 3m34s
CI/CD / lint (push) Successful in 4m6s
CI/CD / all-ci-passed (push) Waiting to run
2024-01-17 21:18:44 +01:00
Max DeichmannandGitHub 15067114a2 feat: support array of strings in model parameters (#905) 2024-01-17 20:13:12 +00:00
Max DeichmannandGitHub 645f14535b fic: use encoding once (#904) 2024-01-17 16:58:25 +00:00
Max Deichmann 250a5b03d2 chore: release v1.33.6
CI/CD / push-docker-image (push) Blocked by required conditions
CI/CD / tests (18) (push) Failing after 58s
CI/CD / tests (20) (push) Failing after 1m7s
release.yml / release (push) Failing after 3s
CI/CD / test-docker-build (push) Successful in 3m6s
CI/CD / e2e-tests (push) Failing after 48s
CI/CD / lint (push) Successful in 4m1s
CI/CD / all-ci-passed (push) Waiting to run
2024-01-17 16:25:56 +01:00
Max DeichmannandGitHub 5ee060dfc6 refactor: remove external id uniqueness constraint (#903) 2024-01-17 15:24:31 +00:00
Max DeichmannandGitHub f08502cde4 fix: upsert traces on ingestion only on the id (#900) 2024-01-17 13:55:32 +00:00
Max Deichmann 90ecb88249 chore: release v1.33.5
CI/CD / push-docker-image (push) Blocked by required conditions
CI/CD / tests (18) (push) Failing after 48s
CI/CD / tests (20) (push) Failing after 44s
release.yml / release (push) Failing after 2s
CI/CD / e2e-tests (push) Failing after 44s
CI/CD / test-docker-build (push) Successful in 2m47s
CI/CD / lint (push) Successful in 4m15s
CI/CD / all-ci-passed (push) Waiting to run
2024-01-17 14:05:47 +01:00
Max DeichmannandGitHub 16e60f74da fix: capture ingestion errors in sentry (#899) 2024-01-17 12:52:56 +00:00
Max Deichmann 0ab252cc7c chore: release v1.33.3
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 43s
CI/CD / e2e-tests (push) Failing after 42s
release.yml / release (push) Failing after 3s
CI/CD / test-docker-build (push) Successful in 3m39s
CI/CD / lint (push) Successful in 4m7s
CI/CD / all-ci-passed (push) Waiting to run
2024-01-17 13:36:03 +01:00
Max DeichmannandGitHub ebadbaf30d fix: increase ingestion API body size (#898) 2024-01-17 12:23:06 +00:00
Max Deichmann 7ce6b6817e chore: release v1.33.2
CI/CD / push-docker-image (push) Blocked by required conditions
CI/CD / tests (18) (push) Failing after 2m32s
CI/CD / tests (20) (push) Failing after 2m2s
CI/CD / e2e-tests (push) Failing after 2m11s
release.yml / release (push) Failing after 3s
CI/CD / test-docker-build (push) Successful in 5m16s
CI/CD / lint (push) Successful in 5m31s
CI/CD / all-ci-passed (push) Waiting to run
2024-01-17 11:39:23 +01:00
Max DeichmannandGitHub 6d4040d5ed fix: free tiktoken encoder to prevent OOM issues (#897) 2024-01-17 10:32:28 +00:00
Max DeichmannandGitHub 4a7a4c0add feat(infrastructure): add decimal.js support for trpc (#895) 2024-01-16 19:29:29 +01:00
Max Deichmann 883e53d8b5 chore: release v1.33.1
CI/CD / push-docker-image (push) Blocked by required conditions
CI/CD / tests (18) (push) Failing after 2m36s
CI/CD / tests (20) (push) Failing after 2m2s
CI/CD / e2e-tests (push) Failing after 2m14s
CI/CD / lint (push) Successful in 5m48s
CI/CD / test-docker-build (push) Successful in 6m10s
CI/CD / all-ci-passed (push) Waiting to run
release.yml / release (push) Failing after 3s
2024-01-16 18:17:46 +01:00
Richard KrümmelandGitHub a9e6b9ae5c fix: remove tags json view from TracePreview (#894) 2024-01-16 17:12:10 +00:00
Max Deichmann a61ee1f29f chore: release v1.33.0
CI/CD / push-docker-image (push) Blocked by required conditions
CI/CD / tests (18) (push) Failing after 2m50s
CI/CD / tests (20) (push) Failing after 2m2s
CI/CD / test-docker-build (push) Successful in 4m42s
CI/CD / lint (push) Successful in 5m33s
CI/CD / e2e-tests (push) Failing after 2m19s
CI/CD / all-ci-passed (push) Waiting to run
release.yml / release (push) Failing after 3s
2024-01-16 17:44:50 +01:00
Richard KrümmelandGitHub eefdf60e31 feat(ui): add ui for tags on traces (#879) 2024-01-16 17:44:16 +01:00
Max DeichmannandGitHub 19a633a756 fix: add new cost visualisation to trace detail header (#893)
add new cost to traces
2024-01-16 15:49:55 +00:00
Max DeichmannandGitHub e84c1a3ab8 fix: upgrade decimal on export (#892) 2024-01-16 15:32:53 +00:00
Max Deichmann 4d41aa7e53 chore: release v1.32.3
CI/CD / push-docker-image (push) Blocked by required conditions
CI/CD / tests (18) (push) Failing after 2m31s
CI/CD / tests (20) (push) Failing after 2m21s
CI/CD / test-docker-build (push) Successful in 4m44s
CI/CD / e2e-tests (push) Failing after 2m13s
CI/CD / lint (push) Successful in 5m48s
CI/CD / all-ci-passed (push) Waiting to run
release.yml / release (push) Failing after 3s
2024-01-16 16:09:48 +01:00
Max DeichmannandGitHub a4c28cc324 fix: generations table latency (#891) 2024-01-16 15:09:28 +00:00
Max DeichmannandGitHub 187f2e7b1d fix: convert decimals on trpc to number (#890) 2024-01-16 14:50:52 +00:00
Max Deichmann b355825295 chore: release v1.32.2
CI/CD / push-docker-image (push) Blocked by required conditions
CI/CD / tests (18) (push) Failing after 2m37s
CI/CD / tests (20) (push) Failing after 2m4s
CI/CD / e2e-tests (push) Failing after 2m11s
CI/CD / test-docker-build (push) Successful in 5m14s
CI/CD / lint (push) Successful in 5m35s
CI/CD / all-ci-passed (push) Waiting to run
release.yml / release (push) Failing after 4s
2024-01-16 14:47:50 +01:00
Max DeichmannandGitHub 33dc18356a fix: increase decimal places for exports and unify across UI (#889) 2024-01-16 13:35:50 +00:00
Max Deichmann c804d90f6a chore: release v1.32.1
CI/CD / push-docker-image (push) Blocked by required conditions
CI/CD / tests (18) (push) Failing after 2m32s
CI/CD / tests (20) (push) Failing after 2m3s
CI/CD / e2e-tests (push) Failing after 2m11s
CI/CD / test-docker-build (push) Successful in 5m4s
CI/CD / lint (push) Successful in 5m30s
CI/CD / all-ci-passed (push) Waiting to run
release.yml / release (push) Failing after 3s
2024-01-15 19:36:51 +01:00
Max DeichmannandGitHub 09aa547de1 fix: use async sentry span (#883) 2024-01-15 18:35:37 +00:00
Max Deichmann 57839eae5c chore: release v1.32.0
CI/CD / push-docker-image (push) Blocked by required conditions
CI/CD / tests (18) (push) Failing after 2m35s
CI/CD / tests (20) (push) Failing after 2m3s
release.yml / release (push) Failing after 2s
CI/CD / e2e-tests (push) Failing after 2m11s
CI/CD / test-docker-build (push) Successful in 5m4s
CI/CD / lint (push) Successful in 5m33s
CI/CD / all-ci-passed (push) Waiting to run
2024-01-15 19:10:40 +01:00
Max DeichmannandGitHub a43f5f38a1 fix: fix return types on traces router (#882) 2024-01-15 18:09:58 +00:00
8220d07057 feat(ui): add navigation to datasets and dataset items (#871)
* Add DetailPageNav component to Dataset item page

* Add useDetailPageLists hook and useEffect in DatasetItemsTable

* Add useDetailPageLists hook and useEffect in DatasetsTable

* Add DetailPageNav component to Dataset page

* Add DetailPageNav component to DatasetItems

* Add useDetailPageLists hook and useEffect to DatasetRunsTable

* Fix: only show navigation when there is more than one item

* Add DetailPageNav component to DatasetRunItemsTable

* Seed db with two datasets each with two runs and fix CL logging

---------

Co-authored-by: Max Deichmann <m.deichmann@tum.de>
2024-01-15 19:01:40 +01:00
Max Deichmann 89ffe20363 chore: release v1.31.4
CI/CD / push-docker-image (push) Blocked by required conditions
CI/CD / tests (18) (push) Failing after 2m36s
CI/CD / tests (20) (push) Failing after 2m4s
release.yml / release (push) Failing after 2s
CI/CD / e2e-tests (push) Failing after 2m13s
CI/CD / test-docker-build (push) Successful in 5m8s
CI/CD / lint (push) Successful in 5m33s
CI/CD / all-ci-passed (push) Waiting to run
2024-01-15 18:45:23 +01:00
Max DeichmannandGitHub 9a3adf54b1 fix: traces table performance (#881) 2024-01-15 17:44:57 +00:00
Max Deichmann 459129118e chore: release v1.31.3
CI/CD / push-docker-image (push) Blocked by required conditions
CI/CD / tests (18) (push) Failing after 2m32s
CI/CD / tests (20) (push) Failing after 2m2s
release.yml / release (push) Failing after 2s
CI/CD / e2e-tests (push) Failing after 2m8s
CI/CD / test-docker-build (push) Successful in 5m9s
CI/CD / lint (push) Successful in 5m36s
CI/CD / all-ci-passed (push) Waiting to run
2024-01-15 11:26:03 +01:00
Max DeichmannandGitHub 0862370ad6 feat(ui): add time to first token to observation detail view (#878)
add time to first token
2024-01-15 10:25:09 +00:00
Max Deichmann 1bddde99f0 chore: release v1.31.2
CI/CD / push-docker-image (push) Blocked by required conditions
CI/CD / tests (18) (push) Failing after 2m34s
CI/CD / tests (20) (push) Failing after 2m3s
release.yml / release (push) Failing after 3s
CI/CD / e2e-tests (push) Failing after 2m12s
CI/CD / lint (push) Successful in 5m44s
CI/CD / test-docker-build (push) Successful in 5m53s
CI/CD / all-ci-passed (push) Waiting to run
2024-01-11 21:01:07 +01:00
Max DeichmannandGitHub fe2e7681da fix: generations table performance (#872) 2024-01-11 20:50:49 +01:00
Marc Klingen d209766f01 docs: add comment to optimistic update 2024-01-11 18:05:51 +01:00
Richard KrümmelandGitHub 82bfe03dba feat: use react query for optimistic updates (#862) 2024-01-11 16:55:28 +00:00
Marc Klingen 15fc8e4c3c chore: release v1.31.1
CI/CD / push-docker-image (push) Blocked by required conditions
CI/CD / tests (18) (push) Failing after 2m33s
CI/CD / tests (20) (push) Failing after 2m3s
release.yml / release (push) Failing after 3s
CI/CD / e2e-tests (push) Failing after 2m13s
CI/CD / lint (push) Successful in 5m32s
CI/CD / test-docker-build (push) Successful in 6m21s
CI/CD / all-ci-passed (push) Waiting to run
2024-01-11 17:44:18 +01:00
Marc KlingenandGitHub 374dbf9716 feat(tokenization): support alternative model name, gpt-35* instead of gpt-3.5* (#869) 2024-01-11 15:58:16 +00:00
jhlopenandGitHub 6819988f90 perf: return immediately if trace not found (#861) 2024-01-11 15:15:34 +00:00
e3079afb2f fix: assign unique keys to sibling items (#860)
Co-authored-by: Max Deichmann <m.deichmann@tum.de>
2024-01-11 15:10:25 +00:00
HarpinderandGitHub 3b6689f44b fix: correct repo in issue template. (#867)
It pointed to scalar and not langfuse causing confusion while creating issues.
2024-01-11 13:51:45 +01:00
Max DeichmannandGitHub fdf417e961 fix: fix cd pipeline (#863) 2024-01-10 21:18:58 +00:00
Max DeichmannandGitHub a751f1f0f1 refactor: reduce sentry sampling (#858) 2024-01-10 13:57:36 +00:00
Marc KlingenandGitHub 5bf348b55e feat(cloud): add self-serve pricing/checkout page (#857) 2024-01-10 11:39:13 +01:00
Marc Klingen 4ffadc8124 chore: release v1.31.0
CI/CD / push-docker-image (push) Blocked by required conditions
CI/CD / tests (18) (push) Failing after 2m25s
release.yml / release (push) Failing after 2s
CI/CD / tests (20) (push) Failing after 2m20s
CI/CD / e2e-tests (push) Failing after 2m9s
CI/CD / lint (push) Successful in 5m31s
CI/CD / all-ci-passed (push) Waiting to run
2024-01-09 12:01:33 +01:00
Marc KlingenandGitHub 8069fb7ddd feat(cloud): ask new users for referral source, SSO and email/pw (#831) 2024-01-09 10:54:19 +00:00
Marc KlingenandGitHub 133fc90718 feat(ui): nested main navigation for tracing (#834) 2024-01-09 11:50:16 +01:00
Marc KlingenandGitHub 32a2a7956d feat(datasets): delete unused datasets instead of archiving them (#830) 2024-01-09 11:42:38 +01:00
Marc Klingen 01cc9832c1 chore: release v1.30.1
CI/CD / push-docker-image (push) Blocked by required conditions
CI/CD / tests (20) (push) Failing after 2m10s
release.yml / release (push) Failing after 2s
CI/CD / tests (18) (push) Failing after 2m33s
CI/CD / e2e-tests (push) Failing after 2m21s
CI/CD / lint (push) Successful in 5m23s
CI/CD / all-ci-passed (push) Waiting to run
2024-01-09 02:32:58 +01:00
Marc Klingen 6127ea6442 fix: next try to fix docker build problem 2024-01-09 02:22:44 +01:00
Marc Klingen 22fbc2a89d fix: revert 2d8cc2baa3 2024-01-09 02:08:36 +01:00
Marc Klingen 1ed2e230e0 fix: revert to prev @sentry/profiling-node to fix docker build issue 2024-01-09 01:57:42 +01:00
Marc Klingen 6563716f21 chore(deps): bump @sentry/types to fix build issues 2024-01-09 01:03:14 +01:00
Marc KlingenandGitHub 487e0a77d6 fix(ui): scroll behavior of large traces (#846)
fix(ui): scroll behvaior of large traces
2024-01-09 00:00:47 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
c084d42b4a chore(deps): bump lucide-react from 0.306.0 to 0.307.0 (#841)
Bumps [lucide-react](https://github.com/lucide-icons/lucide/tree/HEAD/packages/lucide-react) from 0.306.0 to 0.307.0.
- [Release notes](https://github.com/lucide-icons/lucide/releases)
- [Commits](https://github.com/lucide-icons/lucide/commits/0.307.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>
2024-01-08 23:08:01 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2d8cc2baa3 chore(deps): bump the patches group with 4 updates (#839)
Bumps the patches group with 4 updates: [@headlessui/react](https://github.com/tailwindlabs/headlessui/tree/HEAD/packages/@headlessui-react), [@sentry/profiling-node](https://github.com/getsentry/profiling-node), [react-hook-form](https://github.com/react-hook-form/react-hook-form) and [@types/react](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react).


Updates `@headlessui/react` from 1.7.17 to 1.7.18
- [Release notes](https://github.com/tailwindlabs/headlessui/releases)
- [Changelog](https://github.com/tailwindlabs/headlessui/blob/main/packages/@headlessui-react/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/headlessui/commits/@headlessui/react@v1.7.18/packages/@headlessui-react)

Updates `@sentry/profiling-node` from 1.3.2 to 1.3.3
- [Release notes](https://github.com/getsentry/profiling-node/releases)
- [Changelog](https://github.com/getsentry/profiling-node/blob/main/CHANGES)
- [Commits](https://github.com/getsentry/profiling-node/compare/v1.3.2...v1.3.3)

Updates `react-hook-form` from 7.49.2 to 7.49.3
- [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.2...v7.49.3)

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

---
updated-dependencies:
- dependency-name: "@headlessui/react"
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: patches
- dependency-name: "@sentry/profiling-node"
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: patches
- dependency-name: react-hook-form
  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>
2024-01-09 00:03:56 +01:00
Marc Klingen 47630f7c5f chore: issue creation should be enabled 2024-01-08 22:41:06 +01:00
Marc Klingen 082dfaf339 docs: add note on database schema to contributing.md 2024-01-07 17:25:29 +01:00
Max DeichmannandGitHub f8efbd51da style: add db diagram to contributing.md (#833) 2024-01-07 14:13:15 +00:00
Marc Klingen 22d98256ce chore: issue templates 2024-01-06 19:05:27 +01:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
00caa680f2 chore(deps-dev): bump the patches group with 2 updates (#823)
Bumps the patches group with 2 updates: [prettier-plugin-tailwindcss](https://github.com/tailwindlabs/prettier-plugin-tailwindcss) and [tailwindcss](https://github.com/tailwindlabs/tailwindcss).


Updates `prettier-plugin-tailwindcss` from 0.5.10 to 0.5.11
- [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.10...v0.5.11)

Updates `tailwindcss` from 3.4.0 to 3.4.1
- [Release notes](https://github.com/tailwindlabs/tailwindcss/releases)
- [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/v3.4.1/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/tailwindcss/compare/v3.4.0...v3.4.1)

---
updated-dependencies:
- 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>
2024-01-06 02:03:37 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
48d35eb637 chore(deps-dev): bump @typescript-eslint/eslint-plugin from 6.16.0 to 6.17.0 (#824)
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.16.0 to 6.17.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.17.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>
2024-01-06 01:56:09 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
61786fedfb chore(deps): bump date-fns from 3.0.6 to 3.1.0 (#825)
Bumps [date-fns](https://github.com/date-fns/date-fns) from 3.0.6 to 3.1.0.
- [Release notes](https://github.com/date-fns/date-fns/releases)
- [Changelog](https://github.com/date-fns/date-fns/blob/main/CHANGELOG.md)
- [Commits](https://github.com/date-fns/date-fns/compare/v3.0.6...v3.1.0)

---
updated-dependencies:
- dependency-name: date-fns
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-06 01:51:47 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
1f3e3603be chore(deps): bump lucide-react from 0.304.0 to 0.306.0 (#826)
Bumps [lucide-react](https://github.com/lucide-icons/lucide/tree/HEAD/packages/lucide-react) from 0.304.0 to 0.306.0.
- [Release notes](https://github.com/lucide-icons/lucide/releases)
- [Commits](https://github.com/lucide-icons/lucide/commits/0.306.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>
2024-01-06 01:47:38 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
cee2c797c4 chore(deps): bump @aws-sdk/s3-request-presigner from 3.478.0 to 3.485.0 (#827)
Bumps [@aws-sdk/s3-request-presigner](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/packages/s3-request-presigner) from 3.478.0 to 3.485.0.
- [Release notes](https://github.com/aws/aws-sdk-js-v3/releases)
- [Changelog](https://github.com/aws/aws-sdk-js-v3/blob/main/packages/s3-request-presigner/CHANGELOG.md)
- [Commits](https://github.com/aws/aws-sdk-js-v3/commits/v3.485.0/packages/s3-request-presigner)

---
updated-dependencies:
- dependency-name: "@aws-sdk/s3-request-presigner"
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-06 01:43:38 +00:00
95 changed files with 3255 additions and 1440 deletions
+7 -1
View File
@@ -16,7 +16,7 @@ const config = {
rules: {
"@typescript-eslint/no-non-null-assertion": "off",
"@typescript-eslint/no-confusing-void-expression": "off",
}
},
},
],
parser: "@typescript-eslint/parser",
@@ -35,6 +35,12 @@ const config = {
},
],
"@typescript-eslint/no-unused-vars": ["warn", { argsIgnorePattern: "^_" }],
"react/jsx-key": [
"error",
{
warnOnDuplicates: true,
},
],
},
};
+7
View File
@@ -0,0 +1,7 @@
contact_links:
- name: 🤗 Get Help
url: https://github.com/langfuse/langfuse/discussions/new?category=q-a
about: If you cant get something to work the way you expect, open a question in our discussion forums.
- name: 💡 Feature Request
url: https://github.com/langfuse/langfuse/discussions/new?category=ideas
about: Suggest any ideas you have using our discussion forums.
@@ -1,36 +0,0 @@
# This template is heavily inspired by the Next.js's template:
# See here: https://github.com/vercel/next.js/blob/canary/.github/ISSUE_TEMPLATE/3.feature_request.yml
name: 🛠 Feature Request
description: Create a feature request for the core packages
title: "feat: "
labels: ["✨ enhancement"]
body:
- type: markdown
attributes:
value: |
Thank you for taking the time to file a feature request. Please fill out this form as completely as possible.
- type: textarea
attributes:
label: Describe the feature you'd like to request
description: Please describe the feature as clear and concise as possible. Remember to add context as to why you believe this feature is needed.
validations:
required: true
- type: textarea
attributes:
label: Describe the solution you'd like to see
description: Please describe the solution you would like to see. Adding example usage is a good way to provide context.
validations:
required: true
- type: textarea
attributes:
label: Additional information
description: Add any other information related to the feature here. If your feature request is related to any issues or discussions, link them here.
- type: checkboxes
id: contribute
attributes:
label: Contribute
description: Are you willing to contribute to the implementation of this feature?
options:
- label: Yes, I can implement this and raise a PR
+31 -1
View File
@@ -30,6 +30,36 @@ jobs:
- name: lint
run: npm run lint
test-docker-build:
runs-on: ubuntu-latest
env:
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/postgres
NEXTAUTH_SECRET: "secret"
SALT: "salt"
NEXTAUTH_URL: "http://localhost:3030"
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
steps:
- name: Setup node
uses: actions/setup-node@v3
with:
node-version: 20
- name: Checkout
uses: actions/checkout@v3
- name: Build Docker image
uses: docker/build-push-action@v4
with:
context: .
push: false
build-args: |
DATABASE_URL=${{ env.DATABASE_URL }}
NEXTAUTH_SECRET=${{ env.NEXTAUTH_SECRET }}
NEXTAUTH_URL=${{ env.NEXTAUTH_URL }}
SALT=${{ env.SALT }}
tests:
runs-on: ubuntu-latest
strategy:
@@ -101,7 +131,7 @@ jobs:
all-ci-passed:
# This allows us to have a branch protection rule for tests and deploys with matrix
runs-on: ubuntu-latest
needs: [lint, tests, e2e-tests]
needs: [lint, tests, e2e-tests, test-docker-build]
if: always()
steps:
- name: Successful deploy
+8
View File
@@ -79,6 +79,14 @@ flowchart TB
Python --- API
```
### Database Overview
The diagram below may not show all relationships if the foreign key is not defined in the database schema. For instance, `trace_id` in the `observation` table is not defined as a foreign key to the `trace` table to allow unordered ingestion of these objects, but it is still a foreign key in the application code.
Full database schema: [prisma/schema.prisma](prisma/schema.prisma)
<img src="./prisma/database.svg">
### Infrastructure & Network Overview
```mermaid
+1 -1
View File
@@ -80,7 +80,6 @@ types:
properties:
id: string
name: string
status: DatasetStatus
projectId: string
createdAt: datetime
updatedAt: datetime
@@ -130,6 +129,7 @@ types:
- optional<string>
- optional<integer>
- optional<boolean>
- optional<list<string>>
DatasetStatus:
enum:
- ACTIVE
+6
View File
@@ -26,6 +26,9 @@ service:
limit: optional<integer>
userId: optional<string>
name: optional<string>
orderBy:
type: string
docs: Format of the string sort_by=timestamp.asc (id, timestamp, name, userId, release, version, public, bookmarked, sessionId)
tags:
type: optional<string>
allow-multiple: true
@@ -37,3 +40,6 @@ types:
properties:
data: list<commons.TraceWithDetails>
meta: pagination.MetaResponse
Sort:
properties:
id: string
+20 -3
View File
@@ -884,6 +884,14 @@ paths:
schema:
type: string
nullable: true
- name: orderBy
in: query
description: >-
Format of the string sort_by=timestamp.asc (id, timestamp, name,
userId, release, version, public, bookmarked, sessionId)
required: true
schema:
type: string
- name: tags
in: query
description: Only traces that include all of these tags will be returned.
@@ -1147,8 +1155,6 @@ components:
type: string
name:
type: string
status:
$ref: '#/components/schemas/DatasetStatus'
projectId:
type: string
createdAt:
@@ -1168,7 +1174,6 @@ components:
required:
- id
- name
- status
- projectId
- createdAt
- updatedAt
@@ -1278,6 +1283,10 @@ components:
nullable: true
- type: boolean
nullable: true
- type: array
items:
type: string
nullable: true
DatasetStatus:
title: DatasetStatus
type: string
@@ -1991,6 +2000,14 @@ components:
required:
- data
- meta
Sort:
title: Sort
type: object
properties:
id:
type: string
required:
- id
utilsMetaResponse:
title: utilsMetaResponse
type: object
+6 -1
View File
@@ -708,7 +708,7 @@
"request": {
"description": "Get list of traces",
"url": {
"raw": "{{baseUrl}}/api/public/traces?page=&limit=&userId=&name=&tags=",
"raw": "{{baseUrl}}/api/public/traces?page=&limit=&userId=&name=&orderBy=&tags=",
"host": [
"{{baseUrl}}"
],
@@ -738,6 +738,11 @@
"value": "",
"description": null
},
{
"key": "orderBy",
"value": "",
"description": "Format of the string sort_by=timestamp.asc (id, timestamp, name, userId, release, version, public, bookmarked, sessionId)"
},
{
"key": "tags",
"value": "",
+1108 -639
View File
File diff suppressed because it is too large Load Diff
+20 -17
View File
@@ -1,6 +1,6 @@
{
"name": "langfuse-core",
"version": "1.30.0",
"version": "1.34.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",
@@ -16,7 +16,7 @@
"test": "jest --runInBand",
"test:watch": "jest --watch --runInBand",
"test:e2e": "playwright test",
"db:migrate": "npx prisma migrate dev",
"db:migrate": "DISABLE_ERD=false npx prisma migrate dev",
"db:reset": "npx prisma migrate reset",
"db:seed": "npx prisma db seed",
"db:seed:examples": "npx prisma db seed -- --environment examples",
@@ -28,12 +28,12 @@
"dependencies": {
"@anthropic-ai/tokenizer": "^0.0.4",
"@aws-sdk/client-s3": "^3.485.0",
"@aws-sdk/s3-request-presigner": "^3.478.0",
"@aws-sdk/s3-request-presigner": "^3.485.0",
"@headlessui/react": "^1.7.17",
"@heroicons/react": "^2.1.1",
"@hookform/resolvers": "^3.3.4",
"@next-auth/prisma-adapter": "^1.0.7",
"@prisma/client": "^5.7.1",
"@prisma/client": "^5.8.1",
"@radix-ui/react-accordion": "^1.1.2",
"@radix-ui/react-alert-dialog": "^1.0.5",
"@radix-ui/react-avatar": "^1.0.4",
@@ -45,6 +45,7 @@
"@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-label": "^2.0.2",
"@radix-ui/react-popover": "^1.0.7",
"@radix-ui/react-scroll-area": "^1.0.5",
"@radix-ui/react-select": "^2.0.0",
"@radix-ui/react-separator": "^1.0.3",
"@radix-ui/react-slider": "^1.1.2",
@@ -55,8 +56,8 @@
"@radix-ui/react-tooltip": "^1.0.7",
"@react-email/components": "^0.0.12",
"@react-email/render": "^0.0.10",
"@sentry/nextjs": "^7.92.0",
"@sentry/profiling-node": "^1.3.2",
"@sentry/nextjs": "^7.95.0",
"@sentry/profiling-node": "^1.3.5",
"@sentry/types": "^7.88.0",
"@t3-oss/env-nextjs": "^0.7.1",
"@tailwindcss/forms": "^0.5.7",
@@ -74,22 +75,22 @@
"cmdk": "^0.2.0",
"core-js": "^3.35.0",
"cors": "^2.8.5",
"date-fns": "^3.0.6",
"date-fns": "^3.3.1",
"decimal.js": "^10.4.3",
"exponential-backoff": "^3.1.1",
"lodash": "^4.17.21",
"lucide-react": "^0.304.0",
"next": "^14.0.4",
"lucide-react": "^0.314.0",
"next": "^14.1.0",
"next-auth": "^4.24.5",
"next-query-params": "^5.0.0",
"nodemailer": "^6.9.8",
"posthog-js": "^1.96.1",
"posthog-js": "^1.100.0",
"posthog-node": "^3.2.1",
"react": "18.2.0",
"react-day-picker": "^8.10.0",
"react-dom": "18.2.0",
"react-hook-form": "^7.49.2",
"react-icons": "^4.12.0",
"react-icons": "^5.0.1",
"react-responsive": "^9.0.2",
"react18-json-view": "^0.2.7-canary.2",
"superjson": "2.2.1",
@@ -102,6 +103,7 @@
},
"devDependencies": {
"@jedmao/location": "^3.0.0",
"@mermaid-js/mermaid-cli": "^10.6.1",
"@playwright/test": "^1.40.1",
"@release-it/bumper": "^6.0.1",
"@testing-library/jest-dom": "^6.1.5",
@@ -116,20 +118,21 @@
"@types/react": "^18.2.46",
"@types/react-dom": "^18.2.18",
"@types/uuid": "^9.0.7",
"@typescript-eslint/eslint-plugin": "^6.16.0",
"@typescript-eslint/parser": "^6.16.0",
"@typescript-eslint/eslint-plugin": "^6.19.0",
"@typescript-eslint/parser": "^6.19.1",
"autoprefixer": "^10.4.16",
"dotenv-cli": "^7.3.0",
"eslint": "^8.56.0",
"eslint-config-next": "^14.0.4",
"eslint-config-next": "^14.1.0",
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
"postcss": "^8.4.33",
"prettier": "^3.1.1",
"prettier-plugin-tailwindcss": "^0.5.10",
"prisma": "^5.7.1",
"prettier-plugin-tailwindcss": "^0.5.11",
"prisma": "^5.8.1",
"prisma-erd-generator": "^1.11.2",
"release-it": "^17.0.1",
"tailwindcss": "^3.4.0",
"tailwindcss": "^3.4.1",
"ts-node": "^10.9.2",
"tsconfig-paths": "^4.2.0",
"typescript": "^5.3.3"
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 194 KiB

@@ -0,0 +1,8 @@
/*
Warnings:
- You are about to drop the column `status` on the `datasets` table. All the data in the column will be lost.
*/
-- AlterTable
ALTER TABLE "datasets" DROP COLUMN "status";
@@ -0,0 +1,16 @@
-- Migration to add gpt-35 spelling
INSERT INTO pricings (
id,
model_name,
pricing_unit,
price,
currency,
token_type
)
VALUES
('clqqpc2pr000008l3hvy63gxy1','gpt-35-turbo-1106', 'PER_1000_TOKENS', 0.001, 'USD', 'PROMPT'),
('clqqpcb6d000208l3atrfbmou1','gpt-35-turbo-1106', 'PER_1000_TOKENS', 0.002, 'USD', 'COMPLETION'),
('clqqpdh45000008lfgrnx76cv1','gpt-35-turbo-instruct', 'PER_1000_TOKENS', 0.0015, 'USD', 'PROMPT'),
('clqqpdjya000108lf3s4b4c4m1','gpt-35-turbo-instruct', 'PER_1000_TOKENS', 0.002, 'USD', 'COMPLETION')
ON CONFLICT (id) DO NOTHING;
@@ -0,0 +1,2 @@
-- DropIndex
DROP INDEX "traces_project_id_external_id_key";
+8 -2
View File
@@ -13,6 +13,14 @@ datasource db {
shadowDatabaseUrl = env("SHADOW_DATABASE_URL")
}
generator erd {
provider = "prisma-erd-generator"
ignoreTables = ["_prisma_migrations", "Session", "Account", "Example"]
disabled = true
ignoreEnums = true
output = "database.svg"
}
model Example {
id String @id @default(cuid())
createdAt DateTime @default(now()) @map("created_at")
@@ -190,7 +198,6 @@ model Trace {
scores Score[]
@@unique([projectId, externalId])
@@index([projectId])
@@index([sessionId])
@@index([name])
@@ -312,7 +319,6 @@ model CronJobs {
model Dataset {
id String @id @default(cuid())
name String
status DatasetStatus @default(ACTIVE)
projectId String @map("project_id")
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now()) @map("created_at")
+46 -39
View File
@@ -127,10 +127,10 @@ async function main() {
for (let i = 0; i < TRACE_VOLUME; i++) {
// print progress to console with a progress bar that refreshes every 10 iterations
if (i % 10 === 0) {
if ((i + 1) % 10 === 0 || i === TRACE_VOLUME - 1) {
process.stdout.clearLine(0);
process.stdout.cursorTo(0);
process.stdout.write(`Seeding ${i} of ${TRACE_VOLUME}`);
process.stdout.write(`Seeding ${i + 1} of ${TRACE_VOLUME}`);
}
// random date within last 90 days, with a linear bias towards more recent dates
const traceTs = new Date(
@@ -409,47 +409,54 @@ async function main() {
}
}
const dataset = await prisma.dataset.create({
data: {
name: "demo-dataset",
projectId: project2.id,
},
});
const datasetRun = await prisma.datasetRuns.create({
data: {
name: "demo-dataset-run",
datasetId: dataset.id,
},
});
for (let runNumber = 0; runNumber < 10; runNumber++) {
//pick randomly from existingSpanIds
const sourceObservationId =
generationIds[Math.floor(Math.random() * generationIds.length)];
const runObservationId =
generationIds[Math.floor(Math.random() * generationIds.length)];
const datasetItem = await prisma.datasetItem.create({
for (let datasetNumber = 0; datasetNumber < 2; datasetNumber++) {
const dataset = await prisma.dataset.create({
data: {
datasetId: dataset.id,
sourceObservationId:
Math.random() > 0.5 ? sourceObservationId : undefined,
input: [
{ role: "user", content: "How can i create a React component?" },
],
expectedOutput:
"Creating a React component can be done in two ways: as a functional component or as a class component. Let's start with a basic example of both.",
name: `demo-dataset-${datasetNumber}`,
projectId: project2.id,
},
});
await prisma.datasetRunItems.create({
data: {
datasetItemId: datasetItem.id,
observationId: runObservationId!,
datasetRunId: datasetRun.id,
},
});
for (let datasetRunNumber = 0; datasetRunNumber < 2; datasetRunNumber++) {
const datasetRun = await prisma.datasetRuns.create({
data: {
name: `demo-dataset-run-${datasetRunNumber}`,
datasetId: dataset.id,
},
});
for (let runNumber = 0; runNumber < 10; runNumber++) {
//pick randomly from existingSpanIds
const sourceObservationId =
generationIds[Math.floor(Math.random() * generationIds.length)];
const runObservationId =
generationIds[Math.floor(Math.random() * generationIds.length)];
const datasetItem = await prisma.datasetItem.create({
data: {
datasetId: dataset.id,
sourceObservationId:
Math.random() > 0.5 ? sourceObservationId : undefined,
input: [
{
role: "user",
content: "How can i create a React component?",
},
],
expectedOutput:
"Creating a React component can be done in two ways: as a functional component or as a class component. Let's start with a basic example of both.",
},
});
await prisma.datasetRunItems.create({
data: {
datasetItemId: datasetItem.id,
observationId: runObservationId!,
datasetRunId: datasetRun.id,
},
});
}
}
}
}
}
+1 -1
View File
@@ -9,7 +9,7 @@ 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.3,
// Capture Replay for 100% of all sessions,
// plus for 100% of sessions with an error
+1 -1
View File
@@ -7,7 +7,7 @@ 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.75,
tracesSampleRate: 0.3,
// ...
+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.5,
tracesSampleRate: 0.3,
profilesSampleRate: 0.5, // Profiling sample rate is relative to tracesSampleRate
profilesSampleRate: 0.3, // Profiling sample rate is relative to tracesSampleRate
integrations: [
// Add profiling integration to list of integrations
new ProfilingIntegration(),
+1
View File
@@ -2,6 +2,7 @@ import { test, expect } from "@playwright/test";
test("should redirect to sign-in if not signed in", async ({ page }) => {
await page.goto("/");
await page.waitForTimeout(2000);
await expect(page).toHaveURL("/auth/sign-in");
});
+16 -7
View File
@@ -3,28 +3,33 @@ import { test, expect } from "@playwright/test";
test("should see new projects dialog open after clicking new project btn", async ({
page,
}) => {
await page.goto("/auth/sign-in");
await page.fill('input[name="email"]', "demo@langfuse.com");
await page.goto("auth/sign-up");
await page.fill('input[name="name"]', "demo user");
await page.fill('input[name="email"]', randomEmailAddress());
await page.fill('input[type="password"]', "password");
await page.click('button[type="submit"]');
await page.waitForTimeout(2000);
await page.waitForTimeout(2000);
await page.isVisible("Create new project");
expect(await page.innerHTML("data-testid=create-new-project-title")).toBe(
"Create new project",
);
await page.click('[data-testid="create-project-btn"]');
await page.waitForTimeout(2000);
await page.isVisible('[data-testid="new-project-form"]');
await expect(page.locator("data-testid=new-project-form")).toBeVisible();
});
test("Create a project with provided name", async ({ page }) => {
test.setTimeout(60000);
await page.goto("/auth/sign-in");
await page.fill('input[name="email"]', "demo@langfuse.com");
await page.fill('input[type="password"]', "password");
await page.click('button[type="submit"]');
await page.waitForTimeout(2000);
await page.isVisible("Create new project");
await page.isVisible('[data-testid="new-project-form"]');
await page.click('[data-testid="create-project-btn"]');
await page.waitForTimeout(2000);
await page.isVisible('[data-testid="new-project-form"]');
await expect(page.locator("data-testid=new-project-form")).toBeVisible();
await page.fill(
'[data-testid="new-project-name-input"]',
"my e2e demo project",
@@ -33,7 +38,11 @@ test("Create a project with provided name", async ({ page }) => {
await page.waitForTimeout(2000);
expect(page.url()).toContain("/project/");
await page.waitForTimeout(2000);
expect(await page.getByTestId("project-title-span-1").textContent()).toBe(
expect(await page.getByTestId("project-name").textContent()).toContain(
"my e2e demo project",
);
});
// random email address to be used in tests
const randomEmailAddress = () =>
Math.random().toString(36).substring(2, 11) + "@example.com";
+64
View File
@@ -300,6 +300,7 @@ describe("/api/public/ingestion API Endpoint", () => {
id: generationId,
traceId: traceId,
parentObservationId: spanId,
modelParameters: { someKey: ["user-1", "user-2"] },
},
},
{
@@ -376,6 +377,9 @@ describe("/api/public/ingestion API Endpoint", () => {
expect(dbGeneration?.traceId).toBe(traceId);
expect(dbGeneration?.name).toBe("generation-name");
expect(dbGeneration?.parentObservationId).toBe(spanId);
expect(dbGeneration?.modelParameters).toEqual({
someKey: ["user-1", "user-2"],
});
const dbEvent = await prisma.observation.findUnique({
where: {
@@ -790,6 +794,66 @@ describe("/api/public/ingestion API Endpoint", () => {
expect(dbTrace[0]?.version).toBe("2.0.0");
});
it("should not override a trace from a different project", async () => {
const traceId = v4();
const newProjectId = v4();
await prisma.project.create({
data: {
id: newProjectId,
name: "another-project",
},
});
await prisma.trace.create({
data: {
id: traceId,
project: { connect: { id: newProjectId } },
},
});
const responseOne = await makeAPICall("POST", "/api/public/ingestion", {
batch: [
{
id: v4(),
type: "trace-create",
timestamp: new Date().toISOString(),
body: {
id: traceId,
name: "trace-name",
userId: "user-1",
metadata: { key: "value" },
release: "1.0.0",
version: "2.0.0",
},
},
],
});
expect(responseOne.status).toBe(207);
console.log(responseOne.body);
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
const errors = responseOne.body.errors;
expect(errors).toBeDefined();
console.log(errors);
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
expect(errors.length).toBe(1);
const dbTrace = await prisma.trace.findMany({
where: {
id: traceId,
},
});
expect(dbTrace.length).toEqual(1);
expect(dbTrace[0]?.name).toBeNull();
expect(dbTrace[0]?.release).toBeNull();
expect(dbTrace[0]?.metadata).toBeNull();
expect(dbTrace[0]?.version).toBeNull();
});
[
{
inputs: [{ a: "a" }, { b: "b" }],
@@ -53,7 +53,7 @@ describe("Traces TRPC Router", () => {
orderBy: null,
});
expect(traces).toBeDefined();
expect(traces).toMatchObject([trace]);
expect(traces).toMatchObject({ traces: [trace] });
});
test("traces.all RPC orders traces by userId", async () => {
@@ -88,7 +88,7 @@ describe("Traces TRPC Router", () => {
order: "ASC",
},
});
expect(tracesASC).toMatchObject([trace1, trace2]);
expect(tracesASC).toMatchObject({ traces: [trace1, trace2] });
const tracesDESC = await caller.traces.all({
page: 0,
@@ -102,6 +102,6 @@ describe("Traces TRPC Router", () => {
order: "DESC",
},
});
expect(tracesDESC).toMatchObject([trace2, trace1]);
expect(tracesDESC).toMatchObject({ traces: [trace2, trace1] });
});
});
+3
View File
@@ -4,11 +4,13 @@ describe("Token Count Functions", () => {
describe("token count for strings", () => {
[
{ model: "gpt-3.5", tokens: 114 },
{ model: "gpt-35", tokens: 114 },
{ model: "gpt-4-1106-preview", tokens: 114 },
{ 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 },
{ model: "gpt-35-turbo-1106", tokens: 114 },
].forEach(({ model, tokens }) => {
it(`should return token count ${tokens} for ${model}`, () => {
const result = tokenCount({
@@ -71,6 +73,7 @@ describe("Token Count Functions", () => {
[
{ model: "gpt-4", tokens: 44 },
{ model: "gpt-3.5-turbo-16k-0613", tokens: 44 },
{ model: "gpt-35-turbo-16k-0613", tokens: 44 },
{ model: "claude-instant-1.2", tokens: 48 },
].forEach(({ model, tokens }) => {
it(`should return token count ${tokens} for ${model}`, () => {
+17 -6
View File
@@ -4,13 +4,18 @@ 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";
import { cn } from "@/src/utils/tailwind";
export default function Header(props: {
export default function Header({
level = "h2",
...props
}: {
title: string;
breadcrumb?: { name: string; href?: string }[];
status?: Status;
help?: { description: string; href: string };
actionButtons?: React.ReactNode;
level?: "h2" | "h3";
}) {
const router = useRouter();
const session = useSession();
@@ -35,7 +40,7 @@ export default function Header(props: {
[...props.breadcrumb.map((i) => i.href).filter(Boolean)].pop();
return (
<div className="mb-8">
<div className={cn(level === "h2" ? "mb-8" : "mb-1")}>
<div>
{backHref ? (
<nav className="sm:hidden" aria-label="Back">
@@ -51,7 +56,7 @@ export default function Header(props: {
</Link>
</nav>
) : null}
{breadcrumb.length ? (
{(level === "h2" || props.breadcrumb) && breadcrumb.length ? (
<nav className="hidden sm:flex" aria-label="Breadcrumb">
<ol role="list" className="flex items-center space-x-4">
{breadcrumb.map(({ name, href }, index) => (
@@ -85,9 +90,15 @@ export default function Header(props: {
<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="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>
{level === "h2" ? (
<h2 className="text-2xl font-bold leading-7 text-gray-900 sm:truncate sm:text-3xl sm:tracking-tight">
{props.title}
</h2>
) : (
<h3 className="text-lg font-bold leading-7 text-gray-900 sm:truncate sm:text-xl sm:tracking-tight">
{props.title}
</h3>
)}
{props.help ? (
<DocPopup
description={props.help.description}
+208 -133
View File
@@ -1,12 +1,12 @@
import { ROUTES } from "@/src/components/layouts/routes";
import { ROUTES, type Route } from "@/src/components/layouts/routes";
import { Fragment, type PropsWithChildren, useState } from "react";
import { Dialog, Menu, Transition } from "@headlessui/react";
import { Dialog, Disclosure, Menu, Transition } from "@headlessui/react";
import { Bars3Icon, XMarkIcon } from "@heroicons/react/24/outline";
import Link from "next/link";
import { useRouter } from "next/router";
import clsx from "clsx";
import { Code, MessageSquarePlus, Info } from "lucide-react";
import { Code, MessageSquarePlus, Info, ChevronRightIcon } from "lucide-react";
import { signOut, useSession } from "next-auth/react";
import { ChevronDownIcon } from "@heroicons/react/20/solid";
import { cn } from "@/src/utils/tailwind";
@@ -35,9 +35,9 @@ const userNavigation = [
},
];
const pathsWithoutNavigation: string[] = [];
const unauthenticatedPaths = ["/auth/sign-in", "/auth/sign-up"];
const publishablePaths = [
const pathsWithoutNavigation: string[] = ["/onboarding"];
const unauthenticatedPaths: string[] = ["/auth/sign-in", "/auth/sign-up"];
const publishablePaths: string[] = [
"/project/[projectId]/sessions/[sessionId]",
"/project/[projectId]/traces/[traceId]",
];
@@ -50,30 +50,47 @@ export default function Layout(props: PropsWithChildren) {
api.environment.enableExperimentalFeatures.useQuery().data ?? false;
const projectId = router.query.projectId as string | undefined;
const navigation = ROUTES.filter(
({ pathname }) => projectId || !pathname.includes("[projectId]"),
)
.filter(
({ featureFlag }) =>
featureFlag === undefined ||
const mapNavigation = (route: Route): NavigationItem | null => {
// Project-level routes
if (!projectId && route.pathname?.includes("[projectId]")) return null;
// Feature Flags
if (
!(
route.featureFlag === undefined ||
enableExperimentalFeatures ||
session.data?.user?.featureFlags[featureFlag],
session.data?.user?.featureFlags[route.featureFlag]
)
)
.filter(
({ rbacScope }) =>
rbacScope === undefined ||
(projectId && hasAccess({ projectId, scope: rbacScope, session })),
return null;
// RBAC
if (
route.rbacScope !== undefined &&
(!projectId || !hasAccess({ projectId, scope: route.rbacScope, session }))
)
.map(({ pathname, ...rest }) => ({
pathname,
href: pathname.replace("[projectId]", projectId ?? ""),
...rest,
}))
.map(({ pathname, ...rest }) => ({
pathname,
current: router.pathname === pathname,
...rest,
}));
return null;
// apply to children as well
const children: (NavigationItem | null)[] =
route.children?.map((child) => mapNavigation(child)).filter(Boolean) ??
[];
return {
...route,
href: route.pathname?.replace("[projectId]", projectId ?? ""),
current: router.pathname === route.pathname,
children:
children.length > 0
? (children as NavigationItem[]) // does not include null due to filter
: undefined,
};
};
const navigationMapped: (NavigationItem | null)[] = ROUTES.map((route) =>
mapNavigation(route),
).filter(Boolean);
const navigation = navigationMapped.filter(Boolean) as NavigationItem[]; // does not include null due to filter
const currentPathName = navigation.find(({ current }) => current)?.name;
@@ -124,7 +141,6 @@ export default function Layout(props: PropsWithChildren) {
{props.children}
</main>
);
return (
<>
<Head>
@@ -207,58 +223,7 @@ export default function Layout(props: PropsWithChildren) {
<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}
onClick={() => setSidebarOpen(false)}
>
<Link
href={item.href}
className={clsx(
item.current
? "bg-gray-50 text-indigo-600"
: "text-gray-700 hover:bg-gray-50 hover:text-indigo-600",
"group flex gap-x-3 rounded-md p-2 text-sm font-semibold leading-6",
)}
>
<item.icon
className={clsx(
item.current
? "text-indigo-600"
: "text-gray-400 group-hover:text-indigo-600",
"h-6 w-6 shrink-0",
)}
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>
))}
<FeedbackButtonWrapper className="w-full">
<li className="group flex cursor-pointer gap-x-3 rounded-md p-2 text-sm font-semibold leading-6 text-gray-700 hover:bg-gray-50 hover:text-indigo-600">
<MessageSquarePlus
className="h-6 w-6 shrink-0 text-gray-400 group-hover:text-indigo-600"
aria-hidden="true"
/>
Feedback
</li>
</FeedbackButtonWrapper>
</ul>
</li>
<MainNavigation nav={navigation} />
<li>
<div className="flex flex-row place-content-between items-center">
<div className="text-xs font-semibold leading-6 text-gray-400">
@@ -324,56 +289,7 @@ export default function Layout(props: PropsWithChildren) {
<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) => (
<li key={item.name}>
<Link
href={item.href}
className={clsx(
item.current
? "bg-gray-50 text-indigo-600"
: "text-gray-700 hover:bg-gray-50 hover:text-indigo-600",
"group flex gap-x-3 rounded-md p-2 text-sm font-semibold leading-6",
)}
>
<item.icon
className={clsx(
item.current
? "text-indigo-600"
: "text-gray-400 group-hover:text-indigo-600",
"h-6 w-6 shrink-0",
)}
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>
))}
<FeedbackButtonWrapper className="w-full">
<li className="group flex cursor-pointer gap-x-3 rounded-md p-2 text-sm font-semibold leading-6 text-gray-700 hover:bg-gray-50 hover:text-indigo-600">
<MessageSquarePlus
className="h-6 w-6 shrink-0 text-gray-400 group-hover:text-indigo-600"
aria-hidden="true"
/>
Feedback
</li>
</FeedbackButtonWrapper>
</ul>
</li>
<MainNavigation nav={navigation} />
<li className="mt-auto">
<div className="flex flex-row place-content-between items-center">
<div className="text-xs font-semibold leading-6 text-gray-400">
@@ -569,11 +485,170 @@ export default function Layout(props: PropsWithChildren) {
</Button>
</div>
) : null}
<main className="py-4">
<div className="px-4">{props.children}</div>
</main>
<main className="p-4">{props.children}</main>
</div>
</div>
</>
);
}
type NavigationItem = NestedNavigationItem & {
children?: NestedNavigationItem[];
};
type NestedNavigationItem = Omit<Route, "children"> & {
href?: string;
current: boolean;
};
const MainNavigation: React.FC<{
nav: NavigationItem[];
onNavitemClick?: () => void;
}> = ({ nav, onNavitemClick }) => {
const STORAGE_KEY = "sidebar-tracing-default-open";
const getDefaultOpen = () => {
const savedState = localStorage.getItem(STORAGE_KEY);
if (savedState !== null) {
try {
return JSON.parse(savedState) as boolean;
} catch (e) {
console.error("Error parsing saved state: ", e);
}
}
return false;
};
const handleDropDownClick = () => {
const savedState = localStorage.getItem(STORAGE_KEY);
const isOpen =
savedState !== null ? (JSON.parse(savedState) as boolean) : false;
const newState = !isOpen;
localStorage.setItem(
"sidebar-tracing-default-open",
JSON.stringify(newState),
);
};
return (
<li>
<ul role="list" className="-mx-2 space-y-1">
{nav.map((item) => (
<li key={item.name}>
{(!item.children || item.children.length === 0) && item.href ? (
<Link
href={item.href}
className={clsx(
item.current
? "bg-gray-50 text-indigo-600"
: "text-gray-700 hover:bg-gray-50 hover:text-indigo-600",
"group flex gap-x-3 rounded-md p-2 text-sm font-semibold leading-6",
)}
onClick={onNavitemClick}
>
{item.icon && (
<item.icon
className={clsx(
item.current
? "text-indigo-600"
: "text-gray-400 group-hover:text-indigo-600",
"h-6 w-6 shrink-0",
)}
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>
) : item.children && item.children.length > 0 ? (
<Disclosure
as="div"
defaultOpen={
item.children.some((child) => child.current) ||
getDefaultOpen()
}
>
{({ open }) => (
<>
<Disclosure.Button
className="group flex w-full items-center gap-x-3 rounded-md p-2 text-left text-sm font-semibold leading-6 hover:bg-gray-50 hover:text-indigo-600"
onClick={handleDropDownClick}
>
{item.icon && (
<item.icon
className="h-6 w-6 shrink-0 text-gray-400 group-hover:text-indigo-600"
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>
)}
<ChevronRightIcon
className={clsx(
open ? "rotate-90 text-gray-500" : "text-gray-400",
"ml-auto h-5 w-5 shrink-0",
)}
aria-hidden="true"
/>
</Disclosure.Button>
<Disclosure.Panel as="ul" className="mt-1 px-2">
{item.children?.map((subItem) => (
<li key={subItem.name}>
{/* 44px */}
<Link
href={subItem.href ?? "#"}
className={clsx(
subItem.current
? "bg-gray-50 text-indigo-600"
: "text-gray-700 hover:bg-gray-50 hover:text-indigo-600",
"flex w-full items-center gap-x-3 rounded-md py-2 pl-9 pr-2 text-sm leading-6",
)}
>
{subItem.name}
{subItem.label && (
<span className="self-center whitespace-nowrap break-keep rounded-sm border border-gray-200 px-1 py-0.5 text-xs text-gray-400 group-hover:border-indigo-600 group-hover:text-indigo-600">
{subItem.label}
</span>
)}
</Link>
</li>
))}
</Disclosure.Panel>
</>
)}
</Disclosure>
) : null}
</li>
))}
<FeedbackButtonWrapper className="w-full">
<li className="group flex cursor-pointer gap-x-3 rounded-md p-2 text-sm font-semibold leading-6 text-gray-700 hover:bg-gray-50 hover:text-indigo-600">
<MessageSquarePlus
className="h-6 w-6 shrink-0 text-gray-400 group-hover:text-indigo-600"
aria-hidden="true"
/>
Feedback
</li>
</FeedbackButtonWrapper>
</ul>
</li>
);
};
+26 -24
View File
@@ -4,48 +4,50 @@ import {
Database,
LayoutDashboard,
LifeBuoy,
LineChart,
ListTree,
type LucideIcon,
Settings,
TextSelect,
UsersIcon,
Route,
PenSquareIcon,
} from "lucide-react";
export const ROUTES: Array<{
export type Route = {
name: string;
pathname: string;
icon: LucideIcon;
featureFlag?: Flag;
label?: string;
rbacScope?: Scope;
}> = [
icon?: LucideIcon; // ignored for nested routes
pathname?: string; // link, ignored if children
children?: Array<Route>; // folder
};
export const ROUTES: Route[] = [
{
name: "Dashboard",
pathname: `/project/[projectId]`,
icon: LayoutDashboard,
},
{
name: "Sessions",
pathname: `/project/[projectId]/sessions`,
icon: Route,
},
{
name: "Traces",
pathname: `/project/[projectId]/traces`,
name: "Tracing",
icon: ListTree,
},
{
name: "Generations",
pathname: `/project/[projectId]/generations`,
icon: TextSelect,
},
{
name: "Scores",
pathname: `/project/[projectId]/scores`,
icon: LineChart,
children: [
{
name: "Traces",
pathname: `/project/[projectId]/traces`,
},
{
name: "Sessions",
pathname: `/project/[projectId]/sessions`,
},
{
name: "Generations",
pathname: `/project/[projectId]/generations`,
},
{
name: "Scores",
pathname: `/project/[projectId]/scores`,
},
],
},
{
name: "Users",
+11 -4
View File
@@ -68,15 +68,22 @@ export const SessionPage: React.FC<{
/>,
<DetailPageNav
key="nav"
currentId={sessionId}
path={(id) => `/project/${projectId}/sessions/${id}`}
currentId={encodeURIComponent(sessionId)}
path={(id) =>
`/project/${projectId}/sessions/${encodeURIComponent(id)}`
}
listKey="sessions"
/>,
]}
/>
<div className="flex flex-wrap gap-2">
{session.data?.users.map((userId) => (
<Link key={userId} href={`/project/${projectId}/users/${userId}`}>
{session.data?.users.filter(Boolean).map((userId) => (
<Link
key={userId}
href={`/project/${projectId}/users/${encodeURIComponent(
userId ?? "",
)}`}
>
<Badge>User ID: {userId}</Badge>
</Link>
))}
+128 -22
View File
@@ -1,50 +1,37 @@
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";
import { type RouterOutput, type RouterInput } from "@/src/utils/types";
import { useState } from "react";
export function StarToggle({
value,
disabled = false,
onClick,
size = "sm",
isLoading,
}: {
value: boolean;
disabled?: boolean;
onClick: (value: boolean) => Promise<unknown>;
size?: "sm" | "xs";
isLoading: boolean;
}) {
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()}
onClick={() => void onClick(!value)}
disabled={disabled}
loading={loading}
loading={isLoading}
>
<StarIcon
className={cn(
"h-4 w-4",
optimisticValue ? "fill-current text-yellow-500" : "text-gray-500",
value ? "fill-current text-yellow-500" : "text-gray-500",
)}
/>
</Button>
@@ -52,6 +39,88 @@ export function StarToggle({
}
export function StarTraceToggle({
tracesFilter,
projectId,
traceId,
value,
size = "sm",
}: {
tracesFilter: RouterInput["traces"]["all"];
projectId: string;
traceId: string;
value: boolean;
size?: "sm" | "xs";
}) {
const utils = api.useUtils();
const hasAccess = useHasAccess({ projectId, scope: "objects:bookmark" });
const [isLoading, setIsLoading] = useState(false);
const mutBookmarkTrace = api.traces.bookmark.useMutation({
// Optimistic update
// Tanstack docs: https://tanstack.com/query/v4/docs/react/guides/optimistic-updates
onMutate: async () => {
// Cancel any outgoing refetches
// (so they don't overwrite our optimistic update)
await utils.traces.all.cancel();
setIsLoading(true);
// Snapshot the previous value
const prev = utils.traces.all.getData(tracesFilter);
return { prev };
},
onError: (err, _newTodo, context) => {
setIsLoading(false);
// Rollback to the previous value if mutation fails
console.log("error", err);
utils.traces.all.setData(tracesFilter, context?.prev);
},
onSettled: () => {
setIsLoading(false);
utils.traces.all.setData(
tracesFilter,
(oldQueryData: RouterOutput["traces"]["all"] | undefined) => {
return {
totalCount: oldQueryData?.totalCount,
traces: oldQueryData?.traces
? oldQueryData.traces.map((trace) => {
return {
...trace,
bookmarked:
trace.id === traceId
? !trace.bookmarked
: trace.bookmarked,
};
})
: [],
};
},
);
void utils.traces.all.invalidate();
},
});
return (
<StarToggle
value={value}
size={size}
disabled={!hasAccess}
isLoading={isLoading}
onClick={(value) =>
mutBookmarkTrace.mutateAsync({
projectId,
traceId,
bookmarked: value,
})
}
/>
);
}
export function StarTraceDetailsToggle({
projectId,
traceId,
value,
@@ -64,9 +133,44 @@ export function StarTraceToggle({
}) {
const utils = api.useUtils();
const hasAccess = useHasAccess({ projectId, scope: "objects:bookmark" });
const [isLoading, setIsLoading] = useState(false);
const mutBookmarkTrace = api.traces.bookmark.useMutation({
onSuccess: () => {
void utils.traces.invalidate();
onMutate: async () => {
// Cancel any outgoing refetches
// (so they don't overwrite our optimistic update)
await utils.traces.byId.cancel();
setIsLoading(true);
// Snapshot the previous value
const prevData = utils.traces.byId.getData({ traceId });
return { prevData };
},
onError: (err, _newTodo, context) => {
setIsLoading(false);
console.log("error", err);
// Rollback to the previous value if mutation fails
utils.traces.byId.setData({ traceId }, context?.prevData);
},
onSettled: () => {
setIsLoading(false);
utils.traces.byId.setData(
{ traceId },
(oldQueryData: RouterOutput["traces"]["byId"] | undefined) => {
return oldQueryData
? {
...oldQueryData,
bookmarked: !oldQueryData.bookmarked,
}
: undefined;
},
);
void utils.traces.byId.invalidate();
void utils.traces.all.invalidate();
},
});
@@ -75,6 +179,7 @@ export function StarTraceToggle({
value={value}
size={size}
disabled={!hasAccess}
isLoading={isLoading}
onClick={(value) =>
mutBookmarkTrace.mutateAsync({
projectId,
@@ -109,6 +214,7 @@ export function StarSessionToggle({
<StarToggle
value={value}
size={size}
isLoading={mutBookmarkSession.isLoading}
disabled={!hasAccess}
onClick={(value) =>
mutBookmarkSession.mutateAsync({
+8 -4
View File
@@ -122,10 +122,14 @@ export function DataTable<TData extends object, TValue>({
}
if (orderBy?.column === header.column.columnDef.id) {
setOrderBy({
column: header.column.columnDef.id,
order: orderBy.order === "ASC" ? "DESC" : "ASC",
});
if (orderBy.order === "DESC") {
setOrderBy({
column: header.column.columnDef.id,
order: "ASC",
});
} else {
setOrderBy(null);
}
} else {
setOrderBy({
column: header.column.columnDef.id,
+31 -3
View File
@@ -34,6 +34,7 @@ import {
exportOptions,
type ExportFileFormats,
} from "@/src/server/api/interfaces/exportTypes";
import { useOrderByState } from "@/src/features/orderBy/hooks/useOrderByState";
export type GenerationsTableRow = {
id: string;
@@ -82,14 +83,21 @@ export default function GenerationsTable({ projectId }: GenerationsTableProps) {
},
]);
const [orderByState, setOrderByState] = useOrderByState({
column: "startTime",
order: "DESC",
});
const generations = api.generations.all.useQuery({
page: paginationState.pageIndex,
limit: paginationState.pageSize,
projectId,
filter: filterState,
orderBy: orderByState,
searchQuery,
});
const totalCount = generations.data?.slice(1)[0]?.totalCount ?? 0;
const totalCount = generations.data?.totalCount ?? 0;
const filterOptions = api.generations.filterOptions.useQuery({
projectId,
@@ -143,6 +151,7 @@ export default function GenerationsTable({ projectId }: GenerationsTableProps) {
const columns: LangfuseColumnDef<GenerationsTableRow>[] = [
{
accessorKey: "id",
id: "id",
header: "ID",
cell: ({ row }) => {
const observationId = row.getValue("id");
@@ -155,13 +164,17 @@ export default function GenerationsTable({ projectId }: GenerationsTableProps) {
/>
) : null;
},
enableSorting: true,
},
{
accessorKey: "name",
id: "name",
header: "name",
enableSorting: true,
},
{
accessorKey: "traceId",
id: "traceId",
header: "Trace ID",
cell: ({ row }) => {
const value = row.getValue("traceId");
@@ -172,19 +185,25 @@ export default function GenerationsTable({ projectId }: GenerationsTableProps) {
/>
) : undefined;
},
enableSorting: true,
},
{
accessorKey: "traceName",
id: "traceName",
header: "Trace Name",
enableHiding: true,
enableSorting: true,
},
{
accessorKey: "startTime",
id: "startTime",
header: "Start Time",
enableHiding: true,
enableSorting: true,
},
{
accessorKey: "latency",
id: "latency",
header: "Latency",
cell: ({ row }) => {
const value: number | undefined = row.getValue("latency");
@@ -193,6 +212,7 @@ export default function GenerationsTable({ projectId }: GenerationsTableProps) {
) : undefined;
},
enableHiding: true,
enableSorting: true,
},
{
accessorKey: "cost",
@@ -208,6 +228,7 @@ export default function GenerationsTable({ projectId }: GenerationsTableProps) {
},
{
accessorKey: "level",
id: "level",
header: "Level",
enableHiding: true,
cell({ row }) {
@@ -224,6 +245,7 @@ export default function GenerationsTable({ projectId }: GenerationsTableProps) {
</span>
) : undefined;
},
enableSorting: true,
},
{
accessorKey: "statusMessage",
@@ -233,8 +255,10 @@ export default function GenerationsTable({ projectId }: GenerationsTableProps) {
},
{
accessorKey: "model",
id: "model",
header: "Model",
enableHiding: true,
enableSorting: true,
},
{
accessorKey: "usage",
@@ -288,8 +312,10 @@ export default function GenerationsTable({ projectId }: GenerationsTableProps) {
},
{
accessorKey: "version",
id: "version",
header: "Version",
enableHiding: true,
enableSorting: true,
},
];
const [columnVisibility, setColumnVisibility] =
@@ -299,7 +325,7 @@ export default function GenerationsTable({ projectId }: GenerationsTableProps) {
);
const rows: GenerationsTableRow[] = generations.isSuccess
? generations.data.map((generation) => {
? generations.data.generations.map((generation) => {
return {
id: generation.id,
traceId: generation.traceId,
@@ -380,7 +406,7 @@ export default function GenerationsTable({ projectId }: GenerationsTableProps) {
data={
generations.isLoading
? { isLoading: true, isError: false }
: generations.isError
: generations.error
? {
isLoading: false,
isError: true,
@@ -397,6 +423,8 @@ export default function GenerationsTable({ projectId }: GenerationsTableProps) {
onChange: setPaginationState,
state: paginationState,
}}
setOrderBy={setOrderByState}
orderBy={orderByState}
columnVisibility={columnVisibility}
onColumnVisibilityChange={setColumnVisibility}
/>
+22 -2
View File
@@ -7,6 +7,7 @@ import useColumnVisibility from "@/src/features/column-visibility/hooks/useColum
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 { sessionsViewCols } from "@/src/server/api/definitions/sessionsView";
import { api } from "@/src/utils/api";
import { formatInterval, utcDateOffsetByDays } from "@/src/utils/dates";
@@ -63,12 +64,19 @@ export default function SessionsTable({
pageSize: withDefault(NumberParam, 50),
});
const [orderByState, setOrderByState] = useOrderByState({
column: "createdAt",
order: "DESC",
});
const sessions = api.sessions.all.useQuery({
page: paginationState.pageIndex,
limit: paginationState.pageSize,
projectId,
filter: filterState,
orderBy: orderByState,
});
const totalCount = sessions.data?.slice(1)[0]?.totalCount ?? 0;
useEffect(() => {
if (sessions.isSuccess) {
@@ -96,6 +104,7 @@ export default function SessionsTable({
const columns: LangfuseColumnDef<SessionTableRow>[] = [
{
accessorKey: "bookmarked",
id: "bookmarked",
header: undefined,
cell: ({ row }) => {
const bookmarked = row.getValue("bookmarked");
@@ -111,27 +120,33 @@ export default function SessionsTable({
/>
) : undefined;
},
enableSorting: true,
},
{
accessorKey: "id",
id: "id",
header: "ID",
cell: ({ row }) => {
const value = row.getValue("id");
return value && typeof value === "string" ? (
<TableLink
path={`/project/${projectId}/sessions/${value}`}
path={`/project/${projectId}/sessions/${encodeURIComponent(value)}`}
value={value}
/>
) : undefined;
},
enableSorting: true,
},
{
accessorKey: "createdAt",
id: "createdAt",
header: "Created At",
enableHiding: true,
enableSorting: true,
},
{
accessorKey: "sessionDuration",
id: "sessionDuration",
header: "Duration",
enableHiding: true,
cell: ({ row }) => {
@@ -140,6 +155,7 @@ export default function SessionsTable({
? formatInterval(value)
: undefined;
},
enableSorting: true,
},
{
accessorKey: "userIds",
@@ -153,7 +169,7 @@ export default function SessionsTable({
{(value as string[]).map((user) => (
<TableLink
key={user}
path={`/project/${projectId}/users/${user}`}
path={`/project/${projectId}/users/${encodeURIComponent(user)}`}
value={user}
truncateAt={40}
/>
@@ -164,8 +180,10 @@ export default function SessionsTable({
},
{
accessorKey: "countTraces",
id: "countTraces",
header: "Traces",
enableHiding: true,
enableSorting: true,
},
];
@@ -204,6 +222,8 @@ export default function SessionsTable({
onChange: setPaginationState,
state: paginationState,
}}
setOrderBy={setOrderByState}
orderBy={orderByState}
columnVisibility={columnVisibility}
onColumnVisibilityChange={setColumnVisibility}
help={{
+30 -13
View File
@@ -6,6 +6,7 @@ import { TraceTableMultiSelectAction } from "@/src/components/table/data-table-m
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 { TagTracePopver } from "@/src/features/tag/components/TagTracePopver";
import { TokenUsageBadge } from "@/src/components/token-usage-badge";
import { Checkbox } from "@/src/components/ui/checkbox";
import { JSONView } from "@/src/components/ui/code";
@@ -99,21 +100,22 @@ export default function TracesTable({
pageIndex: withDefault(NumberParam, 0),
pageSize: withDefault(NumberParam, 50),
});
const traces = api.traces.all.useQuery({
const tracesAllQueryFilter = {
page: paginationState.pageIndex,
limit: paginationState.pageSize,
projectId,
filter: filterState,
searchQuery,
orderBy: orderByState,
});
const totalCount = traces.data?.slice(1)[0]?.totalCount ?? 0;
};
const traces = api.traces.all.useQuery(tracesAllQueryFilter);
const totalCount = traces.data?.totalCount ?? 0;
useEffect(() => {
if (traces.isSuccess) {
setDetailPageList(
"traces",
traces.data.map((t) => t.id),
traces.data.traces.map((t) => t.id),
);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
@@ -134,9 +136,8 @@ export default function TracesTable({
},
},
);
const convertToTableRow = (
trace: RouterOutput["traces"]["all"][0],
trace: RouterOutput["traces"]["all"]["traces"][0],
): TracesTableRow => {
return {
bookmarked: trace.bookmarked,
@@ -197,10 +198,10 @@ export default function TracesTable({
cell: ({ row }) => {
const bookmarked = row.getValue("bookmarked");
const traceId = row.getValue("id");
return typeof traceId === "string" &&
typeof bookmarked === "boolean" ? (
<StarTraceToggle
tracesFilter={tracesAllQueryFilter}
traceId={traceId}
projectId={projectId}
value={bookmarked}
@@ -246,7 +247,7 @@ export default function TracesTable({
const value = row.getValue("userId");
return value && typeof value === "string" ? (
<TableLink
path={`/project/${projectId}/users/${value}`}
path={`/project/${projectId}/users/${encodeURIComponent(value)}`}
value={value}
truncateAt={40}
/>
@@ -263,7 +264,7 @@ export default function TracesTable({
const value = row.getValue("sessionId");
return value && typeof value === "string" ? (
<TableLink
path={`/project/${projectId}/sessions/${value}`}
path={`/project/${projectId}/sessions/${encodeURIComponent(value)}`}
value={value}
truncateAt={40}
/>
@@ -362,6 +363,21 @@ export default function TracesTable({
accessorKey: "tags",
id: "tags",
header: "Tags",
cell: ({ row }) => {
const tags: string[] = row.getValue("tags");
const traceId: string = row.getValue("id");
const filterOptionTags = traceFilterOptions.data?.tags ?? [];
const allTags = filterOptionTags.map((t) => t.value);
return (
<TagTracePopver
tags={tags}
availableTags={allTags}
projectId={projectId}
traceId={traceId}
tracesFilter={tracesAllQueryFilter}
/>
);
},
enableHiding: true,
},
{
@@ -401,7 +417,8 @@ export default function TracesTable({
<TraceTableMultiSelectAction
// Exclude traces that are not in the current page
selectedTraceIds={Object.keys(selectedRows).filter(
(traceId) => traces.data?.map((t) => t.id).includes(traceId),
(traceId) =>
traces.data?.traces.map((t) => t.id).includes(traceId),
)}
projectId={projectId}
onDeleteSuccess={() => {
@@ -426,11 +443,11 @@ export default function TracesTable({
: {
isLoading: false,
isError: false,
data: traces.data.map((t) => convertToTableRow(t)),
data: traces.data.traces.map((t) => convertToTableRow(t)),
}
}
pagination={{
pageCount: Math.ceil(totalCount / paginationState.pageSize),
pageCount: Math.ceil(Number(totalCount) / paginationState.pageSize),
onChange: setPaginationState,
state: paginationState,
}}
+13 -1
View File
@@ -23,6 +23,7 @@ import { api } from "@/src/utils/api";
import { IOPreview } from "@/src/components/trace/IOPreview";
import { formatInterval } from "@/src/utils/dates";
import Link from "next/link";
import { usdFormatter } from "@/src/utils/numbers";
export const ObservationPreview = (props: {
observations: Array<ObservationReturnType>;
@@ -61,8 +62,19 @@ export const ObservationPreview = (props: {
projectId={preloadedObservation.projectId}
/>
) : undefined}
{preloadedObservation.completionStartTime ? (
<Badge variant="outline">
Time to first token:{" "}
{formatInterval(
(preloadedObservation.completionStartTime.getTime() -
preloadedObservation.startTime.getTime()) /
1000,
)}
</Badge>
) : null}
{preloadedObservation.endTime ? (
<Badge variant="outline">
Latency:{" "}
{formatInterval(
(preloadedObservation.endTime.getTime() -
preloadedObservation.startTime.getTime()) /
@@ -87,7 +99,7 @@ export const ObservationPreview = (props: {
) : null}
{preloadedObservation.price ? (
<Badge variant="outline">
{preloadedObservation.price.toString()} USD
{usdFormatter(preloadedObservation.price.toNumber())}
</Badge>
) : undefined}
-3
View File
@@ -73,9 +73,6 @@ export const TracePreview = ({
title="Metadata"
json={trace.metadata}
/>
{trace.tags.length !== 0 && (
<JSONView key={trace.id + "-tags"} title="Tags" json={trace.tags} />
)}
{scores.find((s) => s.observationId === null) ? (
<div className="mt-5 flex flex-col gap-2">
<h3>Scores</h3>
+63 -32
View File
@@ -6,7 +6,6 @@ import { TracePreview } from "./TracePreview";
import Header from "@/src/components/layouts/header";
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/components/publish-object-switch";
import { DetailPageNav } from "@/src/features/navigate-detail-pages/DetailPageNav";
@@ -14,12 +13,16 @@ 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 { StarTraceDetailsToggle } from "@/src/components/star-toggle";
import Link from "next/link";
import { NoAccessError } from "@/src/components/no-access";
import { TagTraceDetailsPopover } from "@/src/features/tag/components/TagTraceDetailsPopover";
import useLocalStorage from "@/src/components/useLocalStorage";
import { Toggle } from "@/src/components/ui/toggle";
import { Award, ChevronsDownUp, ChevronsUpDown } from "lucide-react";
import { ScrollArea } from "@/src/components/ui/scroll-area";
import { usdFormatter } from "@/src/utils/numbers";
import type Decimal from "decimal.js";
export function Trace(props: {
observations: Array<ObservationReturnType>;
@@ -39,8 +42,8 @@ export function Trace(props: {
);
return (
<div className="grid h-full gap-4 md:grid-cols-3">
<div className="md:col-span-2 md:h-full md:overflow-y-auto">
<div className="grid gap-4 md:h-full md:grid-cols-3">
<ScrollArea className="md:col-span-2 md:h-full">
{currentObservationId === undefined ||
currentObservationId === "" ||
currentObservationId === null ? (
@@ -58,9 +61,9 @@ export function Trace(props: {
traceId={props.trace.id}
/>
)}
</div>
<div className="md:h-full md:overflow-hidden">
<div className="mb-2 flex flex-row justify-end gap-2">
</ScrollArea>
<div className="md:flex md:h-full md:flex-col md:overflow-hidden">
<div className="mb-2 flex flex-shrink-0 flex-row justify-end gap-2">
<Toggle
pressed={scoresOnObservationTree}
onPressedChange={(e) => {
@@ -86,16 +89,17 @@ export function Trace(props: {
)}
</Toggle>
</div>
<ObservationTree
observations={props.observations}
trace={props.trace}
scores={props.scores}
currentObservationId={currentObservationId ?? undefined}
setCurrentObservationId={setCurrentObservationId}
showMetrics={metricsOnObservationTree}
showScores={scoresOnObservationTree}
className="md:h-full md:overflow-y-auto"
/>
<ScrollArea className="flex flex-grow">
<ObservationTree
observations={props.observations}
trace={props.trace}
scores={props.scores}
currentObservationId={currentObservationId ?? undefined}
setCurrentObservationId={setCurrentObservationId}
showMetrics={metricsOnObservationTree}
showScores={scoresOnObservationTree}
/>
</ScrollArea>
</div>
</div>
);
@@ -112,20 +116,36 @@ export function TracePage({ traceId }: { traceId: string }) {
},
},
);
const totalCost = trace.data?.observations.reduce(
(acc, o) => {
if (!o.price) return acc;
return acc ? acc.plus(o.price) : new Decimal(0).plus(o.price);
const traceFilterOptions = api.traces.filterOptions.useQuery(
{
projectId: trace.data?.projectId ?? "",
},
{
trpc: {
context: {
skipBatch: true,
},
},
enabled: !!trace.data?.projectId && trace.isSuccess,
},
undefined as Decimal | undefined,
);
const filterOptionTags = traceFilterOptions.data?.tags ?? [];
const allTags = filterOptionTags.map((t) => t.value);
const totalCost: Decimal | undefined = trace.data?.observations.reduce(
(prev: Decimal | undefined, curr: ObservationReturnType) => {
if (!curr.price) return prev;
return prev ? prev.plus(curr.price) : curr.price;
},
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 md:h-[calc(100vh-100px)] xl:h-[calc(100vh-40px)]">
<div className="flex flex-col overflow-hidden xl:container md:h-[calc(100vh-2rem)]">
<Header
title="Trace Detail"
breadcrumb={[
@@ -137,7 +157,7 @@ export function TracePage({ traceId }: { traceId: string }) {
]}
actionButtons={
<>
<StarTraceToggle
<StarTraceDetailsToggle
traceId={trace.data.id}
projectId={trace.data.projectId}
value={trace.data.bookmarked}
@@ -164,18 +184,18 @@ export function TracePage({ traceId }: { traceId: string }) {
<div className="flex flex-wrap gap-2">
{trace.data.sessionId ? (
<Link
href={`/project/${router.query.projectId as string}/sessions/${
trace.data.sessionId
}`}
href={`/project/${
router.query.projectId as string
}/sessions/${encodeURIComponent(trace.data.sessionId)}`}
>
<Badge>Session: {trace.data.sessionId}</Badge>
</Link>
) : null}
{trace.data.userId ? (
<Link
href={`/project/${router.query.projectId as string}/users/${
trace.data.userId
}`}
href={`/project/${
router.query.projectId as string
}/users/${encodeURIComponent(trace.data.userId)}`}
>
<Badge>User ID: {trace.data.userId}</Badge>
</Link>
@@ -183,10 +203,21 @@ export function TracePage({ traceId }: { traceId: string }) {
<TraceAggUsageBadge observations={trace.data.observations} />
{totalCost ? (
<Badge variant="outline">
Total cost: {totalCost.toString()} USD
Total cost: {usdFormatter(totalCost.toNumber())}
</Badge>
) : undefined}
</div>
<div className="mt-5 rounded-lg border bg-card font-semibold text-card-foreground shadow-sm">
<div className="flex flex-row items-center gap-3 p-2.5">
Tags
<TagTraceDetailsPopover
tags={trace.data.tags}
availableTags={allTags}
traceId={trace.data.id}
projectId={trace.data.projectId}
/>
</div>
</div>
<div className="mt-5 flex-1 overflow-hidden border-t pt-5">
<Trace
key={trace.data.id}
+48
View File
@@ -0,0 +1,48 @@
"use client";
import * as React from "react";
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area";
import { cn } from "@/src/utils/tailwind";
const ScrollArea = React.forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
>(({ className, children, ...props }, ref) => (
<ScrollAreaPrimitive.Root
ref={ref}
className={cn("relative overflow-hidden", className)}
{...props}
>
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">
{children}
</ScrollAreaPrimitive.Viewport>
<ScrollBar />
<ScrollAreaPrimitive.Corner />
</ScrollAreaPrimitive.Root>
));
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName;
const ScrollBar = React.forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
>(({ className, orientation = "vertical", ...props }, ref) => (
<ScrollAreaPrimitive.ScrollAreaScrollbar
ref={ref}
orientation={orientation}
className={cn(
"flex touch-none select-none transition-colors",
orientation === "vertical" &&
"h-full w-2.5 border-l border-l-transparent p-[1px]",
orientation === "horizontal" &&
"h-2.5 flex-col border-t border-t-transparent p-[1px]",
className,
)}
{...props}
>
<ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border" />
</ScrollAreaPrimitive.ScrollAreaScrollbar>
));
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName;
export { ScrollArea, ScrollBar };
+1 -1
View File
@@ -1 +1 @@
export const VERSION = "v1.30.0";
export const VERSION = "v1.34.1";
@@ -1,11 +1,11 @@
import { api } from "@/src/utils/api";
import { type FilterState } from "@/src/features/filters/types";
import { TotalMetric } from "./TotalMetric";
import { compactNumberFormatter, usdFormatter } from "@/src/utils/numbers";
import { DashboardTable } from "@/src/features/dashboard/components/cards/DashboardTable";
import DocPopup from "@/src/components/layouts/doc-popup";
import { RightAlignedCell } from "@/src/features/dashboard/components/RightAlignedCell";
import { DashboardCard } from "@/src/features/dashboard/components/cards/DashboardCard";
import DocPopup from "@/src/components/layouts/doc-popup";
import { DashboardTable } from "@/src/features/dashboard/components/cards/DashboardTable";
import { type FilterState } from "@/src/features/filters/types";
import { api } from "@/src/utils/api";
import { compactNumberFormatter, usdFormatter } from "@/src/utils/numbers";
import { TotalMetric } from "./TotalMetric";
export const MetricTable = ({
className,
@@ -54,14 +54,14 @@ export const MetricTable = ({
.filter((item) => item.model !== null)
.map((item, i) => [
item.model as string,
<RightAlignedCell key={i}>
<RightAlignedCell key={`${i}-tokens`}>
{item.sumTotalTokens
? compactNumberFormatter(item.sumTotalTokens as number)
: "0"}
</RightAlignedCell>,
<RightAlignedCell key={i}>
<RightAlignedCell key={`${i}-cost`}>
{item.totalTokenCost
? usdFormatter(item.totalTokenCost as number)
? usdFormatter(item.totalTokenCost as number, 2, 2)
: "$0"}
</RightAlignedCell>,
])
@@ -76,14 +76,14 @@ export const MetricTable = ({
<DashboardTable
headers={[
"Model",
<RightAlignedCell key={0}>Tokens</RightAlignedCell>,
<RightAlignedCell key={0}>USD</RightAlignedCell>,
<RightAlignedCell key="tokens">Tokens</RightAlignedCell>,
<RightAlignedCell key="cost">USD</RightAlignedCell>,
]}
rows={metricsData}
collapse={{ collapsed: 5, expanded: 20 }}
>
<TotalMetric
metric={totalTokens ? usdFormatter(totalTokens) : "$0"}
metric={totalTokens ? usdFormatter(totalTokens, 2, 2) : "$0"}
description="Total cost"
>
<DocPopup
@@ -94,13 +94,19 @@ export const ModelUsageChart = ({
0,
);
// had to add this function as tremor under the hodd adds more variables
// to the function call which would break usdFormatter.
const oneValueUsdFormatter = (value: number) => {
return usdFormatter(value, 2, 2);
};
const data = [
{
tabTitle: "Total cost",
data: transformedModelCost,
totalMetric: totalCost ? usdFormatter(totalCost) : usdFormatter(0),
totalMetric: totalCost ? usdFormatter(totalCost, 2, 2) : usdFormatter(0),
metricDescription: `Token cost`,
formatter: usdFormatter,
formatter: oneValueUsdFormatter,
},
{
tabTitle: "Total tokens",
@@ -115,7 +121,7 @@ export const ModelUsageChart = ({
return (
<DashboardCard
className={className}
title={"Model Usage"}
title="Model Usage"
isLoading={tokens.isLoading}
>
<TabComponent
@@ -1,18 +1,10 @@
import { type Key, type ReactNode } from "react";
import { cn } from "@/src/utils/tailwind";
import { type ReactNode } from "react";
export const RightAlignedCell = ({
children,
className,
key,
}: {
children: ReactNode;
className?: string;
key?: Key;
}) => {
return (
<div className={cn("text-right", className)} key={key}>
{children}
</div>
);
};
}) => <div className={cn("text-right", className)}>{children}</div>;
@@ -1,12 +1,12 @@
import { api } from "@/src/utils/api";
import { type FilterState } from "@/src/features/filters/types";
import { TotalMetric } from "./TotalMetric";
import { compactNumberFormatter } from "@/src/utils/numbers";
import { DashboardTable } from "@/src/features/dashboard/components/cards/DashboardTable";
import { DashboardCard } from "@/src/features/dashboard/components/cards/DashboardCard";
import { NoData } from "@/src/features/dashboard/components/NoData";
import { RightAlignedCell } from "./RightAlignedCell";
import DocPopup from "@/src/components/layouts/doc-popup";
import { NoData } from "@/src/features/dashboard/components/NoData";
import { DashboardCard } from "@/src/features/dashboard/components/cards/DashboardCard";
import { DashboardTable } from "@/src/features/dashboard/components/cards/DashboardTable";
import { type FilterState } from "@/src/features/filters/types";
import { api } from "@/src/utils/api";
import { compactNumberFormatter } from "@/src/utils/numbers";
import { RightAlignedCell } from "./RightAlignedCell";
import { TotalMetric } from "./TotalMetric";
export const ScoresTable = ({
className,
@@ -128,23 +128,23 @@ export const ScoresTable = ({
<DashboardTable
headers={[
"Name",
<RightAlignedCell key={0}>#</RightAlignedCell>,
<RightAlignedCell key={0}>Avg</RightAlignedCell>,
<RightAlignedCell key={0}>0</RightAlignedCell>,
<RightAlignedCell key={0}>1</RightAlignedCell>,
<RightAlignedCell key="count">#</RightAlignedCell>,
<RightAlignedCell key="average">Avg</RightAlignedCell>,
<RightAlignedCell key="zero">0</RightAlignedCell>,
<RightAlignedCell key="one">1</RightAlignedCell>,
]}
rows={data.map((item, i) => [
item.scoreName,
<RightAlignedCell key={i}>
<RightAlignedCell key={`${i}-count`}>
{compactNumberFormatter(item.countScoreId as number)}
</RightAlignedCell>,
<RightAlignedCell key={i}>
<RightAlignedCell key={`${i}-average`}>
{compactNumberFormatter(item.avgValue)}
</RightAlignedCell>,
<RightAlignedCell key={i}>
<RightAlignedCell key={`${i}-zero`}>
{compactNumberFormatter(item.zeroValueScore as number)}
</RightAlignedCell>,
<RightAlignedCell key={i}>
<RightAlignedCell key={`${i}-one`}>
{compactNumberFormatter(item.oneValueScore)}
</RightAlignedCell>,
])}
@@ -114,15 +114,17 @@ export const UserChart = ({
const maxNumberOfEntries = { collapsed: 5, expanded: 20 } as const;
const localUsdFormatter = (value: number) => usdFormatter(value, 2, 2);
const data = [
{
tabTitle: "Token cost",
data: isExpanded
? transformedCost.slice(0, maxNumberOfEntries.expanded)
: transformedCost.slice(0, maxNumberOfEntries.collapsed),
totalMetric: totalCost ? usdFormatter(totalCost) : usdFormatter(0),
totalMetric: totalCost ? usdFormatter(totalCost, 2, 2) : usdFormatter(0),
metricDescription: "Total cost",
formatter: usdFormatter,
formatter: localUsdFormatter,
},
{
tabTitle: "Count of Traces",
@@ -1,6 +1,6 @@
import { ExpandListButton } from "@/src/features/dashboard/components/cards/ChevronButton";
import { NoData } from "../NoData";
import { useState, type ReactNode } from "react";
import { NoData } from "../NoData";
type TableHeaders = ReactNode[];
type TableRows = ReactNode[][];
@@ -57,11 +57,11 @@ export const DashboardTable = ({
: collapse.collapsed
: undefined,
)
.map((row) => (
<tr key={"1"}>
{row.map((cell, i) => (
.map((row, i) => (
<tr key={i}>
{row.map((cell, j) => (
<td
key={i}
key={j}
className="whitespace-nowrap py-2 pl-3 pr-2 text-xs text-gray-500 sm:pl-0"
>
{cell}
@@ -15,6 +15,8 @@ import { Button } from "@/src/components/ui/button";
import { DatasetStatus, type DatasetItem } from "@prisma/client";
import { cn } from "@/src/utils/tailwind";
import { type LangfuseColumnDef } from "@/src/components/table/types";
import { useDetailPageLists } from "@/src/features/navigate-detail-pages/context";
import { useEffect } from "react";
type RowData = {
id: string;
@@ -31,12 +33,23 @@ export function DatasetItemsTable({
projectId: string;
datasetId: string;
}) {
const { setDetailPageList } = useDetailPageLists();
const utils = api.useUtils();
const items = api.datasets.itemsByDatasetId.useQuery({
projectId,
datasetId,
});
useEffect(() => {
if (items.isSuccess) {
setDetailPageList(
"datasetItems",
items.data.map((t) => t.id),
);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [items.isSuccess, items.data]);
const mutUpdate = api.datasets.updateDatasetItem.useMutation({
onSuccess: () => utils.datasets.invalidate(),
});
@@ -2,9 +2,11 @@ import { GroupedScoreBadges } from "@/src/components/grouped-score-badge";
import { DataTable } from "@/src/components/table/data-table";
import TableLink from "@/src/components/table/table-link";
import { type LangfuseColumnDef } from "@/src/components/table/types";
import { useDetailPageLists } from "@/src/features/navigate-detail-pages/context";
import { api } from "@/src/utils/api";
import { formatInterval } from "@/src/utils/dates";
import { type RouterOutput } from "@/src/utils/types";
import { useEffect } from "react";
type RowData = {
key: {
@@ -25,7 +27,16 @@ export function DatasetRunsTable(props: {
projectId: props.projectId,
datasetId: props.datasetId,
});
const { setDetailPageList } = useDetailPageLists();
useEffect(() => {
if (runs.isSuccess) {
setDetailPageList(
"datasetRuns",
runs.data.map((t) => t.id),
);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [runs.isSuccess, runs.data]);
const columns: LangfuseColumnDef<RowData>[] = [
{
accessorKey: "key",
@@ -10,18 +10,17 @@ import {
DropdownMenuTrigger,
} from "@/src/components/ui/dropdown-menu";
import { NewDatasetButton } from "@/src/features/datasets/components/NewDatasetButton";
import { useDetailPageLists } from "@/src/features/navigate-detail-pages/context";
import { api } from "@/src/utils/api";
import { cn } from "@/src/utils/tailwind";
import { type RouterOutput } from "@/src/utils/types";
import { DatasetStatus } from "@prisma/client";
import { Archive, MoreVertical } from "lucide-react";
import { MoreVertical, Trash } from "lucide-react";
import { useEffect } from "react";
type RowData = {
key: {
id: string;
name: string;
};
status: DatasetStatus;
createdAt: string;
lastRunAt?: string;
countItems: number;
@@ -29,13 +28,23 @@ type RowData = {
};
export function DatasetsTable(props: { projectId: string }) {
const { setDetailPageList } = useDetailPageLists();
const utils = api.useUtils();
const datasets = api.datasets.allDatasets.useQuery({
projectId: props.projectId,
});
const mutArchive = api.datasets.updateDataset.useMutation({
const mutDelete = api.datasets.deleteDataset.useMutation({
onSuccess: () => utils.datasets.invalidate(),
});
useEffect(() => {
if (datasets.isSuccess) {
setDetailPageList(
"datasets",
datasets.data.map((t) => t.id),
);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [datasets.isSuccess, datasets.data]);
const columns: LangfuseColumnDef<RowData>[] = [
{
@@ -52,26 +61,6 @@ export function DatasetsTable(props: { projectId: string }) {
);
},
},
{
accessorKey: "status",
header: "Status",
cell: ({ row }) => {
const status: DatasetStatus = row.getValue("status");
return (
<div className="flex items-center gap-2">
<div
className={cn(
"h-2 w-2 rounded-full",
status === DatasetStatus.ACTIVE
? "bg-green-600"
: "bg-yellow-600",
)}
/>
<span>{status}</span>
</div>
);
},
},
{
accessorKey: "countItems",
header: "Items",
@@ -92,7 +81,6 @@ export function DatasetsTable(props: { projectId: string }) {
id: "actions",
cell: ({ row }) => {
const key: RowData["key"] = row.getValue("key");
const status: DatasetStatus = row.getValue("status");
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
@@ -105,18 +93,14 @@ export function DatasetsTable(props: { projectId: string }) {
<DropdownMenuLabel>Actions</DropdownMenuLabel>
<DropdownMenuItem
onClick={() =>
mutArchive.mutate({
mutDelete.mutate({
projectId: props.projectId,
datasetId: key.id,
status:
status === DatasetStatus.ARCHIVED
? DatasetStatus.ACTIVE
: DatasetStatus.ARCHIVED,
})
}
>
<Archive className="mr-2 h-4 w-4" />
{status === DatasetStatus.ARCHIVED ? "Unarchive" : "Archive"}
<Trash className="mr-2 h-4 w-4" />
Delete permanently
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
@@ -132,7 +116,6 @@ export function DatasetsTable(props: { projectId: string }) {
key: { id: item.id, name: item.name },
createdAt: item.createdAt.toISOString(),
lastRunAt: item.lastRunAt?.toISOString() ?? "",
status: item.status,
countItems: item.countDatasetItems,
countRuns: item.countDatasetRuns,
};
+5 -17
View File
@@ -29,7 +29,6 @@ export const datasetRouter = createTRPCRouter({
d.name,
d.created_at "createdAt",
d.updated_at "updatedAt",
d.status,
count(distinct di.id)::int "countDatasetItems",
count(distinct dr.id)::int "countDatasetRuns",
max(dr.created_at) "lastRunAt"
@@ -37,8 +36,8 @@ export const datasetRouter = createTRPCRouter({
LEFT JOIN dataset_items di ON di.dataset_id = d.id
LEFT JOIN dataset_runs dr ON dr.dataset_id = d.id
WHERE d.project_id = ${input.projectId}
GROUP BY 1,2,3,4,5
ORDER BY d.status ASC, d.created_at DESC
GROUP BY 1,2,3,4
ORDER BY d.created_at DESC
`);
}),
byId: protectedProjectProcedure
@@ -243,30 +242,19 @@ export const datasetRouter = createTRPCRouter({
},
});
}),
updateDataset: protectedProjectProcedure
.input(
z.object({
projectId: z.string(),
datasetId: z.string(),
status: z.enum(["ACTIVE", "ARCHIVED"]).optional(),
name: z.string().optional(),
}),
)
deleteDataset: protectedProjectProcedure
.input(z.object({ projectId: z.string(), datasetId: z.string() }))
.mutation(async ({ input, ctx }) => {
throwIfNoAccess({
session: ctx.session,
projectId: input.projectId,
scope: "datasets:CUD",
});
return ctx.prisma.dataset.update({
return ctx.prisma.dataset.delete({
where: {
id: input.datasetId,
projectId: input.projectId,
},
data: {
status: input.status,
name: input.name,
},
});
}),
createDatasetItem: protectedProjectProcedure
+34 -16
View File
@@ -24,29 +24,30 @@ export function tokenCount(p: {
model: string;
text: unknown;
}): number | undefined {
const model = cleanModelString(p.model);
if (
p.text === null ||
p.text === undefined ||
(Array.isArray(p.text) && p.text.length === 0)
) {
return undefined;
} else if (isOpenAiModel(p.model)) {
} else if (isOpenAiModel(model)) {
return isChatMessageArray(p.text)
? openAiChatTokenCount({
model: p.model,
model: model,
messages: p.text,
})
: isString(p.text)
? openAiStringTokenCount({ model: p.model, text: p.text })
? openAiStringTokenCount({ model: model, text: p.text })
: openAiStringTokenCount({
model: p.model,
model: model,
text: JSON.stringify(p.text),
});
} else if (isClaudeModel(p.model)) {
} else if (isClaudeModel(model)) {
return isString(p.text)
? claudeStringTokenCount({ model: p.model, text: p.text })
? claudeStringTokenCount({ model: model, text: p.text })
: claudeStringTokenCount({
model: p.model,
model: model,
text: JSON.stringify(p.text),
});
} else {
@@ -56,13 +57,6 @@ export function tokenCount(p: {
}
function openAiChatTokenCount(params: TokenCalculationParams) {
let encoding: Tiktoken;
try {
encoding = encoding_for_model(params.model);
} catch (KeyError) {
console.log("Warning: model not found. Using cl100k_base encoding.");
encoding = get_encoding("cl100k_base");
}
let tokens_per_message = 0;
let tokens_per_name = 0;
@@ -99,7 +93,7 @@ function openAiChatTokenCount(params: TokenCalculationParams) {
Object.keys(message).forEach((key) => {
const value = message[key as keyof typeof message];
if (value) {
num_tokens += encoding.encode(value).length;
num_tokens += getTokensByModel(params.model, value);
}
if (key === "name") {
num_tokens += tokens_per_name;
@@ -107,6 +101,7 @@ function openAiChatTokenCount(params: TokenCalculationParams) {
});
});
num_tokens += 3; // every reply is primed with <| start |> assistant <| message |>
return num_tokens;
}
@@ -131,10 +126,29 @@ const claudeStringTokenCount = (p: { model: string; text: string }) => {
const getTokens = (name: TiktokenEncoding, text: string) => {
const encoding = get_encoding(name);
const tokens = encoding.encode(text);
// https://github.com/dqbd/tiktoken/issues/72
// we need to ensure to deallocate memory from the encoder
encoding.free();
return tokens.length;
};
const getTokensByModel = (model: TiktokenModel, text: string) => {
let encoding: Tiktoken;
try {
encoding = encoding_for_model(model);
} catch (KeyError) {
console.log("Warning: model not found. Using cl100k_base encoding.");
encoding = get_encoding("cl100k_base");
}
const length = encoding.encode(text).length;
// https://github.com/dqbd/tiktoken/issues/72
// we need to ensure to deallocate memory from the encoder
encoding.free();
return length;
};
function isString(value: unknown): value is string {
return typeof value === "string";
}
@@ -223,7 +237,8 @@ export function calculateTokenCost(
completionTokens: Decimal;
},
): Decimal | undefined {
const pricing = pricingList.filter((p) => p.modelName === input.model);
const model = cleanModelString(input.model);
const pricing = pricingList.filter((p) => p.modelName === model);
if (pricing.length === 0) {
console.log("no pricing found for model", input.model);
@@ -298,3 +313,6 @@ const calculateValue = (
return undefined;
}
};
const cleanModelString = (model: string) =>
model.toLowerCase().replaceAll("gpt-35", "gpt-3.5");
@@ -50,7 +50,7 @@ export const DetailPageNav = (props: {
return () => window.removeEventListener("keydown", handleKeyDown);
}, [previousPageId, nextPageId, router, props]);
if (ids.length > 0)
if (ids.length > 1)
return (
<div>
<Tooltip>
+18
View File
@@ -0,0 +1,18 @@
import { useSession } from "next-auth/react";
import Script from "next/script";
export function PricingPage(props: { className?: string }) {
const session = useSession();
return (
<>
<Script async src="https://js.stripe.com/v3/pricing-table.js" />
<div className={props.className}>
<stripe-pricing-table
client-reference-id={session.data?.user?.id}
pricing-table-id="prctbl_1OWhugAWilt2EAVVKHi8TEw7"
publishable-key="pk_live_51MPW00AWilt2EAVVFWfPTQhgmLA0EeacLSzAs6e3vECCcMBvwcMse81XgXO6k1bdBHbPBdpOmrXE8P1gBrxE7yhH00RPHQ8SyG"
></stripe-pricing-table>
</div>
</>
);
}
@@ -69,7 +69,7 @@ export default function RenameProject(props: { projectId: string }) {
<b>{form.watch().newName}</b>&quot;.
</p>
) : (
<p className="mb-4 text-sm text-gray-700">
<p className="mb-4 text-sm text-gray-700" data-testid="project-name">
Your Project is currently named &quot;<b>{projectName}</b>
&quot;.
</p>
+74 -83
View File
@@ -5,7 +5,6 @@ import {
} from "@/src/features/public-api/lib/apiKeys";
import { type ApiAccessScope } from "@/src/features/public-api/server/types";
import { prisma } from "@/src/server/db";
import { instrumentAsync } from "@/src/utils/instrumentation";
export type AuthHeaderVerificationResult =
| {
@@ -20,93 +19,85 @@ export type AuthHeaderVerificationResult =
export async function verifyAuthHeaderAndReturnScope(
authHeader: string | undefined,
): Promise<AuthHeaderVerificationResult> {
return instrumentAsync(
{ name: "verifyAuthHeaderAndReturnScope" },
async () => {
if (!authHeader) {
console.error("No authorization header");
return {
validKey: false,
error: "No authorization header",
};
if (!authHeader) {
console.error("No authorization header");
return {
validKey: false,
error: "No authorization header",
};
}
try {
// Basic auth, full scope, needs secret key and public key
if (authHeader.startsWith("Basic ")) {
const { username: publicKey, password: secretKey } =
extractBasicAuthCredentials(authHeader);
const salt = env.SALT;
const hashFromProvidedKey = createShaHash(secretKey, salt);
const apiKey = await prisma.apiKey.findUnique({
where: { fastHashedSecretKey: hashFromProvidedKey },
});
let projectId = apiKey?.projectId;
if (!apiKey || !apiKey.fastHashedSecretKey) {
const dbKey = await findDbKeyOrThrow(publicKey);
const isValid = await verifySecretKey(secretKey, dbKey.hashedSecretKey);
if (!isValid) {
console.log("Old key is invalid", publicKey);
throw new Error("Invalid credentials");
}
const shaKey = createShaHash(secretKey, salt);
await prisma.apiKey.update({
where: { publicKey },
data: {
fastHashedSecretKey: shaKey,
},
});
projectId = dbKey.projectId;
}
try {
// Basic auth, full scope, needs secret key and public key
if (authHeader.startsWith("Basic ")) {
const { username: publicKey, password: secretKey } =
extractBasicAuthCredentials(authHeader);
const salt = env.SALT;
const hashFromProvidedKey = createShaHash(secretKey, salt);
const apiKey = await prisma.apiKey.findUnique({
where: { fastHashedSecretKey: hashFromProvidedKey },
});
let projectId = apiKey?.projectId;
if (!apiKey || !apiKey.fastHashedSecretKey) {
const dbKey = await findDbKeyOrThrow(publicKey);
const isValid = await verifySecretKey(
secretKey,
dbKey.hashedSecretKey,
);
if (!isValid) {
console.log("Old key is invalid", publicKey);
throw new Error("Invalid credentials");
}
const shaKey = createShaHash(secretKey, salt);
await prisma.apiKey.update({
where: { publicKey },
data: {
fastHashedSecretKey: shaKey,
},
});
projectId = dbKey.projectId;
}
if (!projectId) {
console.log("No project id found for key", publicKey);
throw new Error("Invalid credentials");
}
return {
validKey: true,
scope: {
projectId: projectId,
accessLevel: "all",
},
};
}
// Bearer auth, limited scope, only needs public key
if (authHeader.startsWith("Bearer ")) {
const publicKey = authHeader.replace("Bearer ", "");
const dbKey = await findDbKeyOrThrow(publicKey);
return {
validKey: true,
scope: {
projectId: dbKey.projectId,
accessLevel: "scores",
},
};
}
} catch (error: unknown) {
console.error("Error verifying auth header: ", error);
return {
validKey: false,
error: error instanceof Error ? error.message : "Authorization error",
};
if (!projectId) {
console.log("No project id found for key", publicKey);
throw new Error("Invalid credentials");
}
return {
validKey: false,
error: "Invalid authorization header",
validKey: true,
scope: {
projectId: projectId,
accessLevel: "all",
},
};
},
);
}
// Bearer auth, limited scope, only needs public key
if (authHeader.startsWith("Bearer ")) {
const publicKey = authHeader.replace("Bearer ", "");
const dbKey = await findDbKeyOrThrow(publicKey);
return {
validKey: true,
scope: {
projectId: dbKey.projectId,
accessLevel: "scores",
},
};
}
} catch (error: unknown) {
console.error("Error verifying auth header: ", error);
return {
validKey: false,
error: error instanceof Error ? error.message : "Authorization error",
};
}
return {
validKey: false,
error: "Invalid authorization header",
};
}
function extractBasicAuthCredentials(basicAuthHeader: string): {
@@ -96,7 +96,9 @@ export const CreateGenerationBody = CreateSpanBody.extend({
modelParameters: z
.record(
z.string(),
z.union([z.string(), z.number(), z.boolean()]).nullish(),
z
.union([z.string(), z.number(), z.boolean(), z.array(z.string())])
.nullish(),
)
.nullish(),
usage: usage,
@@ -116,7 +118,9 @@ export const UpdateGenerationBody = UpdateSpanBody.extend({
modelParameters: z
.record(
z.string(),
z.union([z.string(), z.number(), z.boolean()]).nullish(),
z
.union([z.string(), z.number(), z.boolean(), z.array(z.string())])
.nullish(),
)
.nullish(),
usage: usage,
@@ -11,6 +11,7 @@ const scopes = [
"objects:publish",
"objects:bookmark",
"objects:tag",
"traces:delete",
@@ -39,6 +40,7 @@ export const roleAccessRights: Record<MembershipRole, Scope[]> = {
"apiKeys:delete",
"objects:publish",
"objects:bookmark",
"objects:tag",
"traces:delete",
"scores:CUD",
"project:delete",
@@ -58,6 +60,7 @@ export const roleAccessRights: Record<MembershipRole, Scope[]> = {
"apiKeys:delete",
"objects:publish",
"objects:bookmark",
"objects:tag",
"traces:delete",
"scores:CUD",
"datasets:CUD",
@@ -68,6 +71,7 @@ export const roleAccessRights: Record<MembershipRole, Scope[]> = {
"members:read",
"objects:publish",
"objects:bookmark",
"objects:tag",
"scores:CUD",
"datasets:CUD",
"prompts:CUD",
+16
View File
@@ -0,0 +1,16 @@
import { Button } from "@/src/components/ui/button";
import React from "react";
export const TagButton: React.FC<{ tag: string; loading: boolean }> =
React.memo(({ tag, loading }) => (
<Button
key={tag}
variant="secondary"
size="xs"
className="text-xs font-semibold hover:bg-white"
loading={loading}
>
{tag}
</Button>
));
TagButton.displayName = "TagButton";
@@ -0,0 +1,38 @@
import { CommandItem } from "@/src/components/ui/command";
import { cn } from "@/src/utils/tailwind";
import { Check } from "lucide-react";
import { Button } from "@/src/components/ui/button";
type TagCommandItemProps = {
value: string;
selectedTags: string[];
setSelectedTags: (value: string[]) => void;
};
const TagCommandItem = ({
value,
selectedTags,
setSelectedTags,
}: TagCommandItemProps) => {
return (
<CommandItem
key={value}
onSelect={() => {
setSelectedTags([...selectedTags, value]);
}}
>
<div
className={cn(
"mr-2 flex h-4 w-4 items-center justify-center rounded-sm border border-primary opacity-50 [&_svg]:invisible",
)}
>
<Check className={cn("h-4 w-4")} />
</div>
<Button variant="secondary" size="xs">
{value}
</Button>
</CommandItem>
);
};
export default TagCommandItem;
@@ -0,0 +1,35 @@
import React from "react";
import { CommandItem } from "cmdk";
type TagItemCreateProps = {
inputValue: string;
options: string[];
onSelect: () => void;
};
const TagItemCreate = ({
inputValue,
options,
onSelect,
}: TagItemCreateProps) => {
const hasNoOption = !options
.map((value) => value.toLowerCase())
.includes(inputValue.toLowerCase());
const render = inputValue !== "" && hasNoOption;
if (!render) return null;
return (
<CommandItem
key={inputValue}
value={inputValue}
className="flex cursor-pointer items-center rounded-sm px-3 py-2 text-sm text-muted-foreground hover:bg-secondary/80"
onSelect={onSelect}
>
Create new tag: &quot;{inputValue}&quot;
</CommandItem>
);
};
export default TagItemCreate;
+52
View File
@@ -0,0 +1,52 @@
import React from "react";
import { cn } from "@/src/utils/tailwind";
import { X } from "lucide-react";
import { Button } from "@/src/components/ui/button";
import { Command as CommandPrimitive } from "cmdk";
type TagInputProps = React.ComponentPropsWithoutRef<
typeof CommandPrimitive.Input
> & {
selectedTags: string[];
setSelectedTags: (tags: string[]) => void;
};
export const TagInput = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Input>,
TagInputProps
>(({ className, selectedTags, setSelectedTags, ...props }, ref) => (
<div
className="flex flex-wrap items-center overflow-auto rounded-lg border px-2"
cmdk-input-wrapper=""
>
{selectedTags.length > 0 && (
<div className="flex flex-wrap items-center gap-x-2 gap-y-1 pt-2">
{selectedTags.map((tag: string) => (
<Button
key={tag}
variant="secondary"
size="xs"
onClick={() => {
const newTags = selectedTags.filter((t) => t !== tag);
setSelectedTags(newTags);
}}
>
{tag}
<X className="ml-1 h-3 w-3" />
</Button>
))}
</div>
)}
<CommandPrimitive.Input
ref={ref}
className={cn(
"flex h-11 w-full rounded-md border-transparent bg-transparent px-1 text-sm outline-none placeholder:text-slate-500 focus:border-0 focus:border-none focus:border-transparent focus:ring-0 disabled:cursor-not-allowed disabled:opacity-50 dark:placeholder:text-slate-400 ",
className,
)}
autoFocus
{...props}
/>
</div>
));
TagInput.displayName = CommandPrimitive.Input.displayName;
+25
View File
@@ -0,0 +1,25 @@
import { Button } from "@/src/components/ui/button";
import { TagButton } from "@/src/features/tag/components/TagButton";
type TagListProps = {
selectedTags: string[];
isLoading: boolean;
};
const TagList = ({ selectedTags, isLoading }: TagListProps) => {
return selectedTags.length > 0 ? (
selectedTags.map((tag) => (
<TagButton key={tag} tag={tag} loading={isLoading} />
))
) : (
<Button
variant="outline"
size="xs"
className="text-xs font-bold opacity-0 hover:bg-white hover:opacity-100"
>
Add tag
</Button>
);
};
export default TagList;
@@ -0,0 +1,88 @@
import TagCommandItem from "@/src/features/tag/components/TagCommandItem";
import TagCreateItem from "@/src/features/tag/components/TagCreateItem";
import { TagInput } from "@/src/features/tag/components/TagInput";
import TagList from "@/src/features/tag/components/TagList";
import useTagManager from "@/src/features/tag/hooks/useTagManager";
import {
Popover,
PopoverTrigger,
PopoverContent,
} from "@/src/components/ui/popover";
import { Command, CommandList, CommandGroup } from "cmdk";
type TagManagerProps = {
tags: string[];
allTags: string[];
hasAccess: boolean;
isLoading: boolean;
mutateTags: (value: string[]) => void;
};
const TagManager = ({
tags,
allTags,
hasAccess,
isLoading,
mutateTags,
}: TagManagerProps) => {
const {
selectedTags,
inputValue,
availableTags,
handleItemCreate,
setInputValue,
setSelectedTags,
} = useTagManager({ initialTags: tags, allTags });
const handlePopoverChange = (open: boolean) => {
if (!open && selectedTags !== tags) {
setInputValue("");
mutateTags(selectedTags);
}
};
if (!hasAccess) {
return <TagList selectedTags={selectedTags} isLoading={isLoading} />;
}
return (
<Popover onOpenChange={(open) => handlePopoverChange(open)}>
<PopoverTrigger className="select-none" asChild>
<div className="flex flex-wrap gap-x-2 gap-y-1">
<TagList selectedTags={selectedTags} isLoading={isLoading} />
</div>
</PopoverTrigger>
<PopoverContent>
<Command>
<TagInput
value={inputValue}
onValueChange={setInputValue}
selectedTags={selectedTags}
setSelectedTags={setSelectedTags}
/>
<CommandList
className={availableTags.length > 0 ? "mt-2" : undefined}
>
<CommandGroup>
{availableTags.slice(0, 5).map((value: string) => (
<TagCommandItem
key={value}
value={value}
selectedTags={selectedTags}
setSelectedTags={setSelectedTags}
/>
))}
<TagCreateItem
onSelect={handleItemCreate}
inputValue={inputValue}
options={allTags}
/>
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
);
};
export default TagManager;
@@ -0,0 +1,74 @@
import React, { useState } from "react";
import { api } from "@/src/utils/api";
import { useHasAccess } from "@/src/features/rbac/utils/checkAccess";
import { type RouterOutput } from "@/src/utils/types";
import TagManager from "@/src/features/tag/components/TagMananger";
type TagTraceDetailsPopoverProps = {
tags: string[];
availableTags: string[];
projectId: string;
traceId: string;
};
export function TagTraceDetailsPopover({
tags,
availableTags,
projectId,
traceId,
}: TagTraceDetailsPopoverProps) {
const [isLoading, setIsLoading] = useState(false);
const hasAccess = useHasAccess({ projectId, scope: "objects:tag" });
const utils = api.useUtils();
const mutTags = api.traces.updateTags.useMutation({
onMutate: async () => {
await utils.traces.byId.cancel();
setIsLoading(true);
// Snapshot the previous value
const prev = utils.traces.byId.getData({ traceId });
return { prev };
},
onError: (err, _newTags, context) => {
setIsLoading(false);
// Rollback to the previous value if mutation fails
utils.traces.byId.setData({ traceId }, context?.prev);
},
onSettled: (data, error, { traceId, tags }) => {
setIsLoading(false);
utils.traces.byId.setData(
{ traceId },
(oldQueryData: RouterOutput["traces"]["byId"] | undefined) => {
return oldQueryData
? {
...oldQueryData,
tags: tags,
}
: undefined;
},
);
void utils.traces.all.invalidate();
void utils.traces.byId.invalidate();
void utils.traces.filterOptions.invalidate();
},
});
function mutateTags(newTags: string[]) {
void mutTags.mutateAsync({
projectId,
traceId,
tags: newTags,
});
}
return (
<TagManager
tags={tags}
allTags={availableTags}
hasAccess={hasAccess}
isLoading={isLoading}
mutateTags={mutateTags}
/>
);
}
@@ -0,0 +1,73 @@
import React, { useState } from "react";
import { api } from "@/src/utils/api";
import { useHasAccess } from "@/src/features/rbac/utils/checkAccess";
import { type RouterOutput, type RouterInput } from "@/src/utils/types";
import TagManager from "@/src/features/tag/components/TagMananger";
type TagTracePopverProps = {
tags: string[];
availableTags: string[];
projectId: string;
traceId: string;
tracesFilter: RouterInput["traces"]["all"];
};
export function TagTracePopver({
tags,
availableTags,
projectId,
traceId,
tracesFilter,
}: TagTracePopverProps) {
const [isLoading, setIsLoading] = useState(false);
const hasAccess = useHasAccess({ projectId, scope: "objects:tag" });
const utils = api.useUtils();
const mutTags = api.traces.updateTags.useMutation({
onMutate: async () => {
await utils.traces.all.cancel();
setIsLoading(true);
const prevTrace = utils.traces.all.getData(tracesFilter);
return { prevTrace };
},
onError: (err, _newTags, context) => {
utils.traces.all.setData(tracesFilter, context?.prevTrace);
console.log("error", err);
setIsLoading(false);
},
onSettled: (data, error, { traceId, tags }) => {
utils.traces.all.setData(
tracesFilter,
(oldQueryData: RouterOutput["traces"]["all"] | undefined) => {
return oldQueryData
? {
totalCount: oldQueryData.totalCount,
traces: oldQueryData.traces.map((trace) => {
return trace.id === traceId ? { ...trace, tags } : trace;
}),
}
: { totalCount: undefined, traces: [] };
},
);
setIsLoading(false);
},
});
function mutateTags(newTags: string[]) {
void mutTags.mutateAsync({
projectId,
traceId,
tags: newTags,
});
}
return (
<TagManager
tags={tags}
allTags={availableTags}
hasAccess={hasAccess}
isLoading={isLoading}
mutateTags={mutateTags}
/>
);
}
+32
View File
@@ -0,0 +1,32 @@
import { useState, useMemo } from "react";
type UseTagManagerProps = {
initialTags: string[];
allTags: string[];
};
function useTagManager({ initialTags, allTags }: UseTagManagerProps) {
const [selectedTags, setSelectedTags] = useState(initialTags);
const [inputValue, setInputValue] = useState("");
const availableTags = useMemo(
() => allTags.filter((value) => !selectedTags.includes(value)),
[allTags, selectedTags],
);
const handleItemCreate = () => {
setSelectedTags([...selectedTags, inputValue]);
availableTags.push(inputValue);
setInputValue("");
};
return {
selectedTags,
inputValue,
availableTags,
handleItemCreate,
setInputValue,
setSelectedTags,
};
}
export default useTagManager;
@@ -4,12 +4,16 @@ import { Button } from "@/src/components/ui/button";
import { env } from "@/src/env.mjs";
import { api } from "@/src/utils/api";
import { Card, Flex, MarkerBar, Metric, Text } from "@tremor/react";
import {
chatAvailable,
sendUserChatMessage,
showAgentChatMessage,
} from "@/src/features/support-chat/chat";
import Link from "next/link";
import { PricingPage } from "@/src/features/pricing-page/PricingPage";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTrigger,
} from "@/src/components/ui/dialog";
import Header from "@/src/components/layouts/header";
import { usePostHog } from "posthog-js/react";
export const ProjectUsageChart: React.FC<{ projectId: string }> = ({
projectId,
@@ -17,6 +21,7 @@ export const ProjectUsageChart: React.FC<{ projectId: string }> = ({
const usage = api.usageMetering.currentMonth.useQuery({
projectId,
});
const posthog = usePostHog();
const project = api.projects.byId.useQuery({ projectId });
const planLimit =
project.data?.cloudConfig?.monthlyObservationLimit ?? 100_000;
@@ -56,28 +61,38 @@ export const ProjectUsageChart: React.FC<{ projectId: string }> = ({
) : null}
</Card>
<div className="mt-4 flex flex-row items-center gap-2">
{chatAvailable && (
<Button
variant="secondary"
className=""
onClick={() => {
sendUserChatMessage(
"I want to change my plan, project: " + projectId,
);
// wait for 2 seconds
setTimeout(() => {
showAgentChatMessage(
"We're happy to help. Which plan would you like to change to? See https://langfuse.com/#pricing for details on available plans.",
);
}, 2000);
}}
>
Request plan change
</Button>
)}
<Button variant="secondary" asChild>
<Link href="https://langfuse.com/pricing">View plans</Link>
</Button>
<Dialog
onOpenChange={(open) => {
if (open) {
posthog.capture("project_settings:pricing_dialog_opened");
}
}}
>
<DialogTrigger asChild>
<Button variant="secondary">Change plans</Button>
</DialogTrigger>
<DialogContent className="max-w-3xl">
<DialogHeader>
<Header
title="Select plan"
level="h3"
actionButtons={
<Button variant="secondary" asChild>
<Link href="https://langfuse.com/pricing">
Pricing page
</Link>
</Button>
}
/>
</DialogHeader>
<p>
All plans offer a 7-day free trial. For more information about the
plans, please visit our pricing page or reach out to us via the
chat.
</p>
<PricingPage className="mb-5 mt-10 " />
</DialogContent>
</Dialog>
<div className="inline-block text-sm text-gray-500">
Currently: {plan}
</div>
@@ -47,7 +47,6 @@ export default async function handler(
where: {
name: name,
projectId: authCheck.scope.projectId,
status: "ACTIVE",
},
include: {
datasetItems: {
@@ -65,7 +64,7 @@ export default async function handler(
if (!dataset) {
return res.status(404).json({
message: "Dataset not found or not active",
message: "Dataset not found",
});
}
+12
View File
@@ -25,6 +25,15 @@ import { ScoreProcessor } from "../../../server/api/services/EventProcessor";
import { isNotNullOrUndefined } from "@/src/utils/types";
import { telemetry } from "@/src/features/telemetry";
import { jsonSchema } from "@/src/utils/zod";
import * as Sentry from "@sentry/nextjs";
export const config = {
api: {
bodyParser: {
sizeLimit: "3mb",
},
},
};
export default async function handler(
req: NextApiRequest,
@@ -298,6 +307,9 @@ export const handleBatchResult = (
error: error.error.message,
});
} else {
if (process.env.NEXT_PUBLIC_SENTRY_DSN) {
Sentry.captureException(error.error);
}
returnedErrors.push({
id: error.id,
status: 500,
+18 -1
View File
@@ -15,12 +15,24 @@ import {
} from "@/src/features/public-api/server/ingestion-api-schema";
import { v4 } from "uuid";
import { telemetry } from "@/src/features/telemetry";
import { orderByToPrismaSql } from "@/src/features/orderBy/server/orderByToPrisma";
import { tracesTableCols } from "@/src/server/api/definitions/tracesTable";
import { orderBy } from "@/src/server/api/interfaces/orderBy";
const GetTracesSchema = z.object({
...paginationZod,
userId: z.string().nullish(),
name: z.string().nullish(),
tags: z.union([z.array(z.string()), z.string()]).nullish(),
orderBy: z
.string() // orderBy=timestamp.asc
.nullish()
.transform((v) => {
if (!v) return null;
const [column, order] = v.split(".");
return { column, order: order?.toUpperCase() };
})
.pipe(orderBy.nullish()),
});
export default async function handler(
@@ -92,6 +104,11 @@ export default async function handler(
)}] <@ t."tags"`
: Prisma.empty;
const orderByCondition = orderByToPrismaSql(
obj.orderBy ?? null,
tracesTableCols,
);
const traces = await prisma.$queryRaw<
Array<Trace & { observations: string[]; scores: string[] }>
>(Prisma.sql`
@@ -116,7 +133,7 @@ export default async function handler(
${nameCondition}
${tagsCondition}
GROUP BY t.id
ORDER BY t."timestamp" DESC
${orderByCondition}
LIMIT ${obj.limit} OFFSET ${skipValue}
`);
const totalItems = await prisma.trace.count({
+11 -9
View File
@@ -1,9 +1,9 @@
import { verifyAuthHeaderAndReturnScope } from "@/src/features/public-api/server/apiAuth";
import { cors, runMiddleware } from "@/src/features/public-api/server/cors";
import { mapUsageOutput } from "@/src/features/public-api/server/outputSchemaConversion";
import { prisma } from "@/src/server/db";
import { type NextApiRequest, type NextApiResponse } from "next";
import { z } from "zod";
import { cors, runMiddleware } from "@/src/features/public-api/server/cors";
import { prisma } from "@/src/server/db";
import { verifyAuthHeaderAndReturnScope } from "@/src/features/public-api/server/apiAuth";
import { mapUsageOutput } from "@/src/features/public-api/server/outputSchemaConversion";
const GetTraceSchema = z.object({
traceId: z.string(),
@@ -53,6 +53,13 @@ export default async function handler(
scores: true,
},
});
if (!trace) {
return res.status(404).json({
message: "Trace not found within authorized project",
});
}
const observations = await prisma.observation.findMany({
where: {
traceId: traceId,
@@ -60,11 +67,6 @@ export default async function handler(
},
});
if (!trace) {
return res.status(404).json({
message: "Trace not found within authorized project",
});
}
return res
.status(200)
.json({ ...trace, observations: observations.map(mapUsageOutput) });
+3 -29
View File
@@ -18,7 +18,6 @@ import type * as z from "zod";
import { env } from "@/src/env.mjs";
import { useState } from "react";
import { LangfuseIcon } from "@/src/components/LangfuseLogo";
import { usePostHog } from "posthog-js/react";
import { CloudPrivacyNotice } from "@/src/features/auth/components/AuthCloudPrivacyNotice";
import { CloudRegionSwitch } from "@/src/features/auth/components/AuthCloudRegionSwitch";
import { SSOButtons, type PageProps } from "@/src/pages/auth/sign-in";
@@ -28,7 +27,6 @@ import { PasswordInput } from "@/src/components/ui/password-input";
export { getServerSideProps } from "@/src/pages/auth/sign-in";
export default function SignIn({ authProviders }: PageProps) {
const posthog = usePostHog();
const [formError, setFormError] = useState<string | null>(null);
const form = useForm<z.infer<typeof signupSchema>>({
resolver: zodResolver(signupSchema),
@@ -36,20 +34,12 @@ export default function SignIn({ authProviders }: PageProps) {
name: "",
email: "",
password: "",
referralSource: "",
},
});
async function onSubmit(values: z.infer<typeof signupSchema>) {
try {
setFormError(null);
if (values.referralSource !== "") {
posthog.capture("survey sent", {
$survey_id: "018ade05-4d8c-0000-36b7-fc390b221590",
$survey_name: "Referral source",
$survey_response: values.referralSource,
});
}
const res = await fetch("/api/auth/signup", {
method: "POST",
headers: { "Content-Type": "application/json" },
@@ -65,7 +55,9 @@ export default function SignIn({ authProviders }: PageProps) {
await signIn<"credentials">("credentials", {
email: values.email,
password: values.password,
callbackUrl: "/?getStarted=1",
callbackUrl: env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION
? "/onboarding"
: "/?getStarted=1",
});
} catch (err) {
setFormError("An error occurred. Please try again.");
@@ -142,24 +134,6 @@ export default function SignIn({ authProviders }: PageProps) {
</FormItem>
)}
/>
{env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION !== undefined ? (
<FormField
control={form.control}
name="referralSource"
render={({ field }) => (
<FormItem>
<FormLabel>
Where did you hear about us?{" "}
<span className="font-normal">(optional)</span>
</FormLabel>
<FormControl>
<Input type="referralSource" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
) : null}
<Button
type="submit"
className="w-full"
+3 -1
View File
@@ -54,7 +54,9 @@ export default function GetStartedPage() {
<div className="grid grid-cols-1 gap-5 lg:grid-cols-2">
<Card className="flex-1">
<CardHeader>
<CardTitle>Create new project</CardTitle>
<CardTitle data-testid="create-new-project-title">
Create new project
</CardTitle>
</CardHeader>
<CardContent>
<p>
+93
View File
@@ -0,0 +1,93 @@
// This page is currently only shown to Langfuse cloud users.
// It might be expanded to everyone in the future when it does not only ask for the referral source.
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { Button } from "@/src/components/ui/button";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/src/components/ui/form";
import { usePostHog } from "posthog-js/react";
import { useRouter } from "next/router";
import { LangfuseIcon } from "@/src/components/LangfuseLogo";
import { Textarea } from "@/src/components/ui/textarea";
const referralSourceSchema = z.object({
referralSource: z.string().optional(),
});
export default function ReferralSource() {
const posthog = usePostHog();
const router = useRouter();
const form = useForm<z.infer<typeof referralSourceSchema>>({
resolver: zodResolver(referralSourceSchema),
defaultValues: {
referralSource: "",
},
});
function onSubmit(values: z.infer<typeof referralSourceSchema>) {
if (values.referralSource && values.referralSource !== "") {
posthog.capture("survey sent", {
$survey_id: "018ade05-4d8c-0000-36b7-fc390b221590",
$survey_name: "Referral source",
$survey_response: values.referralSource,
});
}
void router.push("/?getStarted=1");
}
return (
<div className="flex flex-1 flex-col py-6 sm:min-h-full sm:justify-center sm:px-6 sm:py-12 lg:px-8">
<div className="sm:mx-auto sm:w-full sm:max-w-md">
<LangfuseIcon className="mx-auto" />
<h2 className="mt-4 text-center text-2xl font-bold leading-9 tracking-tight text-gray-900">
Welcome to Langfuse
</h2>
</div>
<div className="mt-14 bg-white px-6 py-10 shadow sm:mx-auto sm:w-full sm:max-w-[480px] sm:rounded-lg sm:px-12">
<Form {...form}>
<form
className="space-y-6"
// eslint-disable-next-line @typescript-eslint/no-misused-promises
onSubmit={form.handleSubmit(onSubmit)}
>
<FormField
control={form.control}
name="referralSource"
render={({ field }) => (
<FormItem>
<FormLabel>
Where did you hear about us?{" "}
<span className="font-normal">(optional)</span>
</FormLabel>
<FormControl>
<Textarea
placeholder="We're curious to know how you discovered the project! Thanks for sharing."
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button
type="submit"
variant={form.formState.isDirty ? "default" : "secondary"}
className="w-full"
loading={form.formState.isSubmitting}
>
{form.formState.isDirty ? "Continue" : "Skip"}
</Button>
</form>
</Form>
</div>
</div>
);
}
@@ -4,6 +4,7 @@ import { api } from "@/src/utils/api";
import { useRouter } from "next/router";
import { Tabs, TabsList, TabsTrigger } from "@/src/components/ui/tabs";
import Link from "next/link";
import { DetailPageNav } from "@/src/features/navigate-detail-pages/DetailPageNav";
export default function Dataset() {
const router = useRouter();
@@ -23,6 +24,13 @@ export default function Dataset() {
{ name: "Datasets", href: `/project/${projectId}/datasets` },
{ name: dataset.data?.name ?? datasetId },
]}
actionButtons={
<DetailPageNav
currentId={datasetId}
path={(id) => `/project/${projectId}/datasets/${id}`}
listKey="datasets"
/>
}
/>
<Tabs value="runs" className="mb-3">
<TabsList>
@@ -4,6 +4,7 @@ import { useRouter } from "next/router";
import { Tabs, TabsList, TabsTrigger } from "@/src/components/ui/tabs";
import Link from "next/link";
import { DatasetItemsTable } from "@/src/features/datasets/components/DatasetItemsTable";
import { DetailPageNav } from "@/src/features/navigate-detail-pages/DetailPageNav";
export default function DatasetItems() {
const router = useRouter();
@@ -23,6 +24,13 @@ export default function DatasetItems() {
{ name: "Datasets", href: `/project/${projectId}/datasets` },
{ name: dataset.data?.name ?? datasetId },
]}
actionButtons={
<DetailPageNav
currentId={datasetId}
path={(id) => `/project/${projectId}/datasets/${id}/items/`}
listKey="datasets"
/>
}
/>
<Tabs value="items" className="mb-3">
<TabsList>
@@ -1,6 +1,7 @@
import Header from "@/src/components/layouts/header";
import { DatasetRunItemsTable } from "@/src/features/datasets/components/DatasetRunItemsTable";
import { EditDatasetItem } from "@/src/features/datasets/components/EditDatasetItem";
import { DetailPageNav } from "@/src/features/navigate-detail-pages/DetailPageNav";
import { api } from "@/src/utils/api";
import { useRouter } from "next/router";
@@ -27,6 +28,15 @@ export default function Dataset() {
},
{ name: "Item: " + itemId },
]}
actionButtons={
<DetailPageNav
currentId={itemId}
path={(id) =>
`/project/${projectId}/datasets/${datasetId}/items/${id}`
}
listKey="datasetItems"
/>
}
/>
<EditDatasetItem
projectId={projectId}
@@ -1,5 +1,6 @@
import Header from "@/src/components/layouts/header";
import { DatasetRunItemsTable } from "@/src/features/datasets/components/DatasetRunItemsTable";
import { DetailPageNav } from "@/src/features/navigate-detail-pages/DetailPageNav";
import { api } from "@/src/utils/api";
import { useRouter } from "next/router";
@@ -26,6 +27,15 @@ export default function Dataset() {
},
{ name: "Run: " + runId },
]}
actionButtons={
<DetailPageNav
currentId={runId}
path={(id) =>
`/project/${projectId}/datasets/${datasetId}/runs/${id}`
}
listKey="datasetRuns"
/>
}
/>
<DatasetRunItemsTable
projectId={projectId}
+1
View File
@@ -105,6 +105,7 @@ export default function Start() {
projectId={projectId}
globalFilterState={globalFilterState}
/>
<MetricTable
className="col-span-1 xl:col-span-2"
projectId={projectId}
@@ -3,7 +3,7 @@ import { SessionPage } from "@/src/components/session";
export default function Trace() {
const router = useRouter();
const sessionId = router.query.sessionId as string;
const sessionId = decodeURIComponent(router.query.sessionId as string);
const projectId = router.query.projectId as string;
return <SessionPage sessionId={sessionId} projectId={projectId} />;
+2 -2
View File
@@ -47,7 +47,7 @@ export default function UsersPage() {
console.log("setting detail page list");
setDetailPageList(
"users",
users.data.map((u) => u.userId),
users.data.map((u) => encodeURIComponent(u.userId)),
);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
@@ -63,7 +63,7 @@ export default function UsersPage() {
return typeof value === "string" ? (
<>
<TableLink
path={`/project/${projectId}/users/${value}`}
path={`/project/${projectId}/users/${encodeURIComponent(value)}`}
value={value}
truncateAt={40}
/>
@@ -12,9 +12,9 @@ import SessionsTable from "@/src/components/table/use-cases/sessions";
const tabs = ["Details", "Sessions", "Traces", "Scores"] as const;
export default function TracePage() {
export default function UserPage() {
const router = useRouter();
const userId = router.query.userId as string;
const userId = decodeURIComponent(router.query.userId as string);
const projectId = router.query.projectId as string;
const [currentTab, setCurrentTab] = useQueryParam(
@@ -51,8 +51,10 @@ export default function TracePage() {
]}
actionButtons={
<DetailPageNav
currentId={userId}
path={(id) => `/project/${projectId}/users/${id}`}
currentId={encodeURIComponent(userId)}
path={(id) =>
`/project/${projectId}/users/${encodeURIComponent(id)}`
}
listKey="users"
/>
}
@@ -8,62 +8,73 @@ import { ObservationLevel } from "@prisma/client";
export const observationsTableCols: ColumnDefinition[] = [
{
name: "id",
id: "id",
type: "string",
internal: 'o."id"',
},
{
name: "name",
id: "name",
type: "stringOptions",
internal: 'o."name"',
options: [], // to be added at runtime
},
{ name: "traceId", type: "string", internal: 't."id"' },
{ name: "traceId", id: "traceId", type: "string", internal: 't."id"' },
{
name: "traceName",
id: "traceName",
type: "stringOptions",
internal: 't."name"',
options: [], // to be added at runtime
},
{ name: "userId", type: "string", internal: 't."user_id"' },
{ name: "userId", id: "userId", type: "string", internal: 't."user_id"' },
{
name: "start_time",
id: "startTime",
type: "datetime",
internal: 'o."start_time"',
},
{
name: "end_time",
id: "endTime",
type: "datetime",
internal: 'o."end_time"',
},
{
name: "latency (s)",
id: "latency",
type: "number",
internal: '"latency"',
},
{
name: "level",
id: "level",
type: "stringOptions",
internal: 'o."level"::text',
options: Object.values(ObservationLevel).map((value) => ({ value })),
},
{
name: "Status Message",
id: "statusMessage",
type: "string",
internal: 'o."status_message"',
},
{
name: "model",
id: "model",
type: "stringOptions",
internal: 'o."model"',
options: [], // to be added at runtime
},
{
name: "metadata",
id: "metadata",
type: "stringObject",
internal: 't."metadata"',
internal: 'o."metadata"',
},
{
name: "version",
id: "version",
type: "string",
internal: 'o."version"',
},
+15 -1
View File
@@ -1,7 +1,13 @@
import { type ColumnDefinition } from "@/src/server/api/interfaces/tableDefinition";
export const sessionsViewCols: ColumnDefinition[] = [
{ name: "⭐️", type: "boolean", internal: "s.bookmarked" },
{ name: "⭐️", id: "bookmarked", type: "boolean", internal: "s.bookmarked" },
{
name: "id",
id: "id",
type: "string",
internal: 's."id"',
},
{
name: "userId",
type: "string",
@@ -9,12 +15,20 @@ export const sessionsViewCols: ColumnDefinition[] = [
},
{
name: "Session duration (s)",
id: "sessionDuration",
type: "number",
internal: 'o."sessionDuration"',
},
{
name: "createdAt",
id: "createdAt",
type: "datetime",
internal: 's."created_at"',
},
{
name: "countTraces",
id: "countTraces",
type: "number",
internal: 't."countTraces"',
},
];
+71 -27
View File
@@ -6,7 +6,7 @@ import {
} from "@/src/server/api/trpc";
import { type Observation, Prisma } from "@prisma/client";
import { paginationZod } from "@/src/utils/zod";
import { jsonSchema, paginationZod } from "@/src/utils/zod";
import { singleFilter } from "@/src/server/api/interfaces/filters";
import {
datetimeFilterToPrismaSql,
@@ -30,6 +30,8 @@ import {
exportFileFormats,
exportOptions,
} from "@/src/server/api/interfaces/exportTypes";
import { orderBy } from "@/src/server/api/interfaces/orderBy";
import { orderByToPrismaSql } from "@/src/features/orderBy/server/orderByToPrisma";
const GenerationFilterOptions = z.object({
projectId: z.string(), // Required for protectedProjectProcedure
@@ -38,6 +40,7 @@ const GenerationFilterOptions = z.object({
});
const ListInputs = GenerationFilterOptions.extend({
orderBy: orderBy,
...paginationZod,
});
@@ -64,6 +67,11 @@ export const generationsRouter = createTRPCRouter({
observationsTableCols,
);
const orderByCondition = orderByToPrismaSql(
input.orderBy,
observationsTableCols,
);
// to improve query performance, add timeseries filter to observation queries as well
const startTimeFilter = input.filter.find(
(f) => f.column === "start_time" && f.type === "datetime",
@@ -115,39 +123,65 @@ export const generationsRouter = createTRPCRouter({
o.total_tokens as "totalTokens",
o.level,
o.status_message as "statusMessage",
o.version,
(count(*) OVER())::int AS "totalCount"
o.version
FROM observations_with_latency o
JOIN traces t ON t.id = o.trace_id
WHERE
t.project_id = ${input.projectId}
${searchCondition}
${filterCondition}
ORDER BY o.start_time DESC
${orderByCondition}
LIMIT ${input.limit}
OFFSET ${input.page * input.limit}
`,
);
const pricings = await ctx.prisma.pricing.findMany();
const totalGenerations = await ctx.prisma.$queryRaw<
Array<{ count: bigint }>
>(
Prisma.sql`
WITH observations_with_latency AS (
SELECT
o.*,
CASE WHEN o.end_time IS NULL THEN NULL ELSE (EXTRACT(EPOCH FROM o."end_time") - EXTRACT(EPOCH FROM o."start_time"))::double precision END AS "latency"
FROM observations o
WHERE o.type = 'GENERATION'
AND o.project_id = ${input.projectId}
${datetimeFilter}
)
SELECT
count(*)
FROM observations_with_latency o
JOIN traces t ON t.id = o.trace_id
WHERE
t.project_id = ${input.projectId}
${searchCondition}
${filterCondition}
`,
);
return generations.map(({ input, output, ...rest }) => {
return {
...rest,
input,
output,
cost: rest.model
? calculateTokenCost(pricings, {
model: rest.model,
totalTokens: new Decimal(rest.totalTokens),
promptTokens: new Decimal(rest.promptTokens),
completionTokens: new Decimal(rest.completionTokens),
input: input,
output: output,
})
: undefined,
};
});
const pricings = await ctx.prisma.pricing.findMany();
const count = totalGenerations[0]?.count;
return {
totalCount: count ? Number(count) : undefined,
generations: generations.map(({ input, output, ...rest }) => {
return {
...rest,
input,
output,
cost: rest.model
? calculateTokenCost(pricings, {
model: rest.model,
totalTokens: new Decimal(rest.totalTokens),
promptTokens: new Decimal(rest.promptTokens),
completionTokens: new Decimal(rest.completionTokens),
input: input,
output: output,
})
: undefined,
};
}),
};
}),
export: protectedProjectProcedure
@@ -253,7 +287,7 @@ export const generationsRouter = createTRPCRouter({
generation.startTime.toISOString(),
generation.endTime?.toISOString() ?? "",
generation.cost
? usdFormatter(generation.cost.toNumber())
? usdFormatter(generation.cost.toNumber(), 2, 8)
: "",
JSON.stringify(generation.input),
JSON.stringify(generation.output),
@@ -277,9 +311,11 @@ export const generationsRouter = createTRPCRouter({
content: z.string(),
}),
);
const outputSchema = z.object({
completion: z.string(),
});
const outputSchema = z
.object({
completion: jsonSchema,
})
.or(jsonSchema);
output = enrichedGenerations
.map((generation) => ({
parsedInput: inputSchemaOpenAI.safeParse(generation.input),
@@ -293,7 +329,14 @@ export const generationsRouter = createTRPCRouter({
? [
{
role: "assistant",
content: generation.parsedOutput.data.completion,
content:
typeof generation.parsedOutput.data ===
"object" &&
"completion" in generation.parsedOutput.data
? JSON.stringify(
generation.parsedOutput.data.completion,
)
: JSON.stringify(generation.parsedOutput.data),
},
]
: [],
@@ -303,6 +346,7 @@ export const generationsRouter = createTRPCRouter({
// to jsonl
.map((row) => JSON.stringify(row))
.join("\n");
console.log(output);
break;
default:
throw new Error("Invalid export file format");
-1
View File
@@ -20,7 +20,6 @@ export const observationsRouter = createTRPCRouter({
traceId: input.traceId,
},
});
const scores = generation.traceId
? await ctx.prisma.score.findMany({
where: {
+8 -1
View File
@@ -12,10 +12,13 @@ import { singleFilter } from "@/src/server/api/interfaces/filters";
import { paginationZod } from "@/src/utils/zod";
import { throwIfNoAccess } from "@/src/features/rbac/utils/checkAccess";
import { TRPCError } from "@trpc/server";
import { orderBy } from "@/src/server/api/interfaces/orderBy";
import { orderByToPrismaSql } from "@/src/features/orderBy/server/orderByToPrisma";
const SessionFilterOptions = z.object({
projectId: z.string(), // Required for protectedProjectProcedure
filter: z.array(singleFilter).nullable(),
orderBy: orderBy,
...paginationZod,
});
@@ -28,6 +31,10 @@ export const sessionRouter = createTRPCRouter({
input.filter ?? [],
sessionsViewCols,
);
const orderByCondition = orderByToPrismaSql(
input.orderBy,
sessionsViewCols,
);
const sessions = await ctx.prisma.$queryRaw<
Array<{
@@ -79,7 +86,7 @@ export const sessionRouter = createTRPCRouter({
WHERE
s."project_id" = ${input.projectId}
${filterCondition}
ORDER BY 2 desc
${orderByCondition}
LIMIT ${input.limit}
OFFSET ${input.page * input.limit}
`);
+144 -74
View File
@@ -22,6 +22,7 @@ import { throwIfNoAccess } from "@/src/features/rbac/utils/checkAccess";
import { TRPCError } from "@trpc/server";
import { orderBy } from "@/src/server/api/interfaces/orderBy";
import { orderByToPrismaSql } from "@/src/features/orderBy/server/orderByToPrisma";
import { type Sql } from "@prisma/client/runtime/library";
const TraceFilterOptions = z.object({
projectId: z.string(), // Required for protectedProjectProcedure
@@ -70,58 +71,8 @@ export const traceRouter = createTRPCRouter({
)`
: Prisma.empty;
const query = Prisma.sql`
WITH usage AS (
SELECT
trace_id,
sum(prompt_tokens) AS "promptTokens",
sum(completion_tokens) AS "completionTokens",
sum(total_tokens) AS "totalTokens"
FROM
"observations"
WHERE
"trace_id" IS NOT NULL
AND "type" = 'GENERATION'
AND "project_id" = ${input.projectId}
${observationTimeseriesFilter}
GROUP BY
trace_id
),
trace_latency AS (
SELECT
trace_id,
EXTRACT(EPOCH FROM COALESCE(MAX("end_time"), MAX("start_time"))) - EXTRACT(EPOCH FROM MIN("start_time"))::double precision AS "latency"
FROM
"observations"
WHERE
"trace_id" IS NOT NULL
AND "project_id" = ${input.projectId}
${observationTimeseriesFilter}
GROUP BY
trace_id
),
-- used for filtering
scores_avg AS (
SELECT
trace_id,
jsonb_object_agg(name::text, avg_value::double precision) AS scores_avg
FROM (
SELECT
trace_id,
name,
avg(value) avg_value
FROM
scores
GROUP BY
1,
2
ORDER BY
1) tmp
GROUP BY
1
)
SELECT
t.*,
const tracesQuery = createTracesQuery(
Prisma.sql`t.*,
t."user_id" AS "userId",
t."metadata" AS "metadata",
t.session_id AS "sessionId",
@@ -129,22 +80,15 @@ export const traceRouter = createTRPCRouter({
COALESCE(u."promptTokens", 0)::int AS "promptTokens",
COALESCE(u."completionTokens", 0)::int AS "completionTokens",
COALESCE(u."totalTokens", 0)::int AS "totalTokens",
tl.latency AS "latency",
(count(*) OVER ())::int AS "totalCount"
FROM
"traces" AS t
LEFT JOIN usage AS u ON u.trace_id = t.id
-- used for filtering
LEFT JOIN scores_avg AS s_avg ON s_avg.trace_id = t.id
LEFT JOIN trace_latency AS tl ON tl.trace_id = t.id
WHERE
t."project_id" = ${input.projectId}
${searchCondition}
${filterCondition}
${orderByCondition}
LIMIT ${input.limit}
OFFSET ${input.page * input.limit}
`;
tl.latency AS "latency"`,
input.projectId,
observationTimeseriesFilter,
input.page,
input.limit,
searchCondition,
filterCondition,
orderByCondition,
);
const traces = await ctx.prisma.$queryRaw<
Array<
@@ -156,7 +100,21 @@ export const traceRouter = createTRPCRouter({
latency: number | null;
}
>
>(query);
>(tracesQuery);
const countQyery = createTracesQuery(
Prisma.sql`count(*)`,
input.projectId,
observationTimeseriesFilter,
0,
1,
searchCondition,
filterCondition,
Prisma.empty,
);
const totalTraces =
await ctx.prisma.$queryRaw<Array<{ count: bigint }>>(countQyery);
// get scores for each trace individually to increase
// performance of the query above
@@ -170,11 +128,14 @@ export const traceRouter = createTRPCRouter({
},
},
});
return traces.map((trace) => {
const filteredScores = scores.filter((s) => s.traceId === trace.id);
return { ...trace, scores: filteredScores };
});
const totalTraceCount = totalTraces[0]?.count;
return {
traces: traces.map((trace) => {
const filteredScores = scores.filter((s) => s.traceId === trace.id);
return { ...trace, scores: filteredScores };
}),
totalCount: totalTraceCount ? Number(totalTraceCount) : undefined,
};
}),
filterOptions: protectedProjectProcedure
.input(z.object({ projectId: z.string() }))
@@ -404,4 +365,113 @@ export const traceRouter = createTRPCRouter({
}
}
}),
updateTags: protectedProjectProcedure
.input(
z.object({
projectId: z.string(),
traceId: z.string(),
tags: z.array(z.string()),
}),
)
.mutation(async ({ input, ctx }) => {
throwIfNoAccess({
session: ctx.session,
projectId: input.projectId,
scope: "objects:tag",
});
try {
const trace = await ctx.prisma.trace.update({
where: {
id: input.traceId,
projectId: input.projectId,
},
data: {
tags: {
set: input.tags,
},
},
});
return trace;
} catch (error) {
console.error(error);
}
}),
});
function createTracesQuery(
select: Sql,
projectId: string,
observationTimeseriesFilter: Sql,
page: number,
limit: number,
searchCondition: Sql,
filterCondition: Sql,
orderByCondition: Sql,
) {
return Prisma.sql`
WITH usage AS (
SELECT
trace_id,
sum(prompt_tokens) AS "promptTokens",
sum(completion_tokens) AS "completionTokens",
sum(total_tokens) AS "totalTokens"
FROM
"observations"
WHERE
"trace_id" IS NOT NULL
AND "type" = 'GENERATION'
AND "project_id" = ${projectId}
${observationTimeseriesFilter}
GROUP BY
trace_id
),
trace_latency AS (
SELECT
trace_id,
EXTRACT(EPOCH FROM COALESCE(MAX("end_time"), MAX("start_time"))) - EXTRACT(EPOCH FROM MIN("start_time"))::double precision AS "latency"
FROM
"observations"
WHERE
"trace_id" IS NOT NULL
AND "project_id" = ${projectId}
${observationTimeseriesFilter}
GROUP BY
trace_id
),
-- used for filtering
scores_avg AS (
SELECT
trace_id,
jsonb_object_agg(name::text, avg_value::double precision) AS scores_avg
FROM (
SELECT
trace_id,
name,
avg(value) avg_value
FROM
scores
GROUP BY
1,
2
ORDER BY
1) tmp
GROUP BY
1
)
SELECT
${select}
FROM
"traces" AS t
LEFT JOIN usage AS u ON u.trace_id = t.id
-- used for filtering
LEFT JOIN scores_avg AS s_avg ON s_avg.trace_id = t.id
LEFT JOIN trace_latency AS tl ON tl.trace_id = t.id
WHERE
t."project_id" = ${projectId}
${searchCondition}
${filterCondition}
${orderByCondition}
LIMIT ${limit}
OFFSET ${page * limit}
`;
}
+70 -46
View File
@@ -57,10 +57,13 @@ export class ObservationProcessor implements EventProcessor {
this.event = event;
}
async convertToObservation(apiScope: ApiAccessScope): Promise<{
async convertToObservation(
apiScope: ApiAccessScope,
existingObservation: Observation | null,
): Promise<{
id: string;
create: Prisma.ObservationCreateInput;
update: Prisma.ObservationUpdateInput;
create: Prisma.ObservationUncheckedCreateInput;
update: Prisma.ObservationUncheckedUpdateInput;
}> {
const { body } = this.event;
@@ -85,12 +88,6 @@ export class ObservationProcessor implements EventProcessor {
const { id, traceId, name, startTime, metadata } = body;
const existingObservation = id
? await prisma.observation.findUnique({
where: { id, projectId: apiScope.projectId },
})
: null;
if (
this.event.type === eventTypes.OBSERVATION_UPDATE &&
!existingObservation
@@ -124,20 +121,26 @@ export class ObservationProcessor implements EventProcessor {
metadata ?? undefined,
);
const prompts =
const prompt =
"promptName" in this.event.body &&
typeof this.event.body.promptName === "string" &&
"promptVersion" in this.event.body &&
typeof this.event.body.promptVersion === "number"
? await prisma.prompt.findMany({
? await prisma.prompt.findUnique({
where: {
projectId: apiScope.projectId,
name: this.event.body.promptName,
version: this.event.body.promptVersion,
projectId_name_version: {
projectId: apiScope.projectId,
name: this.event.body.promptName,
version: this.event.body.promptVersion,
},
},
})
: undefined;
// Only null if promptName and promptVersion are set but prompt is not found
if (prompt === null)
console.warn("Prompt not found for observation", this.event.body);
const observationId = id ?? v4();
return {
id: observationId,
@@ -174,10 +177,8 @@ export class ObservationProcessor implements EventProcessor {
statusMessage: body.statusMessage ?? undefined,
parentObservationId: body.parentObservationId ?? undefined,
version: body.version ?? undefined,
project: { connect: { id: apiScope.projectId } },
...(prompts && prompts.length === 1
? { prompt: { connect: { id: prompts[0]?.id } } }
: undefined),
projectId: apiScope.projectId,
promptId: prompt ? prompt.id : undefined,
},
update: {
name,
@@ -209,9 +210,7 @@ export class ObservationProcessor implements EventProcessor {
statusMessage: body.statusMessage ?? undefined,
parentObservationId: body.parentObservationId ?? undefined,
version: body.version ?? undefined,
...(prompts && prompts.length === 1
? { prompt: { connect: { id: prompts[0]?.id } } }
: undefined),
promptId: prompt ? prompt.id : undefined,
},
};
}
@@ -250,14 +249,28 @@ export class ObservationProcessor implements EventProcessor {
if (apiScope.accessLevel !== "all")
throw new AuthenticationError("Access denied for observation creation");
const obs = await this.convertToObservation(apiScope);
const existingObservation = this.event.body.id
? await prisma.observation.findFirst({
where: { id: this.event.body.id },
})
: null;
if (
existingObservation &&
existingObservation.projectId !== apiScope.projectId
) {
throw new AuthenticationError(
`Access denied for observation creation ${existingObservation.projectId} `,
);
}
const obs = await this.convertToObservation(apiScope, existingObservation);
// Do not use nested upserts or multiple where conditions as this should be a single native database upsert
// https://www.prisma.io/docs/orm/reference/prisma-client-reference#database-upserts
return await prisma.observation.upsert({
where: {
id_projectId: {
id: obs.id,
projectId: apiScope.projectId,
},
id: obs.id,
},
create: obs.create,
update: obs.update,
@@ -277,7 +290,9 @@ export class TraceProcessor implements EventProcessor {
const { body } = this.event;
if (apiScope.accessLevel !== "all")
throw new AuthenticationError("Access denied for trace creation");
throw new AuthenticationError(
`Access denied for trace creation, ${apiScope.accessLevel}`,
);
const internalId = body.id ?? v4();
@@ -288,12 +303,18 @@ export class TraceProcessor implements EventProcessor {
body,
);
const existingTrace = await prisma.trace.findUnique({
const existingTrace = await prisma.trace.findFirst({
where: {
id: internalId,
},
});
if (existingTrace && existingTrace.projectId !== apiScope.projectId) {
throw new AuthenticationError(
`Access denied for trace creation ${existingTrace.projectId} `,
);
}
const mergedMetadata = mergeJson(
existingTrace?.metadata
? jsonSchema.parse(existingTrace.metadata)
@@ -301,10 +322,27 @@ export class TraceProcessor implements EventProcessor {
body.metadata ?? undefined,
);
if (body.sessionId) {
await prisma.traceSession.upsert({
where: {
id_projectId: {
id: body.sessionId,
projectId: apiScope.projectId,
},
},
create: {
id: body.sessionId,
projectId: apiScope.projectId,
},
update: {},
});
}
// Do not use nested upserts or multiple where conditions as this should be a single native database upsert
// https://www.prisma.io/docs/orm/reference/prisma-client-reference#database-upserts
const upsertedTrace = await prisma.trace.upsert({
where: {
id: internalId,
projectId: apiScope.projectId,
},
create: {
id: internalId,
@@ -315,16 +353,9 @@ export class TraceProcessor implements EventProcessor {
metadata: mergedMetadata ?? body.metadata ?? undefined,
release: body.release ?? undefined,
version: body.version ?? undefined,
session: body.sessionId
? {
connectOrCreate: {
where: { id: body.sessionId, projectId: apiScope.projectId },
create: { id: body.sessionId, projectId: apiScope.projectId },
},
}
: undefined,
sessionId: body.sessionId ?? undefined,
public: body.public ?? undefined,
project: { connect: { id: apiScope.projectId } },
projectId: apiScope.projectId,
tags: body.tags ?? undefined,
},
update: {
@@ -335,14 +366,7 @@ export class TraceProcessor implements EventProcessor {
metadata: mergedMetadata ?? body.metadata ?? undefined,
release: body.release ?? undefined,
version: body.version ?? undefined,
session: body.sessionId
? {
connectOrCreate: {
where: { id: body.sessionId, projectId: apiScope.projectId },
create: { id: body.sessionId, projectId: apiScope.projectId },
},
}
: undefined,
sessionId: body.sessionId ?? undefined,
public: body.public ?? undefined,
tags: body.tags ?? undefined,
},
+3
View File
@@ -70,6 +70,9 @@ export const createTRPCContext = async (opts: CreateNextContextOptions) => {
import { initTRPC, TRPCError } from "@trpc/server";
import superjson from "superjson";
import { ZodError } from "zod";
import { setUpSuperjson } from "@/src/utils/superjson";
setUpSuperjson();
const t = initTRPC.context<typeof createTRPCContext>().create({
transformer: superjson,
+5
View File
@@ -211,6 +211,11 @@ export const authOptions: NextAuthOptions = {
providers,
pages: {
signIn: "/auth/sign-in",
...(env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION
? {
newUser: "/onboarding",
}
: {}),
},
cookies: {
sessionToken: {
+3
View File
@@ -16,6 +16,9 @@ import { type inferRouterInputs, type inferRouterOutputs } from "@trpc/server";
import superjson from "superjson";
import { type AppRouter } from "@/src/server/api/root";
import { setUpSuperjson } from "@/src/utils/superjson";
setUpSuperjson();
const getBaseUrl = () => {
if (typeof window !== "undefined") return ""; // browser should use relative url
+11 -5
View File
@@ -14,12 +14,18 @@ export const numberFormatter = (number: number) => {
}).format(number);
};
export const usdFormatter = (number: number) =>
new Intl.NumberFormat("en-US", {
export const usdFormatter = (
number: number,
minimumFractionDigits: number = 2,
maximumFractionDigits: number = 4,
) => {
return new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
// These options are needed to round to whole numbers if that's what you want.
//minimumFractionDigits: 0, // (this suffices for whole numbers, but will print 2500.10 as $2,500.1)
//maximumFractionDigits: 0, // (causes 2500.99 to be printed as $2,501)
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat#minimumfractiondigits
minimumFractionDigits,
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat#maximumfractiondigits
maximumFractionDigits,
}).format(number);
};
+13
View File
@@ -0,0 +1,13 @@
import Decimal from "decimal.js";
import superjson from "superjson";
export const setUpSuperjson = () => {
superjson.registerCustom<Decimal, string>(
{
isApplicable: (v): v is Decimal => Decimal.isDecimal(v),
serialize: (v) => v.toJSON(),
deserialize: (v) => new Decimal(v),
},
"decimal.js",
);
};
+3 -1
View File
@@ -16,6 +16,7 @@
"jsx": "preserve",
"incremental": true,
"noUncheckedIndexedAccess": true,
"noErrorTruncation": true,
"baseUrl": ".",
"types": ["jest", "node"],
"paths": {
@@ -29,7 +30,8 @@
"**/*.tsx",
"**/*.cjs",
"**/*.mjs",
"src/features/navigate-detail-pages"
"src/features/navigate-detail-pages",
"types/global.d.ts"
],
"exclude": ["node_modules", "sdk"]
}
+8
View File
@@ -0,0 +1,8 @@
declare namespace JSX {
interface IntrinsicElements {
"stripe-pricing-table": React.DetailedHTMLProps<
React.HTMLAttributes<HTMLElement>,
HTMLElement
>;
}
}