Compare commits

...
199 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
Marc Klingen 35668469f0 chore: release v1.30.0
CI/CD / push-docker-image (push) Blocked by required conditions
CI/CD / tests (18) (push) Failing after 2m25s
CI/CD / tests (20) (push) Failing after 2m15s
release.yml / release (push) Failing after 3s
CI/CD / e2e-tests (push) Failing after 2m3s
CI/CD / lint (push) Successful in 5m28s
CI/CD / all-ci-passed (push) Waiting to run
2024-01-06 02:35:57 +01:00
Marc Klingen 3cfe7df03e docs: add help tooltips to all major pages 2024-01-06 02:35:04 +01:00
Marc Klingen 3301475810 chore: remove filter page which was meant for development 2024-01-06 02:28:01 +01:00
Marc Klingen 04f703dec0 chore: upgrade fern 2024-01-06 02:20:27 +01:00
Marc KlingenandGitHub bc064bd65b docs: add npm run dx to CONTRIBUTING.md 2024-01-05 21:52:00 +01:00
Marc KlingenandGitHub 917bb8931e feat(api): get traces by tag (#820) 2024-01-05 18:39:28 +01:00
Max Deichmann 150651334c chore: release v1.29.0
CI/CD / push-docker-image (push) Blocked by required conditions
CI/CD / tests (18) (push) Failing after 2m25s
CI/CD / tests (20) (push) Failing after 2m14s
CI/CD / e2e-tests (push) Failing after 1m59s
release.yml / release (push) Failing after 2s
CI/CD / lint (push) Successful in 5m29s
CI/CD / all-ci-passed (push) Waiting to run
2024-01-05 15:02:56 +01:00
858a72aaa1 feat: add tags for traces (#801)
Co-authored-by: gitstart_bot <gitstart_bot@users.noreply.github.com>
Co-authored-by: gitstart-langfuse <gitstart-langfuse@users.noreply.github.com>
2024-01-05 13:58:13 +00:00
Marc Klingen 22134f3917 fix(ui): change icon 2024-01-05 14:39:38 +01:00
Marc Klingen 62a0d87d77 chore: add npm run dx to quickly checkout branch 2024-01-05 14:15:07 +01:00
Marc Klingen e158039639 chore: release v1.28.1
CI/CD / push-docker-image (push) Blocked by required conditions
CI/CD / tests (20) (push) Failing after 2m8s
CI/CD / tests (18) (push) Failing after 2m17s
CI/CD / e2e-tests (push) Failing after 2m6s
release.yml / release (push) Failing after 3s
CI/CD / lint (push) Successful in 5m42s
CI/CD / all-ci-passed (push) Waiting to run
2024-01-05 11:49:31 +01:00
Marc Klingen f62752dfa7 fix(ui): do not parse stringified numbers in JSON view 2024-01-05 11:48:47 +01:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2214d7d0ee chore(deps): bump the sentry group with 1 update (#814)
Bumps the sentry group with 1 update: [@sentry/nextjs](https://github.com/getsentry/sentry-javascript).


Updates `@sentry/nextjs` from 7.91.0 to 7.92.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.91.0...7.92.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-04 23:09:14 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
0c8a7e97ba chore(deps): bump core-js from 3.34.0 to 3.35.0 (#815)
Bumps [core-js](https://github.com/zloirock/core-js/tree/HEAD/packages/core-js) from 3.34.0 to 3.35.0.
- [Release notes](https://github.com/zloirock/core-js/releases)
- [Changelog](https://github.com/zloirock/core-js/blob/master/CHANGELOG.md)
- [Commits](https://github.com/zloirock/core-js/commits/v3.35.0/packages/core-js)

---
updated-dependencies:
- dependency-name: core-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>
2024-01-04 23:04:30 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
ee39eef953 chore(deps): bump @aws-sdk/client-s3 from 3.478.0 to 3.485.0 (#816)
Bumps [@aws-sdk/client-s3](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/clients/client-s3) 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/clients/client-s3/CHANGELOG.md)
- [Commits](https://github.com/aws/aws-sdk-js-v3/commits/v3.485.0/clients/client-s3)

---
updated-dependencies:
- dependency-name: "@aws-sdk/client-s3"
  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-04 23:00:11 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
8d10e66744 chore(deps): bump date-fns from 2.30.0 to 3.0.6 (#817)
Bumps [date-fns](https://github.com/date-fns/date-fns) from 2.30.0 to 3.0.6.
- [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/v2.30.0...v3.0.6)

---
updated-dependencies:
- dependency-name: date-fns
  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-04 22:55:58 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
92900672c8 chore(deps): bump lucide-react from 0.302.0 to 0.304.0 (#818)
Bumps [lucide-react](https://github.com/lucide-icons/lucide/tree/HEAD/packages/lucide-react) from 0.302.0 to 0.304.0.
- [Release notes](https://github.com/lucide-icons/lucide/releases)
- [Commits](https://github.com/lucide-icons/lucide/commits/0.304.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-04 22:52:04 +00:00
Marc Klingen e59808c1a1 feat(ui): link to github release page from version in logo 2024-01-04 23:43:17 +01:00
704ea7b6ca feat(ui): add toggle to show/hide password on sign in/up form (#796)
---------

Co-authored-by: Marc Klingen <git@marcklingen.com>
2024-01-04 22:15:42 +00:00
Marc Klingen 8aac4dbccf chore: release v1.28.0
CI/CD / push-docker-image (push) Blocked by required conditions
CI/CD / tests (18) (push) Failing after 2m22s
release.yml / release (push) Failing after 3s
CI/CD / tests (20) (push) Failing after 2m14s
CI/CD / e2e-tests (push) Failing after 2m8s
CI/CD / lint (push) Successful in 5m17s
CI/CD / all-ci-passed (push) Waiting to run
2024-01-04 22:44:39 +01:00
Marc Klingen 78676b48a9 fix(ui): margins on observation tree 2024-01-04 22:43:30 +01:00
Marc KlingenandGitHub 0077058241 chore: remove unused dependencies (#813) 2024-01-04 22:33:23 +01:00
Max DeichmannandGitHub 343b4b9ff1 refactor: add indices to models (#812) 2024-01-04 21:19:11 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
b09bc03837 chore(deps): bump the patches group with 8 updates (#811)
Bumps the patches group with 8 updates:

| Package | From | To |
| --- | --- | --- |
| [@emotion/react](https://github.com/emotion-js/emotion) | `11.11.1` | `11.11.3` |
| [@hookform/resolvers](https://github.com/react-hook-form/resolvers) | `3.3.2` | `3.3.4` |
| [@tanstack/react-table](https://github.com/tanstack/table) | `8.11.2` | `8.11.3` |
| [nodemailer](https://github.com/nodemailer/nodemailer) | `6.9.7` | `6.9.8` |
| [@types/eslint](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/eslint) | `8.56.0` | `8.56.1` |
| [@types/react](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react) | `18.2.45` | `18.2.46` |
| [postcss](https://github.com/postcss/postcss) | `8.4.32` | `8.4.33` |
| [prettier-plugin-tailwindcss](https://github.com/tailwindlabs/prettier-plugin-tailwindcss) | `0.5.9` | `0.5.10` |


Updates `@emotion/react` from 11.11.1 to 11.11.3
- [Release notes](https://github.com/emotion-js/emotion/releases)
- [Changelog](https://github.com/emotion-js/emotion/blob/main/CHANGELOG.md)
- [Commits](https://github.com/emotion-js/emotion/compare/@emotion/react@11.11.1...@emotion/react@11.11.3)

Updates `@hookform/resolvers` from 3.3.2 to 3.3.4
- [Release notes](https://github.com/react-hook-form/resolvers/releases)
- [Commits](https://github.com/react-hook-form/resolvers/compare/v3.3.2...v3.3.4)

Updates `@tanstack/react-table` from 8.11.2 to 8.11.3
- [Release notes](https://github.com/tanstack/table/releases)
- [Commits](https://github.com/tanstack/table/compare/v8.11.2...v8.11.3)

Updates `nodemailer` from 6.9.7 to 6.9.8
- [Release notes](https://github.com/nodemailer/nodemailer/releases)
- [Changelog](https://github.com/nodemailer/nodemailer/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodemailer/nodemailer/compare/v6.9.7...v6.9.8)

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

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

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

Updates `prettier-plugin-tailwindcss` from 0.5.9 to 0.5.10
- [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.9...v0.5.10)

---
updated-dependencies:
- dependency-name: "@emotion/react"
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: patches
- dependency-name: "@hookform/resolvers"
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: patches
- dependency-name: "@tanstack/react-table"
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: patches
- dependency-name: nodemailer
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: patches
- dependency-name: "@types/eslint"
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: patches
- dependency-name: "@types/react"
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: patches
- dependency-name: postcss
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: patches
- dependency-name: prettier-plugin-tailwindcss
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: patches
...

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

---
updated-dependencies:
- dependency-name: "@typescript-eslint/parser"
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-26 22:58:02 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
de4984cef6 chore(deps-dev): bump @typescript-eslint/eslint-plugin from 6.15.0 to 6.16.0 (#764)
chore(deps-dev): bump @typescript-eslint/eslint-plugin

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

---
updated-dependencies:
- dependency-name: "@typescript-eslint/eslint-plugin"
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-26 22:54:05 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
dba56de614 chore(deps): bump lucide-react from 0.300.0 to 0.302.0 (#765)
Bumps [lucide-react](https://github.com/lucide-icons/lucide/tree/HEAD/packages/lucide-react) from 0.300.0 to 0.302.0.
- [Release notes](https://github.com/lucide-icons/lucide/releases)
- [Commits](https://github.com/lucide-icons/lucide/commits/0.302.0/packages/lucide-react)

---
updated-dependencies:
- dependency-name: lucide-react
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

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


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

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-24 00:52:04 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
f72707ead5 chore(deps): bump lucide-react from 0.299.0 to 0.300.0 (#749)
Bumps [lucide-react](https://github.com/lucide-icons/lucide/tree/HEAD/packages/lucide-react) from 0.299.0 to 0.300.0.
- [Release notes](https://github.com/lucide-icons/lucide/releases)
- [Commits](https://github.com/lucide-icons/lucide/commits/0.300.0/packages/lucide-react)

---
updated-dependencies:
- dependency-name: lucide-react
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-24 01:47:03 +01:00
Marc KlingenandGitHub 963cf945cb feat(cloud): display custom plan limits in project settings (#756) 2023-12-24 01:41:21 +01:00
Marc Klingen d5aa809ff9 chore: release v1.23.0
CI/CD / push-docker-image (push) Blocked by required conditions
CI/CD / tests (18) (push) Failing after 40s
CI/CD / tests (20) (push) Failing after 34s
CI/CD / e2e-tests (push) Failing after 37s
CI/CD / lint (push) Successful in 3m58s
CI/CD / all-ci-passed (push) Waiting to run
release.yml / release (push) Failing after 3s
2023-12-22 19:36:57 +01:00
1dd9a0e04e feat(ui): display host name on project settings page (#746)
* feat(ui): added hostName on the settings page

* simplify component

---------

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

---
updated-dependencies:
- dependency-name: lucide-react
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-22 06:28:03 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
1830e0285a chore(deps): bump tailwind-merge from 2.1.0 to 2.2.0 (#741)
Bumps [tailwind-merge](https://github.com/dcastil/tailwind-merge) from 2.1.0 to 2.2.0.
- [Release notes](https://github.com/dcastil/tailwind-merge/releases)
- [Commits](https://github.com/dcastil/tailwind-merge/compare/v2.1.0...v2.2.0)

---
updated-dependencies:
- dependency-name: tailwind-merge
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-22 06:24:08 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
d31b8ebc8b chore(deps): bump @tanstack/react-table from 8.10.7 to 8.11.2 (#742)
Bumps [@tanstack/react-table](https://github.com/tanstack/table) from 8.10.7 to 8.11.2.
- [Release notes](https://github.com/tanstack/table/releases)
- [Commits](https://github.com/tanstack/table/compare/v8.10.7...v8.11.2)

---
updated-dependencies:
- dependency-name: "@tanstack/react-table"
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

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


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

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-21 01:00:19 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
c048372928 chore(deps-dev): bump @types/eslint from 8.44.9 to 8.56.0 (#731)
Bumps [@types/eslint](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/eslint) from 8.44.9 to 8.56.0.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/eslint)

---
updated-dependencies:
- dependency-name: "@types/eslint"
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-21 00:56:05 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
0271582c0d chore(deps-dev): bump tailwindcss from 3.3.6 to 3.4.0 (#733)
Bumps [tailwindcss](https://github.com/tailwindlabs/tailwindcss) from 3.3.6 to 3.4.0.
- [Release notes](https://github.com/tailwindlabs/tailwindcss/releases)
- [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/master/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/tailwindcss/compare/v3.3.6...v3.4.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-21 01:51:49 +01:00
Marc KlingenandGitHub a4b71e0805 fix: remove console log 2023-12-20 14:33:01 +01:00
Max DeichmannandGitHub 66ac595850 style: add comment on filter state (#729) 2023-12-20 13:16:15 +00:00
187 changed files with 8918 additions and 2144 deletions
+8
View File
@@ -7,6 +7,7 @@ NEXTAUTH_COOKIE_DOMAIN=".langfuse.com"
# https://www.prisma.io/docs/reference/database-reference/connection-urls#env
DIRECT_URL="postgresql://postgres:[pw]@db.[db_id].supabase.co:5432/postgres"
DATABASE_URL="postgres://postgres:[pw]@db.[db_id].supabase.co:6543/postgres?pgbouncer=true&connection_limit=1"
# SHADOW_DATABASE_URL=
# Next Auth
@@ -48,3 +49,10 @@ AUTH_GITHUB_CLIENT_SECRET=
# Email
EMAIL_FROM_ADDRESS="" # Defines the email address to use as the from address.
SMTP_CONNECTION_URL="" # Defines the connection url for smtp server.
# S3 storage (eg for exports), necessary for serverless deployments
S3_ENDPOINT=""
S3_ACCESS_KEY_ID=""
S3_SECRET_ACCESS_KEY=""
S3_BUCKET_NAME=""
S3_REGION=""
+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
+75 -20
View File
@@ -21,36 +21,86 @@ _Before making any significant changes, please [open an issue](https://github.co
Once we've discussed your changes and you've got your code ready, make sure that tests are passing and open your pull request.
## Getting started
A good first step is to search for open [issues](https://github.com/langfuse/langfuse/issues). Issues are labeled, and some good issues to start with are labeled: [good first issue](https://github.com/langfuse/langfuse/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22).
## Project Overview
### Technologies we use
- NextJS, pages router
- NextAuth.js / Auth.js
- tRPC
- Prisma ORM
- Zod
- Tailwind CSS
- shadcn/ui tailwind components (using Radix and tanstack)
- Application (this repository)
- NextJS 14, pages router
- NextAuth.js / Auth.js
- tRPC: Frontend APIs
- Prisma ORM
- Zod
- Tailwind CSS
- shadcn/ui tailwind components (using Radix and tanstack)
- Fern: generate OpenAPI spec and Pydantic models
- JS SDK ([langfuse/langfuse-js](https://github.com/langfuse/langfuse-js))
- openapi-typescript to generated types based on OpenAPI spec
- Python SDK ([langfuse/langfuse-python](https://github.com/langfuse/langfuse-python))
- Pydantic for input validation, models generated by fern
### Architecture overview
### Architecture Overview
```mermaid
flowchart TD
flowchart TB
subgraph s4["Clients"]
subgraph s2["langfuse/langfuse-python"]
Python["Python SDK"]
OAI["OpenAI drop-in replacement"] -->|extends| Python
LCPYTHON["Langchain Python Integration"] -->|extends| Python
Langflow -->|uses| LCPYTHON
LiteLLM -->|uses| Python
end
subgraph s3["langfuse/langfuse-js"]
JS["JS SDK"]
LCJS["Langchain JS Integration"] -->|extends| JS
Flowise -->|uses| LCJS
end
end
A[Python SDK - Backend] -->|REST| C[NextJs Server]
B[JS/TS SDK - Frontend + Backend] -->|REST| C
C <-->|Prisma| D[Postgres Database]
E[Langfuse UI] -->|tRPC| C
DB[Postgres Database]
subgraph s1["Application (langfuse/langfuse)"]
API[Public HTTP API]
G[TRPC API]
I[NextAuth]
H[React Frontend]
Prisma[Prisma ORM]
H --> G
H --> I
G --> I
G --- Prisma
API --- Prisma
I --- Prisma
end
Prisma --- DB
JS --- API
Python --- API
```
| Component | Technology | Description |
| --------------------------------------------------------- | --------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [Python SDK](https://github.com/langfuse/langfuse-python) | Python | Fully async SDK. Events are sent to server in batches from a background thread. |
| [JS/TS SDK](https://github.com/langfuse/langfuse-js) | TypeScript | Fully async SDK. As of SDK version 2.0.0, events will be sent in batches to the server. |
| NextJs Server (JS) | TypeScript, [T3 Stack](https://create.t3.gg/) | We use Vercel Cloud Functions to serve our back-end and front-end. The front-end uses tRPC to communicate with the back-end. For self-hosting, this component is available as Docker container. |
### 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
flowchart LR
Browser ---|Web UI & TRPC API| App
Integrations/SDKs ---|Public HTTP API| App
subgraph i1["Application Network"]
App["Langfuse Application (Docker or Serverless)"]
end
subgraph i2["Database Network"]
DB["Postgres Database"]
end
App --- DB
```
## Development Setup
@@ -96,6 +146,9 @@ Requirements
npm run dev
```
> [!NOTE]
> If you frequently switch branches, use `npm run dx` instead of `npm run dev`. This command will install dependencies, reset the database (wipe and apply all migrations), and run the database seeder with example data before starting the development server.
## Commit messages
On the main branch, we adhere to the best practices of [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/). All pull requests and branches are squash-merged to maintain a clean and readable history. This approach ensures the addition of a conventional commit message when merging contributions.
@@ -153,3 +206,5 @@ When a new release is tagged on the `main` branch (excluding prereleases), it tr
## License
Langfuse is MIT licensed, except for `ee/` folder. See [LICENSE](LICENSE) and [docs](https://langfuse.com/docs/open-source) for more details.
When contributing to the Langfuse codebase, you need to agree to the [Contributor License Agreement](https://cla-assistant.io/langfuse/langfuse). You only need to do this once and the CLA bot will remind you if you haven't signed it yet.
+4 -2
View File
@@ -13,8 +13,10 @@ if [ -z "$DATABASE_URL" ]; then
fi
fi
# Set DIRECT_URL to the value of DATABASE_URL, required for migrations
export DIRECT_URL=$DATABASE_URL
# Set DIRECT_URL to the value of DATABASE_URL if it is not set, required for migrations
if [ -z "$DIRECT_URL" ]; then
export DIRECT_URL=$DATABASE_URL
fi
# Apply migrations
prisma migrate deploy
+6
View File
@@ -1,4 +1,10 @@
name: langfuse
docs: |
## Authentication
Authenticate with the API using Basic Auth, get API keys in the project settings:
- username: Langfuse Public Key
- password: Langfuse Secret Key
error-discrimination:
strategy: status-code
auth: basic
+3 -1
View File
@@ -14,6 +14,7 @@ types:
version: optional<string>
userId: optional<string>
metadata: optional<unknown>
tags: optional<list<string>>
public:
type: optional<boolean>
docs: Public traces are accessible via url without login
@@ -59,6 +60,7 @@ types:
level: ObservationLevel
statusMessage: optional<string>
parentObservationId: optional<string>
promptId: optional<string>
Usage:
properties:
input: optional<integer>
@@ -78,7 +80,6 @@ types:
properties:
id: string
name: string
status: DatasetStatus
projectId: string
createdAt: datetime
updatedAt: datetime
@@ -128,6 +129,7 @@ types:
- optional<string>
- optional<integer>
- optional<boolean>
- optional<list<string>>
DatasetStatus:
enum:
- ACTIVE
+13
View File
@@ -7,9 +7,22 @@ service:
docs: Check health of API and database
method: GET
path: /health
response: HealthResponse
errors:
- ServiceUnavailableError
types:
HealthResponse:
properties:
version:
type: string
docs: Langfuse server version
status: string
examples:
- value:
version: 1.25.0
status: OK
errors:
ServiceUnavailableError:
status-code: 503
+15
View File
@@ -28,6 +28,7 @@ types:
generation-update: UpdateGenerationEvent
span-create: CreateSpanEvent
span-update: UpdateSpanEvent
sdk-log: SDKLogEvent
# both are legacy
observation-create: CreateObservationEvent
@@ -91,6 +92,8 @@ types:
model: optional<string>
modelParameters: optional<map<string, commons.MapValue>>
usage: optional<IngestionUsage>
promptName: optional<string>
promptVersion: optional<integer>
UpdateGenerationBody:
extends: UpdateSpanBody
@@ -99,6 +102,8 @@ types:
model: optional<string>
modelParameters: optional<map<string, commons.MapValue>>
usage: optional<IngestionUsage>
promptName: optional<string>
promptVersion: optional<integer>
ObservationBody:
properties:
@@ -131,10 +136,15 @@ types:
release: optional<string>
version: optional<string>
metadata: optional<unknown>
tags: optional<list<string>>
public:
type: optional<boolean>
docs: Make trace publicly accessible via url
SDKLogBody:
properties:
log: unknown
ScoreBody:
properties:
id: optional<string>
@@ -170,6 +180,11 @@ types:
properties:
body: ScoreBody
SDKLogEvent:
extends: BaseEvent
properties:
body: SDKLogBody
CreateGenerationEvent:
extends: BaseEvent
properties:
+36
View File
@@ -0,0 +1,36 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/fern-api/fern/main/fern.schema.json
imports:
commons: ./commons.yml
pagination: ./utils/pagination.yml
service:
auth: true
base-path: /api/public
endpoints:
get:
docs: Get a specific prompt
method: GET
path: /prompts
request:
name: GetParameterRequest
query-parameters:
name: string
version: optional<integer>
response: Prompt
create:
docs: Create a specific prompt
method: POST
path: /prompts
request: CreatePromptRequest
response: Prompt
types:
CreatePromptRequest:
properties:
name: string
isActive: boolean
prompt: string
Prompt:
properties:
name: string
version: integer
prompt: string
+10
View File
@@ -26,6 +26,13 @@ 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
docs: Only traces that include all of these tags will be returned.
response: Traces
types:
@@ -33,3 +40,6 @@ types:
properties:
data: list<commons.TraceWithDetails>
meta: pagination.MetaResponse
Sort:
properties:
id: string
+1 -1
View File
@@ -1,4 +1,4 @@
{
"organization": "finto",
"version": "0.16.7"
"version": "0.16.22"
}
+236 -4
View File
@@ -2,6 +2,17 @@ openapi: 3.0.1
info:
title: langfuse
version: ''
description: >-
## Authentication
Authenticate with the API using Basic Auth, get API keys in the project
settings:
- username: Langfuse Public Key
- password: Langfuse Secret Key
paths:
/api/public/dataset-items:
post:
@@ -290,8 +301,12 @@ paths:
- Health
parameters: []
responses:
'204':
'200':
description: ''
content:
application/json:
schema:
$ref: '#/components/schemas/HealthResponse'
'400':
description: ''
content:
@@ -541,6 +556,102 @@ paths:
application/json:
schema: {}
security: *ref_0
/api/public/prompts:
get:
description: Get a specific prompt
operationId: prompts_get
tags:
- Prompts
parameters:
- name: name
in: query
required: true
schema:
type: string
- name: version
in: query
required: false
schema:
type: integer
nullable: true
responses:
'200':
description: ''
content:
application/json:
schema:
$ref: '#/components/schemas/Prompt'
'400':
description: ''
content:
application/json:
schema: {}
'401':
description: ''
content:
application/json:
schema: {}
'403':
description: ''
content:
application/json:
schema: {}
'404':
description: ''
content:
application/json:
schema: {}
'405':
description: ''
content:
application/json:
schema: {}
security: *ref_0
post:
description: Create a specific prompt
operationId: prompts_create
tags:
- Prompts
parameters: []
responses:
'200':
description: ''
content:
application/json:
schema:
$ref: '#/components/schemas/Prompt'
'400':
description: ''
content:
application/json:
schema: {}
'401':
description: ''
content:
application/json:
schema: {}
'403':
description: ''
content:
application/json:
schema: {}
'404':
description: ''
content:
application/json:
schema: {}
'405':
description: ''
content:
application/json:
schema: {}
security: *ref_0
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CreatePromptRequest'
/api/public/scores:
post:
description: Add a score to the database, upserts on id
@@ -773,6 +884,23 @@ 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.
required: false
schema:
type: array
items:
type: string
nullable: true
responses:
'200':
description: ''
@@ -839,6 +967,11 @@ components:
nullable: true
metadata:
nullable: true
tags:
type: array
items:
type: string
nullable: true
public:
type: boolean
nullable: true
@@ -962,6 +1095,9 @@ components:
parentObservationId:
type: string
nullable: true
promptId:
type: string
nullable: true
required:
- id
- type
@@ -1019,8 +1155,6 @@ components:
type: string
name:
type: string
status:
$ref: '#/components/schemas/DatasetStatus'
projectId:
type: string
createdAt:
@@ -1040,7 +1174,6 @@ components:
required:
- id
- name
- status
- projectId
- createdAt
- updatedAt
@@ -1150,6 +1283,10 @@ components:
nullable: true
- type: boolean
nullable: true
- type: array
items:
type: string
nullable: true
DatasetStatus:
title: DatasetStatus
type: string
@@ -1193,6 +1330,20 @@ components:
type: string
required:
- name
HealthResponse:
title: HealthResponse
type: object
properties:
version:
type: string
description: Langfuse server version
example: 1.25.0
status:
type: string
example: OK
required:
- version
- status
IngestionEvent:
title: IngestionEvent
oneOf:
@@ -1273,6 +1424,17 @@ components:
- $ref: '#/components/schemas/UpdateSpanEvent'
required:
- type
- type: object
allOf:
- type: object
properties:
type:
type: string
enum:
- sdk-log
- $ref: '#/components/schemas/SDKLogEvent'
required:
- type
- type: object
allOf:
- type: object
@@ -1410,6 +1572,12 @@ components:
usage:
$ref: '#/components/schemas/IngestionUsage'
nullable: true
promptName:
type: string
nullable: true
promptVersion:
type: integer
nullable: true
allOf:
- $ref: '#/components/schemas/CreateSpanBody'
UpdateGenerationBody:
@@ -1431,6 +1599,12 @@ components:
usage:
$ref: '#/components/schemas/IngestionUsage'
nullable: true
promptName:
type: string
nullable: true
promptVersion:
type: integer
nullable: true
allOf:
- $ref: '#/components/schemas/UpdateSpanBody'
ObservationBody:
@@ -1519,10 +1693,22 @@ components:
nullable: true
metadata:
nullable: true
tags:
type: array
items:
type: string
nullable: true
public:
type: boolean
nullable: true
description: Make trace publicly accessible via url
SDKLogBody:
title: SDKLogBody
type: object
properties:
log: {}
required:
- log
ScoreBody:
title: ScoreBody
type: object
@@ -1600,6 +1786,16 @@ components:
- body
allOf:
- $ref: '#/components/schemas/BaseEvent'
SDKLogEvent:
title: SDKLogEvent
type: object
properties:
body:
$ref: '#/components/schemas/SDKLogBody'
required:
- body
allOf:
- $ref: '#/components/schemas/BaseEvent'
CreateGenerationEvent:
title: CreateGenerationEvent
type: object
@@ -1726,6 +1922,34 @@ components:
required:
- id
- name
CreatePromptRequest:
title: CreatePromptRequest
type: object
properties:
name:
type: string
isActive:
type: boolean
prompt:
type: string
required:
- name
- isActive
- prompt
Prompt:
title: Prompt
type: object
properties:
name:
type: string
version:
type: integer
prompt:
type: string
required:
- name
- version
- prompt
CreateScoreRequest:
title: CreateScoreRequest
type: object
@@ -1776,6 +2000,14 @@ components:
required:
- data
- meta
Sort:
title: Sort
type: object
properties:
id:
type: string
required:
- id
utilsMetaResponse:
title: utilsMetaResponse
type: object
+89 -3
View File
@@ -2,7 +2,7 @@
"info": {
"name": "Langfuse",
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json",
"description": null
"description": "## Authentication\n\nAuthenticate with the API using Basic Auth, get API keys in the project settings:\n\n- username: Langfuse Public Key\n- password: Langfuse Secret Key"
},
"variable": [
{
@@ -320,7 +320,7 @@
"auth": null,
"body": {
"mode": "raw",
"raw": "{\n \"batch\": [\n {\n \"type\": \"trace-create\",\n \"body\": {\n \"id\": \"example\",\n \"name\": \"example\",\n \"userId\": \"example\",\n \"input\": \"UNKNOWN\",\n \"output\": \"UNKNOWN\",\n \"sessionId\": \"example\",\n \"release\": \"example\",\n \"version\": \"example\",\n \"metadata\": \"UNKNOWN\",\n \"public\": true\n },\n \"id\": \"example\",\n \"timestamp\": \"example\",\n \"metadata\": \"UNKNOWN\"\n }\n ]\n}",
"raw": "{\n \"batch\": [\n {\n \"type\": \"trace-create\",\n \"body\": {\n \"id\": \"example\",\n \"name\": \"example\",\n \"userId\": \"example\",\n \"input\": \"UNKNOWN\",\n \"output\": \"UNKNOWN\",\n \"sessionId\": \"example\",\n \"release\": \"example\",\n \"version\": \"example\",\n \"metadata\": \"UNKNOWN\",\n \"tags\": [\n \"example\"\n ],\n \"public\": true\n },\n \"id\": \"example\",\n \"timestamp\": \"example\",\n \"metadata\": \"UNKNOWN\"\n }\n ]\n}",
"options": {
"raw": {
"language": "json"
@@ -464,6 +464,82 @@
}
]
},
{
"_type": "container",
"description": null,
"name": "Prompts",
"item": [
{
"_type": "endpoint",
"name": "Get",
"request": {
"description": "Get a specific prompt",
"url": {
"raw": "{{baseUrl}}/api/public/prompts?name=&version=",
"host": [
"{{baseUrl}}"
],
"path": [
"api",
"public",
"prompts"
],
"query": [
{
"key": "name",
"value": "",
"description": null
},
{
"key": "version",
"value": "",
"description": null
}
],
"variable": []
},
"header": [],
"method": "GET",
"auth": null,
"body": null
},
"response": []
},
{
"_type": "endpoint",
"name": "Create",
"request": {
"description": "Create a specific prompt",
"url": {
"raw": "{{baseUrl}}/api/public/prompts",
"host": [
"{{baseUrl}}"
],
"path": [
"api",
"public",
"prompts"
],
"query": [],
"variable": []
},
"header": [],
"method": "POST",
"auth": null,
"body": {
"mode": "raw",
"raw": "{\n \"name\": \"example\",\n \"isActive\": true,\n \"prompt\": \"example\"\n}",
"options": {
"raw": {
"language": "json"
}
}
}
},
"response": []
}
]
},
{
"_type": "container",
"description": null,
@@ -632,7 +708,7 @@
"request": {
"description": "Get list of traces",
"url": {
"raw": "{{baseUrl}}/api/public/traces?page=&limit=&userId=&name=",
"raw": "{{baseUrl}}/api/public/traces?page=&limit=&userId=&name=&orderBy=&tags=",
"host": [
"{{baseUrl}}"
],
@@ -661,6 +737,16 @@
"key": "name",
"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": "",
"description": "Only traces that include all of these tags will be returned."
}
],
"variable": []
+6 -1
View File
@@ -18,6 +18,8 @@ const serverTestConfig = {
testEnvironment: "jest-environment-node",
};
// To avoid the "Cannot use import statement outside a module" errors while transforming ESM.
const esModules = ["superjson"];
// Add any custom config to be passed to Jest
/** @type {import('jest').Config} */
const config = {
@@ -26,7 +28,10 @@ const config = {
verbose: true,
projects: [
await createJestConfig(clientTestConfig)(),
await createJestConfig(serverTestConfig)(),
{
...(await createJestConfig(serverTestConfig)()),
transformIgnorePatterns: [`/node_modules/(?!(${esModules.join("|")})/)`],
},
],
};
+2906 -909
View File
File diff suppressed because it is too large Load Diff
+42 -36
View File
@@ -1,11 +1,12 @@
{
"name": "langfuse-core",
"version": "1.19.4",
"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",
"build": "next build",
"dev": "next dev",
"dx": "npm i && npm run db:reset && npm run db:seed:examples && npm run dev",
"postinstall": "prisma generate",
"lint": "next lint",
"lint:fix": "next lint --fix",
@@ -15,7 +16,8 @@
"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",
"release": "release-it"
@@ -25,14 +27,15 @@
},
"dependencies": {
"@anthropic-ai/tokenizer": "^0.0.4",
"@emotion/react": "^11.11.1",
"@emotion/styled": "^11.11.0",
"@formkit/auto-animate": "^0.8.1",
"@aws-sdk/client-s3": "^3.485.0",
"@aws-sdk/s3-request-presigner": "^3.485.0",
"@headlessui/react": "^1.7.17",
"@heroicons/react": "^2.1.1",
"@hookform/resolvers": "^3.3.2",
"@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",
"@radix-ui/react-checkbox": "^1.0.4",
"@radix-ui/react-collapsible": "^1.0.3",
@@ -42,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",
@@ -52,86 +56,83 @@
"@radix-ui/react-tooltip": "^1.0.7",
"@react-email/components": "^0.0.12",
"@react-email/render": "^0.0.10",
"@sentry/nextjs": "^7.88.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",
"@tailwindcss/line-clamp": "^0.4.4",
"@tanstack/react-query": "^4.36.1",
"@tanstack/react-table": "^8.10.7",
"@tanstack/react-table": "^8.11.3",
"@tremor/react": "^3.11.1",
"@trpc/client": "^10.44.1",
"@trpc/next": "^10.44.1",
"@trpc/react-query": "^10.44.1",
"@trpc/server": "^10.44.1",
"@vercel/edge-config": "^0.4.1",
"axios": "^1.6.2",
"bcryptjs": "^2.4.3",
"class-variance-authority": "^0.7.0",
"clsx": "^2.0.0",
"cmdk": "^0.2.0",
"core-js": "^3.35.0",
"cors": "^2.8.5",
"date-fns": "^2.30.0",
"date-fns": "^3.3.1",
"decimal.js": "^10.4.3",
"exponential-backoff": "^3.1.1",
"js-yaml": "^4.1.0",
"lodash": "^4.17.21",
"lucide-react": "^0.298.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.7",
"posthog-js": "^1.96.1",
"nodemailer": "^6.9.8",
"posthog-js": "^1.100.0",
"posthog-node": "^3.2.1",
"react": "18.2.0",
"react-day-picker": "^8.9.1",
"react-day-picker": "^8.10.0",
"react-dom": "18.2.0",
"react-hook-form": "^7.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",
"superagent": "^8.1.2",
"superjson": "2.2.1",
"tailwind-merge": "^2.1.0",
"tailwind-merge": "^2.2.0",
"tailwindcss-animate": "^1.0.7",
"tiktoken": "^1.0.11",
"ts-pattern": "^5.0.6",
"use-query-params": "^2.2.1",
"uuid": "^9.0.1",
"zod": "^3.22.4"
},
"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",
"@testing-library/react": "^14.1.2",
"@types/bcryptjs": "^2.4.6",
"@types/cors": "^2.8.17",
"@types/eslint": "^8.44.9",
"@types/eslint": "^8.56.1",
"@types/jest": "^29.5.11",
"@types/js-yaml": "^4.0.9",
"@types/lodash": "^4.14.202",
"@types/node": "20.10.5",
"@types/nodemailer": "^6.4.14",
"@types/react": "^18.2.45",
"@types/react": "^18.2.46",
"@types/react-dom": "^18.2.18",
"@types/swagger-ui-react": "^4.18.3",
"@types/uuid": "^9.0.7",
"@typescript-eslint/eslint-plugin": "^6.14.0",
"@typescript-eslint/parser": "^6.15.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.32",
"postcss": "^8.4.33",
"prettier": "^3.1.1",
"prettier-plugin-tailwindcss": "^0.5.9",
"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",
"swagger-jsdoc": "^6.2.8",
"tailwindcss": "^3.3.6",
"tailwindcss": "^3.4.1",
"ts-node": "^10.9.2",
"tsconfig-paths": "^4.2.0",
"typescript": "^5.3.3"
@@ -151,7 +152,12 @@
"release": true,
"web": true,
"autoGenerate": true,
"releaseName": "v${version}"
"releaseName": "v${version}",
"comments": {
"submit": true,
"issue": ":rocket: _This issue has been resolved in v${version}. See [${releaseName}](${releaseUrl}) for release notes._",
"pr": ":rocket: _This pull request is included in v${version}. See [${releaseName}](${releaseUrl}) for release notes._"
}
},
"plugins": {
"@release-it/bumper": {
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 194 KiB

@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "projects" ADD COLUMN "cloud_config" JSONB;
@@ -0,0 +1,3 @@
-- AlterTable
ALTER TABLE "Account" ADD COLUMN "expires_in" INTEGER,
ADD COLUMN "ext_expires_in" INTEGER;
@@ -0,0 +1,29 @@
-- AlterTable
ALTER TABLE "observations" ADD COLUMN "prompt_id" TEXT;
-- CreateTable
CREATE TABLE "prompts" (
"id" TEXT NOT NULL,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"project_id" TEXT NOT NULL,
"created_by" TEXT NOT NULL,
"prompt" TEXT NOT NULL,
"name" TEXT NOT NULL,
"version" INTEGER NOT NULL,
"is_active" BOOLEAN NOT NULL,
CONSTRAINT "prompts_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE INDEX "prompts_project_id_name_version_idx" ON "prompts"("project_id", "name", "version");
-- CreateIndex
CREATE UNIQUE INDEX "prompts_project_id_name_version_key" ON "prompts"("project_id", "name", "version");
-- AddForeignKey
ALTER TABLE "observations" ADD CONSTRAINT "observations_prompt_id_fkey" FOREIGN KEY ("prompt_id") REFERENCES "prompts"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "prompts" ADD CONSTRAINT "prompts_project_id_fkey" FOREIGN KEY ("project_id") REFERENCES "projects"("id") ON DELETE CASCADE ON UPDATE CASCADE;
@@ -0,0 +1,30 @@
-- Migration to add gpt-3.5-turbo-1106 and gpt-3.5-turbo-instruct models
INSERT INTO pricings (
id,
model_name,
pricing_unit,
price,
currency,
token_type
)
VALUES
('clqqpc2pr000008l3hvy63gxy','gpt-3.5-turbo-1106', 'PER_1000_TOKENS', 0.001, 'USD', 'PROMPT'),
('clqqpcb6d000208l3atrfbmou','gpt-3.5-turbo-1106', 'PER_1000_TOKENS', 0.002, 'USD', 'COMPLETION'),
('clqqpdh45000008lfgrnx76cv','gpt-3.5-turbo-instruct', 'PER_1000_TOKENS', 0.0015, 'USD', 'PROMPT'),
('clqqpdjya000108lf3s4b4c4m','gpt-3.5-turbo-instruct', 'PER_1000_TOKENS', 0.002, 'USD', 'COMPLETION'),
('clnnuiuq6000008l4dxp43wc6', 'claude-instant-1.2', 'PER_1000_TOKENS', 0.00163, 'USD', 'PROMPT'),
('clnnujlmj000108l48wne4ii9', 'claude-instant-1.2', 'PER_1000_TOKENS', 0.00551, 'USD', 'COMPLETION'),
('clnnukutg000208l49qqt9lyr', 'claude-instant-1.1', 'PER_1000_TOKENS', 0.00163, 'USD', 'PROMPT'),
('clnnun3x3000408l490qz9uv0', 'claude-instant-1.1', 'PER_1000_TOKENS', 0.00551, 'USD', 'COMPLETION'),
('clnnuosvy000508l4gsjy2pp4', 'claude-2.0', 'PER_1000_TOKENS', 0.01102, 'USD', 'PROMPT'),
('clnnuqcns000608l40qaz8trt', 'claude-2.0', 'PER_1000_TOKENS', 0.03268, 'USD', 'COMPLETION'),
('clnnuuif8000808l4gal4fjq4', 'claude-1.0', 'PER_1000_TOKENS', 0.01102, 'USD', 'PROMPT'),
('clnnutptp000708l47r091vvd', 'claude-1.0', 'PER_1000_TOKENS', 0.03268, 'USD', 'COMPLETION'),
('clnon8riv000308mlgfr1agiv', 'text-embedding-ada-002', 'PER_1000_TOKENS', 0.0001, 'USD', 'PROMPT'),
('clnon9kfz000408ml1bg81o6z', 'text-embedding-ada-002', 'PER_1000_TOKENS', 0.0001, 'USD', 'COMPLETION'),
('clqwniv8a000d08l2frjl7mmw', 'codechat-bison-32k', 'PER_1000_CHARS', 0.0005, 'USD', 'PROMPT'),
('clqwnj044000e08l2dfjm5g90', 'chat-bison-32k', 'PER_1000_CHARS', 0.0005, 'USD', 'PROMPT'),
('clqwnj47r000f08l2a16fekls', 'chat-bison-32k', 'PER_1000_CHARS', 0.0005, 'USD', 'COMPLETION'),
('clqwnj863000g08l2bwxgdapm', 'codechat-bison-32k', 'PER_1000_CHARS', 0.0005, 'USD', 'COMPLETION')
ON CONFLICT (id) DO NOTHING;
@@ -0,0 +1,3 @@
-- CreateIndex
CREATE INDEX CONCURRENTLY "observations_model_idx" ON "observations"("model");
@@ -0,0 +1,2 @@
-- CreateIndex
CREATE INDEX CONCURRENTLY "pricings_model_name_idx" ON "pricings"("model_name");
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "traces" ADD COLUMN "tags" TEXT[] DEFAULT ARRAY[]::TEXT[];
@@ -0,0 +1,2 @@
-- CreateIndex
CREATE INDEX "traces_tags_idx" ON "traces" USING GIN ("tags" array_ops);
@@ -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";
+48 -9
View File
@@ -7,9 +7,18 @@ generator client {
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
directUrl = env("DIRECT_URL")
provider = "postgresql"
url = env("DATABASE_URL")
directUrl = env("DIRECT_URL")
shadowDatabaseUrl = env("SHADOW_DATABASE_URL")
}
generator erd {
provider = "prisma-erd-generator"
ignoreTables = ["_prisma_migrations", "Session", "Account", "Example"]
disabled = true
ignoreEnums = true
output = "database.svg"
}
model Example {
@@ -28,6 +37,8 @@ model Account {
refresh_token String? // @db.Text
access_token String? // @db.Text
expires_at Int?
expires_in Int?
ext_expires_in Int?
token_type String?
scope String?
id_token String? // @db.Text
@@ -79,6 +90,7 @@ model Project {
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
name String
cloudConfig Json? @map("cloud_config") // Langfuse Cloud, for zod schema see projectsRouter.ts
members Membership[]
traces Trace[]
observations Observation[]
@@ -87,6 +99,7 @@ model Project {
RawEvents Events[]
invitations MembershipInvitation[]
sessions TraceSession[]
Prompt Prompt[]
@@map("projects")
}
@@ -173,18 +186,18 @@ model Trace {
metadata Json?
release String?
version String?
input Json?
output Json?
sessionId String? @map("session_id")
session TraceSession? @relation(fields: [sessionId], references: [id], onDelete: SetNull)
projectId String @map("project_id")
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
public Boolean @default(false)
bookmarked Boolean @default(false)
tags String[] @default([])
input Json?
output Json?
sessionId String? @map("session_id")
session TraceSession? @relation(fields: [sessionId], references: [id], onDelete: SetNull)
scores Score[]
@@unique([projectId, externalId])
@@index([projectId])
@@index([sessionId])
@@index([name])
@@ -192,6 +205,7 @@ model Trace {
@@index([externalId])
@@index(timestamp)
@@index(release)
@@index([tags(ops: ArrayOps)], type: Gin)
@@map("traces")
}
@@ -225,6 +239,9 @@ model Observation {
derivedDatasetItems DatasetItem[]
datasetRunItems DatasetRunItems[]
promptId String? @map("prompt_id")
prompt Prompt? @relation(fields: [promptId], onDelete: SetNull, references: [id])
@@unique([id, projectId])
@@index([traceId])
@@index([type])
@@ -232,6 +249,7 @@ model Observation {
@@index(createdAt)
@@index(projectId)
@@index(parentObservationId)
@@index(model)
@@map("observations")
}
@@ -285,6 +303,7 @@ model Pricing {
currency String @default("USD")
tokenType TokenType @map("token_type")
@@index(modelName)
@@map("pricings")
}
@@ -300,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")
@@ -381,3 +399,24 @@ model Events {
@@index(projectId)
@@map("events")
}
model Prompt {
id String @id @default(cuid())
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
projectId String @map("project_id")
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
createdBy String @map("created_by")
prompt String
name String
version Int
isActive Boolean @map("is_active")
Observation Observation[]
@@unique([projectId, name, version])
@@index([projectId, name, version])
@@map("prompts")
}
+78 -39
View File
@@ -53,6 +53,25 @@ async function main() {
},
});
const prompt = await prisma.prompt.upsert({
where: {
projectId_name_version: {
projectId: seedProjectId,
name: "summary-prompt",
version: 1,
},
},
create: {
name: "summary-prompt",
project: { connect: { id: seedProjectId } },
prompt: "prompt {{variable}} {{anotherVariable}}",
isActive: true,
version: 1,
createdBy: "user-1",
},
update: {},
});
const seedApiKey = {
id: "seed-api-key",
secret: process.env.SEED_SECRET_KEY ?? "sk-lf-1234567890",
@@ -103,13 +122,15 @@ async function main() {
});
const generationIds: string[] = [];
const envTags = [null, "development", "staging", "production"];
const colorTags = [null, "red", "blue", "yellow"];
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(
@@ -117,6 +138,11 @@ async function main() {
Math.floor(Math.random() ** 1.5 * 90 * 24 * 60 * 60 * 1000),
);
const envTag = envTags[Math.floor(Math.random() * envTags.length)];
const colorTag = colorTags[Math.floor(Math.random() * colorTags.length)];
const tags = [envTag, colorTag].filter((tag) => tag !== null);
const trace = await prisma.trace.create({
data: {
id: `trace-${Math.floor(Math.random() * 1000000000)}`,
@@ -127,6 +153,7 @@ async function main() {
metadata: {
user: `user-${i}@langfuse.com`,
},
tags: tags as string[],
project: {
connect: {
id: [project1.id, project2.id][i % 2],
@@ -326,6 +353,11 @@ async function main() {
totalTokens: promptTokens + completionTokens,
parentObservationId: span.id,
traceId: trace.id,
...{
...(Math.random() > 0.5 && trace.projectId === prompt.projectId
? { prompt: { connect: { id: prompt.id } } }
: {}),
},
},
});
if (Math.random() > 0.6)
@@ -377,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.8,
tracesSampleRate: 0.3,
profilesSampleRate: 0.8, // 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";
@@ -0,0 +1,110 @@
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/no-unsafe-call */
import { cleanup, render } from "@testing-library/react";
import { TestRouter } from "@/src/__tests__/fixtures/TestRouter";
import { useOrderByState } from "@/src/features/orderBy/hooks/useOrderByState";
import { LocationMock } from "@jedmao/location";
import NextAdapterPages from "next-query-params/pages";
import { useRouter } from "next/router";
import { QueryParamProvider } from "use-query-params";
afterEach(cleanup);
jest.mock("next/router", () => ({
useRouter: jest.fn(),
}));
const { location: savedLocation } = window;
// The test for the useOrderByState hook
describe("useOrderByState hook", () => {
let testRouter: TestRouter;
let locationMock: LocationMock;
beforeAll(() => {
// @ts-expect-error - TS only allows to delete optional params; but here we want to delete it
// as it will be set in beforeEach
delete window.location;
});
beforeEach(() => {
jest.resetAllMocks();
locationMock = new LocationMock("https://langfuse.com");
testRouter = new TestRouter(locationMock);
window.location = locationMock;
(useRouter as jest.Mock).mockReturnValue(testRouter);
});
afterAll(() => {
window.location = savedLocation;
});
test("orderBy takes the default value if no url param is given", () => {
const result = render(
<QueryParamProvider adapter={NextAdapterPages}>
<UseOrderByStateExample />
</QueryParamProvider>,
);
expect(result.queryByText(/column Column 1, order DESC/)).toBeTruthy();
});
test("setOrderBy updates orderBy query param", () => {
const result = render(
<QueryParamProvider adapter={NextAdapterPages}>
<UseOrderByStateExample />
</QueryParamProvider>,
);
result.queryByText(/Set Order/)!.click();
result.rerender(
<QueryParamProvider adapter={NextAdapterPages}>
<UseOrderByStateExample />
</QueryParamProvider>,
);
expect(result.queryByText(/column Column 2, order ASC/)).toBeTruthy();
expect(locationMock.searchParams.get("orderBy")).toBe(
"column-Column 2_order-ASC",
);
});
test("orderBy reads the given param from the url", () => {
locationMock.replace(
"https://langfuse.com?orderBy=column-Column 3_order-DESC",
);
const result = render(
<QueryParamProvider adapter={NextAdapterPages}>
<UseOrderByStateExample />
</QueryParamProvider>,
);
expect(result.queryByText(/column Column 3, order DESC/)).toBeTruthy();
});
});
const UseOrderByStateExample = () => {
const [orderBy, setOrderBy] = useOrderByState({
column: "Column 1",
order: "DESC",
});
return (
<>
<h1>{`column ${orderBy?.column}, order ${orderBy?.order}`}</h1>
<button
onClick={() => {
setOrderBy({
column: "Column 2",
order: "ASC",
});
}}
>
Set Order
</button>
</>
);
};
@@ -0,0 +1,36 @@
import { orderByToPrismaSql } from "@/src/features/orderBy/server/orderByToPrisma";
import { tracesTableCols } from "@/src/server/api/definitions/tracesTable";
import { Prisma } from "@prisma/client";
// The test for the orderByToPrisma function
describe("orderByToPrisma (Convert orderBy to Prisma.sql)", () => {
test("orderByToPrisma returns default sql when orderBy=null", () => {
expect(orderByToPrismaSql(null, tracesTableCols)).toStrictEqual(
Prisma.sql`ORDER BY t.timestamp DESC`,
);
});
test("orderByToPrisma returns correct clause for orderBy column included in column defs", () => {
expect(
orderByToPrismaSql(
{
column: "latency",
order: "ASC",
},
tracesTableCols,
),
).toStrictEqual(Prisma.sql`ORDER BY tl.latency ASC`);
});
test("orderByToPrisma throws error for orderBy column not included in column defs", () => {
expect(() =>
orderByToPrismaSql(
{
column: "InvalidCol",
order: "ASC",
},
tracesTableCols,
),
).toThrow(/Invalid filter column: InvalidCol/);
});
});
+56
View File
@@ -0,0 +1,56 @@
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-call */
import { type Router } from "next/router";
import type { UrlObject } from "url";
import { type LocationMock } from "@jedmao/location";
type PartialRouter = Partial<Router>;
export const BASE_URL = "https://langfuse.com";
/**
* A Router to be used for testing which provides the bare minimum needed
* for the useQueryParam(s) hook and NextAdapter to work.
*/
export class TestRouter implements PartialRouter {
isReady = true;
pathname = "/";
private currentUrl = "";
private history: string[] = [];
constructor(private locationMock: LocationMock) {}
replace = (url: string | UrlObject) => {
// eslint-disable-next-line @typescript-eslint/no-base-to-string, @typescript-eslint/restrict-template-expressions
this.locationMock.assign(`${BASE_URL}${url}`);
this.currentUrl = TestRouter.getURLString(url);
this.locationMock.assign(`${BASE_URL}${this.currentUrl}`);
return Promise.resolve(true);
};
push = (url: string | UrlObject) => {
this.history.push(this.currentUrl);
this.currentUrl = TestRouter.getURLString(url);
this.locationMock.assign(`${BASE_URL}${this.currentUrl}`);
return Promise.resolve(true);
};
setIsReady = (isReady: boolean) => {
this.isReady = isReady;
};
get asPath() {
return this.pathname;
}
static getURLString(url: string | UrlObject): string {
if (typeof url === "string") {
return url;
}
return `${url.pathname}${url.search}`;
}
getParams(): URLSearchParams {
return new URL(`${BASE_URL}${this.currentUrl}`).searchParams;
}
}
-1
View File
@@ -6,7 +6,6 @@ import { v4 as uuidv4 } from "uuid";
describe("/api/public/generations API Endpoint", () => {
beforeEach(async () => await pruneDatabase());
afterEach(async () => await pruneDatabase());
[
{
+95
View File
@@ -107,6 +107,7 @@ describe("/api/public/ingestion API Endpoint", () => {
metadata: { key: "value" },
release: "1.0.0",
version: "2.0.0",
tags: ["tag-1", "tag-2"],
},
},
{
@@ -185,6 +186,7 @@ describe("/api/public/ingestion API Endpoint", () => {
expect(dbTrace[0]?.projectId).toBe(
"7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
);
expect(dbTrace[0]?.tags).toEqual(["tag-1", "tag-2"]);
const dbGeneration = await prisma.observation.findUnique({
where: {
@@ -253,6 +255,14 @@ describe("/api/public/ingestion API Endpoint", () => {
const eventId = v4();
const scoreId = v4();
const exception = `
ERROR langfuse:callback.py:677 'model_name'
Traceback (most recent call last):
File "/Users/maximiliandeichmann/development/github.com/langfuse/langfuse-python/langfuse/callback.py", line 674, in __on_llm_action
model_name = kwargs["invocation_params"]["model_name"]
KeyError: 'model_name'
`;
const response = await makeAPICall("POST", "/api/public/ingestion", {
batch: [
{
@@ -290,6 +300,7 @@ describe("/api/public/ingestion API Endpoint", () => {
id: generationId,
traceId: traceId,
parentObservationId: spanId,
modelParameters: { someKey: ["user-1", "user-2"] },
},
},
{
@@ -324,6 +335,14 @@ describe("/api/public/ingestion API Endpoint", () => {
observationId: generationId,
},
},
{
id: v4(),
type: "sdk-log",
timestamp: new Date().toISOString(),
body: {
log: exception,
},
},
],
});
@@ -358,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: {
@@ -380,6 +402,19 @@ describe("/api/public/ingestion API Endpoint", () => {
expect(dbScore?.traceId).toBe(traceId);
expect(dbScore?.observationId).toBe(generationId);
expect(dbScore?.value).toBe(100.5);
const logEvent = await prisma.events.findFirst({
where: {
data: {
path: ["body", "log"],
string_contains: "ERROR",
},
},
});
expect(logEvent).toBeDefined();
expect(logEvent).not.toBeFalsy();
expect(JSON.stringify(logEvent?.data)).toContain("KeyError: 'model_name'");
});
it("should upsert threats", async () => {
@@ -759,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" }],
+346
View File
@@ -0,0 +1,346 @@
/** @jest-environment node */
import { prisma } from "@/src/server/db";
import { makeAPICall, pruneDatabase } from "@/src/__tests__/test-utils";
import { v4 as uuidv4, v4 } from "uuid";
import { type Prompt } from "@prisma/client";
describe("/api/public/prompts API Endpoint", () => {
beforeEach(async () => await pruneDatabase());
it("should fetch a prompt", async () => {
const promptId = uuidv4();
await prisma.prompt.create({
data: {
id: promptId,
name: "prompt-name",
prompt: "prompt",
isActive: true,
version: 1,
project: {
connect: { id: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a" },
},
createdBy: "user-1",
},
});
const fetchedObservations = await makeAPICall(
"GET",
"/api/public/prompts?name=prompt-name&version=1",
undefined,
);
expect(fetchedObservations.status).toBe(200);
if (!isPrompt(fetchedObservations.body)) {
throw new Error("Expected body to be a prompt");
}
expect(fetchedObservations.body.id).toBe(promptId);
expect(fetchedObservations.body.name).toBe("prompt-name");
expect(fetchedObservations.body.prompt).toBe("prompt");
expect(fetchedObservations.body.version).toBe(1);
expect(fetchedObservations.body.isActive).toBe(true);
expect(fetchedObservations.body.createdBy).toBe("user-1");
});
it("should fetch active prompt only if no prompt version is given", async () => {
const promptId = uuidv4();
await prisma.prompt.create({
data: {
id: promptId,
name: "prompt-name",
prompt: "prompt",
isActive: false,
version: 1,
project: {
connect: { id: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a" },
},
createdBy: "user-1",
},
});
const fetchedObservations = await makeAPICall(
"GET",
"/api/public/prompts?name=prompt-name",
undefined,
);
expect(fetchedObservations.status).toBe(404);
});
it("should fetch inactive prompt if prompt version is given", async () => {
const promptId = uuidv4();
const promptTwoId = uuidv4();
await prisma.prompt.create({
data: {
id: promptId,
name: "prompt-name",
prompt: "prompt-one",
isActive: false,
version: 1,
project: {
connect: { id: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a" },
},
createdBy: "user-1",
},
});
await prisma.prompt.create({
data: {
id: promptTwoId,
name: "prompt-name",
prompt: "prompt",
isActive: true,
version: 2,
project: {
connect: { id: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a" },
},
createdBy: "user-1",
},
});
const fetchedObservations = await makeAPICall(
"GET",
"/api/public/prompts?name=prompt-name&version=1",
undefined,
);
expect(fetchedObservations.status).toBe(200);
if (!isPrompt(fetchedObservations.body)) {
throw new Error("Expected body to be a prompt");
}
expect(fetchedObservations.body.id).toBe(promptId);
expect(fetchedObservations.body.name).toBe("prompt-name");
expect(fetchedObservations.body.prompt).toBe("prompt-one");
expect(fetchedObservations.body.version).toBe(1);
expect(fetchedObservations.body.isActive).toBe(false);
expect(fetchedObservations.body.createdBy).toBe("user-1");
});
it("should fetch active prompt when multiple exist", async () => {
const promptIdOne = uuidv4();
const promptIdTwo = uuidv4();
await prisma.prompt.create({
data: {
id: promptIdOne,
name: "prompt-name",
prompt: "prompt",
isActive: false,
version: 1,
project: {
connect: { id: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a" },
},
createdBy: "user-1",
},
});
await prisma.prompt.create({
data: {
id: promptIdTwo,
name: "prompt-name",
prompt: "prompt",
isActive: true,
version: 2,
project: {
connect: { id: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a" },
},
createdBy: "user-1",
},
});
const fetchedObservations = await makeAPICall(
"GET",
"/api/public/prompts?name=prompt-name",
undefined,
);
expect(fetchedObservations.status).toBe(200);
if (!isPrompt(fetchedObservations.body)) {
throw new Error("Expected body to be a prompt");
}
expect(fetchedObservations.body.id).toBe(promptIdTwo);
expect(fetchedObservations.body.name).toBe("prompt-name");
expect(fetchedObservations.body.prompt).toBe("prompt");
expect(fetchedObservations.body.version).toBe(2);
expect(fetchedObservations.body.isActive).toBe(true);
expect(fetchedObservations.body.createdBy).toBe("user-1");
});
it("should create and fetch a prompt", async () => {
await makeAPICall("POST", "/api/public/prompts", {
name: "prompt-name",
prompt: "prompt",
isActive: true,
projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
});
const fetchedObservations = await makeAPICall(
"GET",
"/api/public/prompts?name=prompt-name&version=1",
undefined,
);
expect(fetchedObservations.status).toBe(200);
if (!isPrompt(fetchedObservations.body)) {
throw new Error("Expected body to be an array of observations");
}
expect(fetchedObservations.body.name).toBe("prompt-name");
expect(fetchedObservations.body.prompt).toBe("prompt");
expect(fetchedObservations.body.version).toBe(1);
expect(fetchedObservations.body.isActive).toBe(true);
expect(fetchedObservations.body.createdBy).toBe("API");
});
it("should relate generation to prompt", async () => {
const traceId = v4();
const generationId = v4();
const promptId = uuidv4();
await prisma.prompt.create({
data: {
id: promptId,
name: "prompt-name",
prompt: "prompt",
isActive: true,
version: 1,
project: {
connect: { id: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a" },
},
createdBy: "user-1",
},
});
const response = await makeAPICall("POST", "/api/public/ingestion", {
metadata: {
sdk_verion: "1.0.0",
sdk_name: "python",
},
batch: [
{
id: v4(),
type: "trace-create",
timestamp: new Date().toISOString(),
body: {
id: traceId,
name: "trace-name",
},
},
{
id: v4(),
type: "generation-create",
timestamp: new Date().toISOString(),
body: {
id: generationId,
traceId: traceId,
type: "GENERATION",
name: "generation-name",
promptName: "prompt-name",
promptVersion: 1,
},
},
],
});
expect(response.status).toBe(207);
console.log("response body", response.body);
const dbGeneration = await prisma.observation.findUnique({
where: {
id: generationId,
},
});
expect(dbGeneration?.id).toBe(generationId);
expect(dbGeneration?.promptId).toBe(promptId);
});
it("should fail if prompt version is missing", async () => {
const traceId = v4();
const generationId = v4();
const promptId = uuidv4();
await prisma.prompt.create({
data: {
id: promptId,
name: "prompt-name",
prompt: "prompt",
isActive: true,
version: 1,
project: {
connect: { id: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a" },
},
createdBy: "user-1",
},
});
const response = await makeAPICall("POST", "/api/public/ingestion", {
metadata: {
sdk_verion: "1.0.0",
sdk_name: "python",
},
batch: [
{
id: v4(),
type: "trace-create",
timestamp: new Date().toISOString(),
body: {
id: traceId,
name: "trace-name",
},
},
{
id: v4(),
type: "generation-create",
timestamp: new Date().toISOString(),
body: {
id: generationId,
traceId: traceId,
type: "GENERATION",
name: "generation-name",
promptName: "prompt-name",
},
},
],
});
expect(response.status).toBe(207);
console.log("response body", response.body);
const dbGeneration = await prisma.observation.findUnique({
where: {
id: generationId,
},
});
expect(dbGeneration).toBeNull();
});
});
const isPrompt = (x: unknown): x is Prompt => {
if (typeof x !== "object" || x === null) return false;
const prompt = x as Prompt;
return (
typeof prompt.id === "string" &&
typeof prompt.name === "string" &&
typeof prompt.version === "number" &&
typeof prompt.prompt === "string" &&
typeof prompt.isActive === "boolean" &&
typeof prompt.projectId === "string" &&
typeof prompt.createdBy === "string"
);
};
@@ -0,0 +1,107 @@
/** @jest-environment node */
import { pruneDatabase } from "@/src/__tests__/test-utils";
import { appRouter } from "@/src/server/api/root";
import { createInnerTRPCContext } from "@/src/server/api/trpc";
import { prisma } from "@/src/server/db";
import type { Session } from "next-auth";
describe("Traces TRPC Router", () => {
beforeEach(async () => await pruneDatabase());
afterEach(async () => await pruneDatabase());
const session: Session = {
expires: "1",
user: {
id: "clgb17vnp000008jjere5g15i",
name: "John Doe",
projects: [
{
id: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
role: "ADMIN",
name: "test",
},
],
featureFlags: {
templateFlag: true,
},
admin: true,
},
};
const ctx = createInnerTRPCContext({ session });
const caller = appRouter.createCaller({ ...ctx, prisma });
test("traces.all RPC returns an array of traces", async () => {
const trace = {
name: "trace-name",
userId: "user-1",
metadata: { key: "value" },
release: "1.0.0",
version: "2.0.0",
};
await prisma.trace.create({
data: { ...trace, projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a" },
});
const traces = await caller.traces.all({
page: 0,
limit: 10,
// projectId from `seed.ts`
projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
filter: null,
searchQuery: "",
orderBy: null,
});
expect(traces).toBeDefined();
expect(traces).toMatchObject({ traces: [trace] });
});
test("traces.all RPC orders traces by userId", async () => {
const traceTmpl = {
name: "trace-name",
userId: "user-1",
metadata: { key: "value" },
release: "1.0.0",
version: "2.0.0",
};
const trace1 = traceTmpl;
const trace2 = { ...traceTmpl, userId: "user-2" };
await prisma.trace.create({
data: { ...traceTmpl, projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a" },
});
await prisma.trace.create({
data: {
...trace2,
projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
},
});
const tracesASC = await caller.traces.all({
page: 0,
limit: 10,
// projectId from `seed.ts`
projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
filter: null,
searchQuery: "",
orderBy: {
column: "userId",
order: "ASC",
},
});
expect(tracesASC).toMatchObject({ traces: [trace1, trace2] });
const tracesDESC = await caller.traces.all({
page: 0,
limit: 10,
// projectId from `seed.ts`
projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
filter: null,
searchQuery: "",
orderBy: {
column: "userId",
order: "DESC",
},
});
expect(tracesDESC).toMatchObject({ traces: [trace2, trace1] });
});
});
+2
View File
@@ -12,6 +12,8 @@ export const pruneDatabase = async () => {
await prisma.datasetItem.deleteMany();
await prisma.dataset.deleteMany();
await prisma.datasetRuns.deleteMany();
await prisma.prompt.deleteMany();
await prisma.events.deleteMany();
};
export function createBasicAuthHeader(
+50 -2
View File
@@ -40,7 +40,6 @@ describe("/api/public/traces API Endpoint", () => {
id: "trace-id",
name: "trace-name",
userId: "user-1",
projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
metadata: { key: "value" },
release: "1.0.0",
version: "2.0.0",
@@ -65,7 +64,6 @@ describe("/api/public/traces API Endpoint", () => {
await makeAPICall("POST", "/api/public/traces", {
id: "trace-id",
projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
metadata: { key: "value" },
release: "1.0.0",
version: "5.0.0",
@@ -88,4 +86,54 @@ describe("/api/public/traces API Endpoint", () => {
userId: "user-1",
});
});
it("should use tags correctly on POST and GET", async () => {
await pruneDatabase();
await makeAPICall("POST", "/api/public/traces", {
id: "trace-1",
tags: ["tag-1", "tag-2", "tag-3"],
});
await makeAPICall("POST", "/api/public/traces", {
id: "trace-2",
tags: ["tag-1"],
});
await makeAPICall("POST", "/api/public/traces", {
id: "trace-3",
tags: ["tag-2", "tag-3"],
});
// multiple tags
const traces = await makeAPICall(
"GET",
"/api/public/traces?tags=tag-2&tags=tag-3",
);
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
const traceIds = traces.body.data.map((t: { id: string }) => t.id);
// check for equality ok as ordered by timestamp
expect(traceIds).toEqual(["trace-3", "trace-1"]);
// single tag
const traces2 = await makeAPICall("GET", "/api/public/traces?tags=tag-1");
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
const traceIds2 = traces2.body.data.map((t: { id: string }) => t.id);
// check for equality ok as ordered by timestamp
expect(traceIds2).toEqual(["trace-2", "trace-1"]);
// wrong tag
const traces3 = await makeAPICall("GET", "/api/public/traces?tags=tag-10");
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
const traceIds3 = traces3.body.data.map((t: { id: string }) => t.id);
// check for equality ok as ordered by timestamp
expect(traceIds3).toEqual([]);
// no tag
const traces4 = await makeAPICall("GET", "/api/public/traces?tags=");
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
const traceIds4 = traces4.body.data.map((t: { id: string }) => t.id);
// check for equality ok as ordered by timestamp
expect(traceIds4).toEqual(["trace-3", "trace-2", "trace-1"]);
});
});
+4
View File
@@ -4,10 +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({
@@ -70,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}`, () => {
+14
View File
@@ -1,3 +1,4 @@
import { VERSION } from "@/src/constants/VERSION";
import { cn } from "@/src/utils/tailwind";
export const LangfuseIcon = ({
@@ -20,9 +21,11 @@ export const LangfuseIcon = ({
export const LangfuseLogo = ({
className,
size = "sm",
version = false,
}: {
size?: "sm" | "xl";
className?: string;
version?: boolean;
}) => (
<div className={cn("flex items-center", className)}>
<LangfuseIcon size={size === "sm" ? 16 : 20} />
@@ -34,5 +37,16 @@ export const LangfuseLogo = ({
>
Langfuse
</span>
{version && (
<a
href="https://github.com/langfuse/langfuse/releases"
target="_blank"
rel="noopener"
title="View releases on GitHub"
className="ml-2 text-xs text-gray-400"
>
{VERSION}
</a>
)}
</div>
);
+1 -1
View File
@@ -191,7 +191,7 @@ export function DatePickerWithRange({
{DEFAULT_DATE_RANGE_SELECTION}
</SelectItem>
{dateTimeAggregationOptions.toReversed().map((item) => (
<SelectItem key={item} value={`${item}`}>
<SelectItem key={item} value={item}>
{item}
</SelectItem>
))}
+20 -16
View File
@@ -3,13 +3,19 @@ import Link from "next/link";
import { useRouter } from "next/router";
import { useSession } from "next-auth/react";
import DocPopup from "@/src/components/layouts/doc-popup";
import { type Status, StatusBadge } from "./status-badge";
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 }[];
live?: boolean;
status?: Status;
help?: { description: string; href: string };
actionButtons?: React.ReactNode;
level?: "h2" | "h3";
}) {
const router = useRouter();
const session = useSession();
@@ -34,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">
@@ -50,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) => (
@@ -84,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}
@@ -95,15 +107,7 @@ export default function Header(props: {
/>
) : null}
</div>
{props.live ? (
<div className="flex items-center gap-2 rounded-sm bg-green-100 px-3 text-green-600">
<span className="relative flex h-2 w-2 ">
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-green-500 opacity-75"></span>
<span className="relative inline-flex h-2 w-2 rounded-full bg-green-600"></span>
</span>
Live
</div>
) : null}
{props.status && <StatusBadge type={props.status} />}
</div>
<div className="flex items-center gap-3">
{props.actionButtons ?? null}
+265 -185
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";
@@ -23,6 +23,7 @@ import Head from "next/head";
import { env } from "@/src/env.mjs";
import { LangfuseLogo } from "@/src/components/LangfuseLogo";
import { Spinner } from "@/src/components/layouts/spinner";
import { hasAccess } from "@/src/features/rbac/utils/checkAccess";
const userNavigation = [
{
@@ -34,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]",
];
@@ -49,25 +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]
)
)
.map(({ pathname, ...rest }) => ({
pathname,
href: pathname.replace("[projectId]", projectId ?? ""),
...rest,
}))
.map(({ pathname, ...rest }) => ({
pathname,
current: router.pathname === pathname,
...rest,
}));
return null;
// RBAC
if (
route.rbacScope !== undefined &&
(!projectId || !hasAccess({ projectId, scope: route.rbacScope, session }))
)
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;
@@ -118,7 +141,6 @@ export default function Layout(props: PropsWithChildren) {
{props.children}
</main>
);
return (
<>
<Head>
@@ -198,58 +220,10 @@ export default function Layout(props: PropsWithChildren) {
</Transition.Child>
{/* Sidebar component, swap this element with another sidebar if you like */}
<div className="flex grow flex-col gap-y-5 overflow-y-auto bg-white px-6 py-4">
<LangfuseLogo size="xl" />
<LangfuseLogo version size="xl" />
<nav className="flex flex-1 flex-col">
<ul role="list" className="flex flex-1 flex-col gap-y-7">
<li>
<ul role="list" className="-mx-2 space-y-1">
{navigation.map((item) => (
<li key={item.name}>
<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">
@@ -312,59 +286,10 @@ export default function Layout(props: PropsWithChildren) {
<div className="hidden xl:fixed xl:inset-y-0 xl:z-50 xl:flex xl:w-72 xl:flex-col">
{/* Sidebar component, swap this element with another sidebar if you like */}
<div className="flex h-screen grow flex-col gap-y-5 border-r border-gray-200 bg-white pt-7">
<LangfuseLogo size="xl" className="mb-2 px-6" />
<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">
@@ -417,64 +342,60 @@ export default function Layout(props: PropsWithChildren) {
))}
</ul>
</li>
<li className="-mx-6">
<Menu as="div" className="relative">
<Menu.Button className="flex w-full items-center gap-x-4 p-1.5 px-6 py-3 text-sm font-semibold leading-6 text-gray-900 hover:bg-gray-50">
<span className="sr-only">Open user menu</span>
<Avatar className="h-8 w-8">
<AvatarImage
src={session.data?.user?.image ?? undefined}
/>
<AvatarFallback>
{session.data?.user?.name
? session.data.user.name
.split(" ")
.map((word) => word[0])
.slice(0, 2)
.concat("")
: null}
</AvatarFallback>
</Avatar>
<span className="flex-shrink truncate text-sm font-semibold leading-6 text-gray-900">
{session.data?.user?.name}
</span>
<ChevronDownIcon
className="h-5 w-5 text-gray-400"
aria-hidden="true"
/>
</Menu.Button>
<Transition
as={Fragment}
enter="transition ease-out duration-100"
enterFrom="transform opacity-0 scale-95"
enterTo="transform opacity-100 scale-100"
leave="transition ease-in duration-75"
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
<Menu.Items className="absolute -top-full right-0 z-10 mt-2.5 w-32 rounded-md bg-white py-2 shadow-lg ring-1 ring-gray-900/5 focus:outline-none">
{userNavigation.map((item) => (
<Menu.Item key={item.name}>
{({ active }) => (
<a
onClick={() => void item.onClick()}
className={cn(
active ? "bg-gray-50" : "",
"block cursor-pointer px-3 py-1 text-sm leading-6 text-gray-900",
)}
>
{item.name}
</a>
)}
</Menu.Item>
))}
</Menu.Items>
</Transition>
</Menu>
</li>
</ul>
</nav>
<Menu as="div" className="relative left-1">
<Menu.Button className="flex w-full items-center gap-x-4 p-1.5 px-6 py-3 text-sm font-semibold leading-6 text-gray-900 hover:bg-gray-50">
<span className="sr-only">Open user menu</span>
<Avatar className="h-8 w-8">
<AvatarImage src={session.data?.user?.image ?? undefined} />
<AvatarFallback>
{session.data?.user?.name
? session.data.user.name
.split(" ")
.map((word) => word[0])
.slice(0, 2)
.concat("")
: null}
</AvatarFallback>
</Avatar>
<span className="flex-shrink truncate text-sm font-semibold leading-6 text-gray-900">
{session.data?.user?.name}
</span>
<ChevronDownIcon
className="h-5 w-5 text-gray-400"
aria-hidden="true"
/>
</Menu.Button>
<Transition
as={Fragment}
enter="transition ease-out duration-100"
enterFrom="transform opacity-0 scale-95"
enterTo="transform opacity-100 scale-100"
leave="transition ease-in duration-75"
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
<Menu.Items className="absolute -top-full right-0 z-10 mt-2.5 w-32 rounded-md bg-white py-2 shadow-lg ring-1 ring-gray-900/5 focus:outline-none">
{userNavigation.map((item) => (
<Menu.Item key={item.name}>
{({ active }) => (
<a
onClick={() => void item.onClick()}
className={cn(
active ? "bg-gray-50" : "",
"block cursor-pointer px-3 py-1 text-sm leading-6 text-gray-900",
)}
>
{item.name}
</a>
)}
</Menu.Item>
))}
</Menu.Items>
</Transition>
</Menu>
</div>
</div>
@@ -487,7 +408,7 @@ export default function Layout(props: PropsWithChildren) {
<span className="sr-only">Open sidebar</span>
<Bars3Icon className="h-6 w-6" aria-hidden="true" />
</button>
<LangfuseLogo className="flex-1" />
<LangfuseLogo version className="flex-1" />
<Menu as="div" className="relative">
<Menu.Button className="flex items-center gap-x-4 text-sm font-semibold leading-6 text-gray-900">
<span className="sr-only">Open user menu</span>
@@ -564,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>
);
};
+36 -24
View File
@@ -1,54 +1,66 @@
import { type Flag } from "@/src/features/feature-flags/types";
import { type Scope } from "@/src/features/rbac/constants/roleAccessRights";
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",
pathname: `/project/[projectId]/users`,
icon: UsersIcon,
},
{
name: "Prompts",
pathname: "/project/[projectId]/prompts",
icon: PenSquareIcon,
label: "Beta",
rbacScope: "prompts:read",
},
{
name: "Datasets",
pathname: `/project/[projectId]/datasets`,
+43
View File
@@ -0,0 +1,43 @@
import { cn } from "@/src/utils/tailwind";
const statusCategories = {
active: ["production", "live"],
inactive: ["disabled", "inactive"],
};
export type Status =
(typeof statusCategories)[keyof typeof statusCategories][number];
export const StatusBadge = (props: { className?: string; type: Status }) => {
return (
<div
className={cn(
"inline-flex items-center gap-2 rounded-sm px-2 py-1 text-xs",
statusCategories.active.includes(props.type)
? " bg-green-100 text-green-600"
: "bg-gray-100 text-gray-800",
props.className,
)}
>
<span className="relative inline-flex h-2 w-2">
<span
className={cn(
"absolute inline-flex h-full w-full rounded-full opacity-75",
statusCategories.active.includes(props.type)
? "animate-ping bg-green-500"
: "bg-gray-500",
)}
></span>
<span
className={cn(
"relative inline-flex h-2 w-2 rounded-full ",
statusCategories.active.includes(props.type)
? "bg-green-600"
: "bg-gray-600",
)}
></span>
</span>
<span>{props.type}</span>
</div>
);
};
+23 -4
View File
@@ -8,13 +8,16 @@ import { Badge } from "@/src/components/ui/badge";
import { Card } from "@/src/components/ui/card";
import { ManualScoreButton } from "@/src/features/manual-scoring/components/ManualScoreButton";
import { DetailPageNav } from "@/src/features/navigate-detail-pages/DetailPageNav";
import { useDetailPageLists } from "@/src/features/navigate-detail-pages/context";
import { api } from "@/src/utils/api";
import Link from "next/link";
import { useEffect } from "react";
export const SessionPage: React.FC<{
sessionId: string;
projectId: string;
}> = ({ sessionId, projectId }) => {
const { setDetailPageList } = useDetailPageLists();
const session = api.sessions.byId.useQuery(
{
sessionId,
@@ -27,6 +30,15 @@ export const SessionPage: React.FC<{
},
},
);
useEffect(() => {
if (session.isSuccess) {
setDetailPageList(
"traces",
session.data.traces.map((t) => t.id),
);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [session.isSuccess, session.data]);
if (session.error?.data?.code === "UNAUTHORIZED") return <NoAccessError />;
@@ -56,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>
))}
+132 -24
View File
@@ -1,55 +1,126 @@
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={`h-4 w-4 ${
optimisticValue ? "fill-current text-yellow-500" : "text-gray-500"
}`}
className={cn(
"h-4 w-4",
value ? "fill-current text-yellow-500" : "text-gray-500",
)}
/>
</Button>
);
}
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,
@@ -62,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();
},
});
@@ -73,6 +179,7 @@ export function StarTraceToggle({
value={value}
size={size}
disabled={!hasAccess}
isLoading={isLoading}
onClick={(value) =>
mutBookmarkTrace.mutateAsync({
projectId,
@@ -107,6 +214,7 @@ export function StarSessionToggle({
<StarToggle
value={value}
size={size}
isLoading={mutBookmarkSession.isLoading}
disabled={!hasAccess}
onClick={(value) =>
mutBookmarkSession.mutateAsync({
+76 -22
View File
@@ -1,16 +1,8 @@
"use client";
import {
flexRender,
getCoreRowModel,
useReactTable,
type ColumnFiltersState,
getFilteredRowModel,
type OnChangeFn,
type PaginationState,
type RowSelectionState,
type VisibilityState,
} from "@tanstack/react-table";
import DocPopup from "@/src/components/layouts/doc-popup";
import { DataTablePagination } from "@/src/components/table/data-table-pagination";
import { type LangfuseColumnDef } from "@/src/components/table/types";
import {
Table,
TableBody,
@@ -19,10 +11,20 @@ import {
TableHeader,
TableRow,
} from "@/src/components/ui/table";
import { type OrderByState } from "@/src/features/orderBy/types";
import { cn } from "@/src/utils/tailwind";
import {
flexRender,
getCoreRowModel,
getFilteredRowModel,
useReactTable,
type ColumnFiltersState,
type OnChangeFn,
type PaginationState,
type RowSelectionState,
type VisibilityState,
} from "@tanstack/react-table";
import { useState } from "react";
import { DataTablePagination } from "@/src/components/table/data-table-pagination";
import { type LangfuseColumnDef } from "@/src/components/table/types";
import DocPopup from "@/src/components/layouts/doc-popup";
interface DataTableProps<TData, TValue> {
columns: LangfuseColumnDef<TData, TValue>[];
@@ -36,6 +38,8 @@ interface DataTableProps<TData, TValue> {
setRowSelection?: OnChangeFn<RowSelectionState>;
columnVisibility?: VisibilityState;
onColumnVisibilityChange?: OnChangeFn<VisibilityState>;
orderBy?: OrderByState;
setOrderBy?: (s: OrderByState) => void;
help?: { description: string; href: string };
}
@@ -55,8 +59,11 @@ export function DataTable<TData extends object, TValue>({
columnVisibility,
onColumnVisibilityChange,
help,
orderBy,
setOrderBy,
}: DataTableProps<TData, TValue>) {
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
const table = useReactTable({
data: data.data ?? [],
columns,
@@ -93,17 +100,58 @@ export function DataTable<TData extends object, TValue>({
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => {
const sortingEnabled =
header.column.columnDef.enableSorting;
return header.column.getIsVisible() ? (
<TableHead
key={header.id}
className="whitespace-nowrap p-2"
className={cn(
sortingEnabled ? "cursor-pointer" : null,
"whitespace-nowrap p-2",
)}
title={sortingEnabled ? "Sort by this column" : ""}
onClick={(event) => {
event.preventDefault(); // Add this line
if (
!setOrderBy ||
!header.column.columnDef.id ||
!sortingEnabled
) {
return;
}
if (orderBy?.column === header.column.columnDef.id) {
if (orderBy.order === "DESC") {
setOrderBy({
column: header.column.columnDef.id,
order: "ASC",
});
} else {
setOrderBy(null);
}
} else {
setOrderBy({
column: header.column.columnDef.id,
order: "DESC",
});
}
}}
>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext(),
)}
{header.isPlaceholder ? null : (
<>
<div className="select-none">
{flexRender(
header.column.columnDef.header,
header.getContext(),
)}
{orderBy?.column === header.column.columnDef.id
? renderOrderingIndicator(orderBy)
: null}
</div>
</>
)}
</TableHead>
) : null;
})}
@@ -126,7 +174,7 @@ export function DataTable<TData extends object, TValue>({
{row.getVisibleCells().map((cell) => (
<TableCell
key={cell.id}
className="overflow-hidden whitespace-nowrap p-2 text-xs"
className="overflow-hidden whitespace-nowrap px-2 py-1 text-xs first:pl-2"
>
{flexRender(
cell.column.columnDef.cell,
@@ -163,3 +211,9 @@ export function DataTable<TData extends object, TValue>({
</>
);
}
function renderOrderingIndicator(orderBy?: OrderByState) {
if (!orderBy) return;
if (orderBy.order === "ASC") return <span className="ml-1"></span>;
else return <span className="ml-1"></span>;
}
+10 -16
View File
@@ -1,4 +1,4 @@
import { useRouter } from "next/router";
import Link from "next/link";
export type TableLinkProps = {
path: string;
@@ -11,21 +11,15 @@ export default function TableLink({
value,
truncateAt = 7,
}: TableLinkProps) {
const router = useRouter();
return (
<div>
<button
key="openTrace"
className="rounded bg-indigo-50 px-2 py-1 text-xs font-semibold text-blue-600 shadow-sm hover:bg-indigo-100"
onClick={() => {
void router.push(path);
}}
>
{value.length > truncateAt
? `...${value.substring(value.length - truncateAt)}`
: value}
</button>
</div>
<Link
className="inline-block rounded bg-indigo-50 px-2 py-1 text-xs font-semibold text-blue-600 shadow-sm hover:bg-indigo-100"
href={path}
title={value}
>
{value.length > truncateAt
? `...${value.substring(value.length - truncateAt)}`
: value}
</Link>
);
}
+60 -39
View File
@@ -12,7 +12,6 @@ import {
} from "@/src/components/ui/dropdown-menu";
import { Button } from "@/src/components/ui/button";
import { ChevronDownIcon, Loader } from "lucide-react";
import { type ExportFileFormats } from "@/src/server/api/routers/generations";
import { usePostHog } from "posthog-js/react";
import {
NumberParam,
@@ -31,6 +30,11 @@ import { type ObservationLevel } from "@prisma/client";
import { cn } from "@/src/utils/tailwind";
import { LevelColors } from "@/src/components/level-colors";
import { usdFormatter } from "@/src/utils/numbers";
import {
exportOptions,
type ExportFileFormats,
} from "@/src/server/api/interfaces/exportTypes";
import { useOrderByState } from "@/src/features/orderBy/hooks/useOrderByState";
export type GenerationsTableRow = {
id: string;
@@ -53,23 +57,6 @@ export type GenerationsTableRow = {
};
};
const exportOptions: Record<
ExportFileFormats,
{
label: string;
extension: string;
fileType: string;
}
> = {
CSV: { label: "CSV", extension: "csv", fileType: "text/csv" },
JSON: { label: "JSON", extension: "json", fileType: "application/json" },
"OPENAI-JSONL": {
label: "OpenAI JSONL (fine-tuning)",
extension: "jsonl",
fileType: "application/json",
},
} as const;
export type GenerationsTableProps = {
projectId: string;
};
@@ -96,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,
@@ -114,42 +108,50 @@ export default function GenerationsTable({ projectId }: GenerationsTableProps) {
setIsExporting(true);
posthog.capture("generations:export", { file_format: fileFormat });
const fileData = await directApi.generations.export.query({
projectId,
fileFormat,
filter: filterState,
searchQuery,
});
try {
const fileData = await directApi.generations.export.query({
projectId,
fileFormat,
filter: filterState,
searchQuery,
});
if (fileData) {
const file = new File(
[fileData],
`generations.${exportOptions[fileFormat].extension}`,
{
let url: string;
if (fileData.type === "s3") {
url = fileData.url;
} else {
const file = new File([fileData.data], fileData.fileName, {
type: exportOptions[fileFormat].fileType,
},
);
});
// create url from file
const url = URL.createObjectURL(file);
// create url from file
url = URL.createObjectURL(file);
}
// Use a dynamically created anchor element to trigger the download
const a = document.createElement("a");
document.body.appendChild(a);
a.href = url;
a.download = `generations.${exportOptions[fileFormat].extension}`; // name of the downloaded file
a.download = fileData.fileName; // name of the downloaded file
a.click();
a.remove();
// Revoke the blob URL after using it
setTimeout(() => URL.revokeObjectURL(url), 100);
if (fileData.type === "data") {
setTimeout(() => URL.revokeObjectURL(url), 100);
}
setIsExporting(false);
} catch (e) {
console.error(e);
setIsExporting(false);
}
setIsExporting(false);
};
const columns: LangfuseColumnDef<GenerationsTableRow>[] = [
{
accessorKey: "id",
id: "id",
header: "ID",
cell: ({ row }) => {
const observationId = row.getValue("id");
@@ -162,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");
@@ -179,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");
@@ -200,6 +212,7 @@ export default function GenerationsTable({ projectId }: GenerationsTableProps) {
) : undefined;
},
enableHiding: true,
enableSorting: true,
},
{
accessorKey: "cost",
@@ -215,6 +228,7 @@ export default function GenerationsTable({ projectId }: GenerationsTableProps) {
},
{
accessorKey: "level",
id: "level",
header: "Level",
enableHiding: true,
cell({ row }) {
@@ -231,6 +245,7 @@ export default function GenerationsTable({ projectId }: GenerationsTableProps) {
</span>
) : undefined;
},
enableSorting: true,
},
{
accessorKey: "statusMessage",
@@ -240,8 +255,10 @@ export default function GenerationsTable({ projectId }: GenerationsTableProps) {
},
{
accessorKey: "model",
id: "model",
header: "Model",
enableHiding: true,
enableSorting: true,
},
{
accessorKey: "usage",
@@ -295,8 +312,10 @@ export default function GenerationsTable({ projectId }: GenerationsTableProps) {
},
{
accessorKey: "version",
id: "version",
header: "Version",
enableHiding: true,
enableSorting: true,
},
];
const [columnVisibility, setColumnVisibility] =
@@ -306,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,
@@ -387,7 +406,7 @@ export default function GenerationsTable({ projectId }: GenerationsTableProps) {
data={
generations.isLoading
? { isLoading: true, isError: false }
: generations.isError
: generations.error
? {
isLoading: false,
isError: true,
@@ -404,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={{
+82 -21
View File
@@ -1,20 +1,26 @@
import { DeleteTrace } from "@/src/components/delete-trace";
import { TraceTableMultiSelectAction } from "@/src/components/table/data-table-multi-select-actions/trace-table-multi-select-action";
import { GroupedScoreBadges } from "@/src/components/grouped-score-badge";
import { StarTraceToggle } from "@/src/components/star-toggle";
import { DataTable } from "@/src/components/table/data-table";
import { TraceTableMultiSelectAction } from "@/src/components/table/data-table-multi-select-actions/trace-table-multi-select-action";
import { DataTableToolbar } from "@/src/components/table/data-table-toolbar";
import TableLink from "@/src/components/table/table-link";
import { type LangfuseColumnDef } from "@/src/components/table/types";
import { 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";
import useColumnVisibility from "@/src/features/column-visibility/hooks/useColumnVisibility";
import { useQueryFilterState } from "@/src/features/filters/hooks/useFilterState";
import { type FilterState } from "@/src/features/filters/types";
import { useDetailPageLists } from "@/src/features/navigate-detail-pages/context";
import { useOrderByState } from "@/src/features/orderBy/hooks/useOrderByState";
import { tracesTableColsWithOptions } from "@/src/server/api/definitions/tracesTable";
import { api } from "@/src/utils/api";
import { formatInterval, utcDateOffsetByDays } from "@/src/utils/dates";
import { type RouterInput, type RouterOutput } from "@/src/utils/types";
import { type Score } from "@prisma/client";
import { type RowSelectionState } from "@tanstack/react-table";
import { useEffect, useState } from "react";
import {
NumberParam,
@@ -23,10 +29,6 @@ import {
useQueryParams,
withDefault,
} from "use-query-params";
import { StarTraceToggle } from "@/src/components/star-toggle";
import { JSONView } from "@/src/components/ui/code";
import { type LangfuseColumnDef } from "@/src/components/table/types";
import { type RowSelectionState } from "@tanstack/react-table";
export type TracesTableRow = {
bookmarked: boolean;
@@ -42,6 +44,7 @@ export type TracesTableRow = {
output?: unknown;
sessionId?: string;
scores: Score[];
tags: string[];
usage: {
promptTokens: number;
completionTokens: number;
@@ -76,6 +79,10 @@ export default function TracesTable({
value: utcDateOffsetByDays(-14),
},
]);
const [orderByState, setOrderByState] = useOrderByState({
column: "timestamp",
order: "DESC",
});
const userIdFilter: FilterState = userId
? [
@@ -93,31 +100,44 @@ 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,
});
const totalCount = traces.data?.slice(1)[0]?.totalCount ?? 0;
orderBy: orderByState,
};
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
}, [traces.isSuccess, traces.data]);
const traceFilterOptions = api.traces.filterOptions.useQuery({
projectId,
});
// loading filter options individually from the remaining calls
// traces.all should load first together with everything else.
// This here happens in the background.
const traceFilterOptions = api.traces.filterOptions.useQuery(
{
projectId,
},
{
trpc: {
context: {
skipBatch: true,
},
},
},
);
const convertToTableRow = (
trace: RouterOutput["traces"]["all"][0],
trace: RouterOutput["traces"]["all"]["traces"][0],
): TracesTableRow => {
return {
bookmarked: trace.bookmarked,
@@ -133,6 +153,7 @@ export default function TracesTable({
input: trace.input,
output: trace.output,
latency: trace.latency === null ? undefined : trace.latency,
tags: trace.tags,
usage: {
promptTokens: trace.promptTokens,
completionTokens: trace.completionTokens,
@@ -173,13 +194,14 @@ export default function TracesTable({
{
accessorKey: "bookmarked",
header: undefined,
id: "bookmarked",
cell: ({ row }) => {
const bookmarked = row.getValue("bookmarked");
const traceId = row.getValue("id");
return typeof traceId === "string" &&
typeof bookmarked === "boolean" ? (
<StarTraceToggle
tracesFilter={tracesAllQueryFilter}
traceId={traceId}
projectId={projectId}
value={bookmarked}
@@ -191,6 +213,7 @@ export default function TracesTable({
{
accessorKey: "id",
header: "ID",
id: "id",
cell: ({ row }) => {
const value = row.getValue("id");
return value && typeof value === "string" ? (
@@ -200,32 +223,38 @@ export default function TracesTable({
/>
) : undefined;
},
enableSorting: true,
},
{
accessorKey: "timestamp",
header: "Timestamp",
id: "timestamp",
enableHiding: true,
enableSorting: true,
},
{
accessorKey: "name",
header: "Name",
id: "name",
enableHiding: true,
},
{
accessorKey: "userId",
enableColumnFilter: !omittedFilter.find((f) => f === "userId"),
header: "User ID",
id: "userId",
cell: ({ row }) => {
const value = row.getValue("userId");
return value && typeof value === "string" ? (
<TableLink
path={`/project/${projectId}/users/${value}`}
path={`/project/${projectId}/users/${encodeURIComponent(value)}`}
value={value}
truncateAt={40}
/>
) : undefined;
},
enableHiding: true,
enableSorting: true,
},
{
accessorKey: "sessionId",
@@ -235,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}
/>
@@ -245,6 +274,7 @@ export default function TracesTable({
},
{
accessorKey: "latency",
id: "latency",
header: "Latency",
// add seconds to the end of the latency
cell: ({ row }) => {
@@ -252,8 +282,10 @@ export default function TracesTable({
return value !== undefined ? formatInterval(value) : undefined;
},
enableHiding: true,
enableSorting: true,
},
{
// TODO: Enable Ordering By Usage (not covered by API yet)
accessorKey: "usage",
header: "Usage",
cell: ({ row }) => {
@@ -275,6 +307,7 @@ export default function TracesTable({
},
{
accessorKey: "scores",
id: "scores",
header: "Scores",
enableColumnFilter: !omittedFilter.find((f) => f === "scores"),
cell: ({ row }) => {
@@ -314,13 +347,38 @@ export default function TracesTable({
},
{
accessorKey: "version",
id: "version",
header: "Version",
enableHiding: true,
enableSorting: true,
},
{
accessorKey: "release",
id: "release",
header: "Release",
enableHiding: true,
enableSorting: true,
},
{
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,
},
{
accessorKey: "action",
@@ -359,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={() => {
@@ -384,14 +443,16 @@ 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,
}}
setOrderBy={setOrderByState}
orderBy={orderByState}
rowSelection={selectedRows}
setRowSelection={setSelectedRows}
columnVisibility={columnVisibility}
+3 -2
View File
@@ -103,7 +103,8 @@ export const IOPreview: React.FC<{
const OpenAiMessageSchema = z
.object({
role: z.enum(["system", "user", "assistant"]).optional(),
content: z.string().nullable(),
name: z.string().optional(),
content: z.union([z.record(z.any()), z.string()]).nullable(),
})
.strict() // no additional properties
.refine((value) => value.content !== null || value.role !== undefined);
@@ -129,7 +130,7 @@ const OpenAiMessageView: React.FC<{
.map((message, index) => (
<Fragment key={index}>
<JSONView
title={message.role}
title={message.name ?? message.role}
json={message.content}
className={cn(
message.role === "system" && "bg-gray-100",
+40 -1
View File
@@ -22,6 +22,8 @@ import { type ObservationReturnType } from "@/src/server/api/routers/traces";
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>;
@@ -54,8 +56,25 @@ export const ObservationPreview = (props: {
{preloadedObservation.startTime.toLocaleString()}
</CardDescription>
<div className="flex flex-wrap gap-2">
{preloadedObservation.promptId ? (
<PromptBadge
promptId={preloadedObservation.promptId}
projectId={preloadedObservation.projectId}
/>
) : undefined}
{preloadedObservation.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()) /
@@ -80,9 +99,10 @@ export const ObservationPreview = (props: {
) : null}
{preloadedObservation.price ? (
<Badge variant="outline">
{preloadedObservation.price.toString()} USD
{usdFormatter(preloadedObservation.price.toNumber())}
</Badge>
) : undefined}
{preloadedObservation.modelParameters &&
typeof preloadedObservation.modelParameters === "object"
? Object.entries(preloadedObservation.modelParameters)
@@ -173,3 +193,22 @@ export const ObservationPreview = (props: {
</Card>
);
};
const PromptBadge = (props: { promptId: string; projectId: string }) => {
const prompt = api.prompts.byId.useQuery({
id: props.promptId,
projectId: props.projectId,
});
if (prompt.isLoading) return null;
return (
<Link href={`/project/${props.projectId}/prompts/${props.promptId}`}>
<Badge>
Prompt: {prompt.data?.name}
{" - "}
{prompt.data?.version}
</Badge>
</Link>
);
};
+52 -31
View File
@@ -13,15 +13,20 @@ export const ObservationTree = (props: {
scores: Score[];
currentObservationId: string | undefined;
setCurrentObservationId: (id: string | undefined) => void;
showMetrics: boolean;
showScores: boolean;
className?: string;
}) => {
const nestedObservations = nestObservations(props.observations);
return (
<div className="flex flex-col">
<div className={props.className}>
<ObservationTreeTraceNode
trace={props.trace}
scores={props.scores}
currentObservationId={props.currentObservationId}
setCurrentObservationId={props.setCurrentObservationId}
showMetrics={props.showMetrics}
showScores={props.showScores}
/>
<ObservationTreeNode
observations={nestedObservations}
@@ -29,6 +34,8 @@ export const ObservationTree = (props: {
indentationLevel={1}
currentObservationId={props.currentObservationId}
setCurrentObservationId={props.setCurrentObservationId}
showMetrics={props.showMetrics}
showScores={props.showScores}
/>
</div>
);
@@ -38,10 +45,12 @@ const ObservationTreeTraceNode = (props: {
scores: Score[];
currentObservationId: string | undefined;
setCurrentObservationId: (id: string | undefined) => void;
showMetrics?: boolean;
showScores?: boolean;
}) => (
<div
className={cn(
"group my-1 flex cursor-pointer flex-col gap-1 rounded-sm p-2",
"group mb-0.5 flex cursor-pointer flex-col gap-1 rounded-sm p-1.5",
props.currentObservationId === undefined ||
props.currentObservationId === ""
? "bg-gray-100"
@@ -51,23 +60,23 @@ const ObservationTreeTraceNode = (props: {
>
<div className="flex gap-2">
<span className={cn("rounded-sm bg-gray-200 p-1 text-xs")}>TRACE</span>
<span>{props.trace.name}</span>
<span className="text-sm">{props.trace.name}</span>
</div>
{props.trace.latency ? (
{props.showMetrics && props.trace.latency ? (
<div className="flex gap-2">
<span className="text-xs text-gray-500">
{formatInterval(props.trace.latency)}
</span>
</div>
) : null}
<div className="flex flex-wrap gap-1">
{props.scores.find((s) => s.observationId === null) ? (
{props.showScores && props.scores.find((s) => s.observationId === null) ? (
<div className="flex flex-wrap gap-1">
<GroupedScoreBadges
scores={props.scores.filter((s) => s.observationId === null)}
/>
) : null}
</div>
</div>
) : null}
</div>
);
const ObservationTreeNode = (props: {
@@ -76,6 +85,8 @@ const ObservationTreeNode = (props: {
indentationLevel: number;
currentObservationId: string | undefined;
setCurrentObservationId: (id: string | undefined) => void;
showMetrics?: boolean;
showScores?: boolean;
}) => (
<>
{props.observations
@@ -84,11 +95,11 @@ const ObservationTreeNode = (props: {
<Fragment key={observation.id}>
<div className="flex">
{Array.from({ length: props.indentationLevel }, (_, i) => (
<div className="mx-2 border-r lg:mr-4" key={i} />
<div className="mx-2 border-r" key={i} />
))}
<div
className={cn(
"group my-1 flex flex-1 cursor-pointer flex-col gap-1 rounded-sm p-2 ",
"group my-0.5 flex flex-1 cursor-pointer flex-col gap-1 rounded-sm p-1.5",
props.currentObservationId === observation.id
? "bg-gray-100"
: "hover:bg-gray-50",
@@ -103,27 +114,34 @@ const ObservationTreeNode = (props: {
>
{observation.type}
</span>
<span className="line-clamp-1">{observation.name}</span>
</div>
<div className="flex gap-2">
{observation.endTime ? (
<span className="text-xs text-gray-500">
{formatInterval(
(observation.endTime.getTime() -
observation.startTime.getTime()) /
1000,
)}
</span>
) : null}
{observation.promptTokens ||
observation.completionTokens ||
observation.totalTokens ? (
<span className="text-xs text-gray-500">
{observation.promptTokens} {observation.completionTokens}{" "}
( {observation.totalTokens})
</span>
) : null}
<span className="line-clamp-1 text-sm">{observation.name}</span>
</div>
{props.showMetrics &&
(observation.promptTokens ||
observation.completionTokens ||
observation.totalTokens ||
observation.endTime) && (
<div className="flex gap-2">
{observation.endTime ? (
<span className="text-xs text-gray-500">
{formatInterval(
(observation.endTime.getTime() -
observation.startTime.getTime()) /
1000,
)}
</span>
) : null}
{observation.promptTokens ||
observation.completionTokens ||
observation.totalTokens ? (
<span className="text-xs text-gray-500">
{observation.promptTokens} {" "}
{observation.completionTokens} ({" "}
{observation.totalTokens})
</span>
) : null}
</div>
)}
{observation.level !== "DEFAULT" ? (
<div className="flex">
<span
@@ -137,7 +155,8 @@ const ObservationTreeNode = (props: {
</span>
</div>
) : null}
{props.scores.find((s) => s.observationId === observation.id) ? (
{props.showScores &&
props.scores.find((s) => s.observationId === observation.id) ? (
<div className="flex flex-wrap gap-1">
<GroupedScoreBadges
scores={props.scores.filter(
@@ -154,6 +173,8 @@ const ObservationTreeNode = (props: {
indentationLevel={props.indentationLevel + 1}
currentObservationId={props.currentObservationId}
setCurrentObservationId={props.setCurrentObservationId}
showMetrics={props.showMetrics}
showScores={props.showScores}
/>
</Fragment>
))}
+97 -28
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,9 +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>;
@@ -28,10 +34,16 @@ export function Trace(props: {
"observation",
StringParam,
);
const [metricsOnObservationTree, setMetricsOnObservationTree] =
useLocalStorage("metricsOnObservationTree", true);
const [scoresOnObservationTree, setScoresOnObservationTree] = useLocalStorage(
"scoresOnObservationTree",
true,
);
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 ? (
@@ -49,15 +61,45 @@ export function Trace(props: {
traceId={props.trace.id}
/>
)}
</div>
<div className="md:h-full md:overflow-y-auto">
<ObservationTree
observations={props.observations}
trace={props.trace}
scores={props.scores}
currentObservationId={currentObservationId ?? undefined}
setCurrentObservationId={setCurrentObservationId}
/>
</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) => {
setScoresOnObservationTree(e);
}}
size="sm"
title="Show scores"
>
<Award className="h-4 w-4" />
</Toggle>
<Toggle
pressed={metricsOnObservationTree}
onPressedChange={(e) => {
setMetricsOnObservationTree(e);
}}
size="sm"
title="Show metrics"
>
{metricsOnObservationTree ? (
<ChevronsDownUp className="h-4 w-4" />
) : (
<ChevronsUpDown className="h-4 w-4" />
)}
</Toggle>
</div>
<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>
);
@@ -74,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={[
@@ -99,7 +157,7 @@ export function TracePage({ traceId }: { traceId: string }) {
]}
actionButtons={
<>
<StarTraceToggle
<StarTraceDetailsToggle
traceId={trace.data.id}
projectId={trace.data.projectId}
value={trace.data.bookmarked}
@@ -126,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>
@@ -145,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}
+1 -1
View File
@@ -4,7 +4,7 @@ import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/src/utils/tailwind";
const alertVariants = cva(
"relative w-full rounded-lg border p-4 [&:has(svg)]:pl-11 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground",
"relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground",
{
variants: {
variant: {
+1
View File
@@ -2,6 +2,7 @@
import * as React from "react";
import * as AvatarPrimitive from "@radix-ui/react-avatar";
import { cn } from "@/src/utils/tailwind";
const Avatar = React.forwardRef<
+4 -4
View File
@@ -4,16 +4,16 @@ import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/src/utils/tailwind";
const badgeVariants = cva(
"inline-flex items-center border rounded-full px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
{
variants: {
variant: {
default:
"bg-primary hover:bg-primary/80 border-transparent text-primary-foreground",
"border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
secondary:
"bg-secondary hover:bg-secondary/80 border-transparent text-secondary-foreground",
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
destructive:
"bg-destructive hover:bg-destructive/80 border-transparent text-destructive-foreground",
"border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
outline: "text-foreground",
},
},
+10 -11
View File
@@ -1,13 +1,12 @@
// https://ui.shadcn.com/docs/components/button
import * as React from "react";
import { Slot } from "@radix-ui/react-slot";
import { type VariantProps, cva } from "class-variance-authority";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/src/utils/tailwind";
import { Loader2 } from "lucide-react";
const buttonVariants = cva(
"inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none ring-offset-background",
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
{
variants: {
variant: {
@@ -15,18 +14,18 @@ const buttonVariants = cva(
destructive:
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline:
"border border-input hover:bg-accent hover:text-accent-foreground",
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "underline-offset-4 hover:underline text-primary",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-10 py-2 px-4",
xs: "h-7 px-2 rounded-sm",
sm: "h-9 px-3 rounded-md",
lg: "h-11 px-8 rounded-md",
icon: "h-9 w-9",
default: "h-10 px-4 py-2",
xs: "h-6 px-1 rounded-sm",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
icon: "h-10 w-10",
},
},
defaultVariants: {
+4 -2
View File
@@ -36,15 +36,17 @@ function Calendar({
head_cell:
"text-muted-foreground rounded-md w-9 font-normal text-[0.8rem]",
row: "flex w-full mt-2",
cell: "text-center text-sm p-0 relative [&:has([aria-selected])]:bg-accent first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md focus-within:relative focus-within:z-20",
cell: "h-9 w-9 text-center text-sm p-0 relative [&:has([aria-selected].day-range-end)]:rounded-r-md [&:has([aria-selected].day-outside)]:bg-accent/50 [&:has([aria-selected])]:bg-accent first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md focus-within:relative focus-within:z-20",
day: cn(
buttonVariants({ variant: "ghost" }),
"h-9 w-9 p-0 font-normal aria-selected:opacity-100",
),
day_range_end: "day-range-end",
day_selected:
"bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground",
day_today: "bg-accent text-accent-foreground",
day_outside: "text-muted-foreground opacity-50",
day_outside:
"day-outside text-muted-foreground opacity-50 aria-selected:bg-accent/50 aria-selected:text-muted-foreground aria-selected:opacity-30",
day_disabled: "text-muted-foreground opacity-50",
day_range_middle:
"aria-selected:bg-accent aria-selected:text-accent-foreground",
+4 -5
View File
@@ -1,7 +1,6 @@
// https://ui.shadcn.com/docs/components/card
import * as React from "react";
import { cn } from "@/src/utils/tailwind";
import * as React from "react";
const Card = React.forwardRef<
HTMLDivElement,
@@ -37,7 +36,7 @@ const CardTitle = React.forwardRef<
<h3
ref={ref}
className={cn(
"text-lg font-semibold leading-none tracking-tight",
"text-2xl font-semibold leading-none tracking-tight",
className,
)}
{...props}
@@ -49,7 +48,7 @@ const CardDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
<div
<p
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
@@ -71,7 +70,7 @@ const CardFooter = React.forwardRef<
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(" flex items-center p-6 pt-0", className)}
className={cn("flex items-center p-6 pt-0", className)}
{...props}
/>
));
+14 -11
View File
@@ -5,8 +5,8 @@ import { type DialogProps } from "@radix-ui/react-dialog";
import { Command as CommandPrimitive } from "cmdk";
import { Search } from "lucide-react";
import { cn } from "@/src/utils/tailwind";
import { Dialog, DialogContent } from "@/src/components/ui/dialog";
import { cn } from "@/src/utils/tailwind";
const Command = React.forwardRef<
React.ElementRef<typeof CommandPrimitive>,
@@ -15,7 +15,7 @@ const Command = React.forwardRef<
<CommandPrimitive
ref={ref}
className={cn(
"flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground",
"flex h-full w-full flex-col overflow-hidden rounded-md bg-white text-slate-950 dark:bg-slate-950 dark:text-slate-50",
className,
)}
{...props}
@@ -23,13 +23,13 @@ const Command = React.forwardRef<
));
Command.displayName = CommandPrimitive.displayName;
type CommandDialogProps = DialogProps;
interface CommandDialogProps extends DialogProps {}
const CommandDialog = ({ children, ...props }: CommandDialogProps) => {
return (
<Dialog {...props}>
<DialogContent className="overflow-hidden p-0 shadow-2xl">
<Command className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
<DialogContent className="overflow-hidden p-0 shadow-lg">
<Command className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-slate-500 dark:[&_[cmdk-group-heading]]:text-slate-400 [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
{children}
</Command>
</DialogContent>
@@ -41,12 +41,15 @@ const CommandInput = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Input>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
>(({ className, ...props }, ref) => (
<div className="flex items-center border-b px-3" cmdk-input-wrapper="">
<div
className="flex items-center rounded-lg border px-3"
cmdk-input-wrapper=""
>
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
<CommandPrimitive.Input
ref={ref}
className={cn(
"flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
"flex h-11 w-full rounded-md border-transparent bg-transparent py-3 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,
)}
{...props}
@@ -89,7 +92,7 @@ const CommandGroup = React.forwardRef<
<CommandPrimitive.Group
ref={ref}
className={cn(
"overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground",
"overflow-hidden p-1 text-slate-950 dark:text-slate-50 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-slate-500 dark:[&_[cmdk-group-heading]]:text-slate-400",
className,
)}
{...props}
@@ -104,7 +107,7 @@ const CommandSeparator = React.forwardRef<
>(({ className, ...props }, ref) => (
<CommandPrimitive.Separator
ref={ref}
className={cn("-mx-1 h-px bg-border", className)}
className={cn("-mx-1 h-px bg-slate-200 dark:bg-slate-800", className)}
{...props}
/>
));
@@ -117,7 +120,7 @@ const CommandItem = React.forwardRef<
<CommandPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none aria-selected:bg-accent aria-selected:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
"relative flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none aria-selected:bg-slate-100 aria-selected:text-slate-900 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 dark:aria-selected:bg-slate-800 dark:aria-selected:text-slate-50",
className,
)}
{...props}
@@ -133,7 +136,7 @@ const CommandShortcut = ({
return (
<span
className={cn(
"ml-auto text-xs tracking-widest text-muted-foreground",
"ml-auto text-xs tracking-widest text-slate-500 dark:text-slate-400",
className,
)}
{...props}
+9 -21
View File
@@ -3,31 +3,16 @@
import * as React from "react";
import * as DialogPrimitive from "@radix-ui/react-dialog";
import { X } from "lucide-react";
import { cn } from "@/src/utils/tailwind";
const Dialog = DialogPrimitive.Root;
const DialogTrigger = DialogPrimitive.Trigger;
const DialogPortal = ({
className,
children,
...props
}: DialogPrimitive.DialogPortalProps & {
className?: string;
}) => (
<DialogPrimitive.Portal {...props}>
<div
className={cn(
className,
"fixed inset-0 z-50 flex items-start justify-center sm:items-center",
)}
>
{children}
</div>
</DialogPrimitive.Portal>
);
DialogPortal.displayName = DialogPrimitive.Portal.displayName;
const DialogPortal = DialogPrimitive.Portal;
const DialogClose = DialogPrimitive.Close;
const DialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>,
@@ -36,7 +21,7 @@ const DialogOverlay = React.forwardRef<
<DialogPrimitive.Overlay
ref={ref}
className={cn(
"fixed inset-0 z-50 bg-background/80 backdrop-blur-sm transition-all duration-100 data-[state=closed]:animate-out data-[state=closed]:fade-out data-[state=open]:fade-in",
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className,
)}
{...props}
@@ -53,7 +38,7 @@ const DialogContent = React.forwardRef<
<DialogPrimitive.Content
ref={ref}
className={cn(
"fixed z-50 grid w-full gap-4 overflow-hidden rounded-b-lg border bg-background p-6 shadow-lg animate-in data-[state=open]:fade-in-90 data-[state=open]:slide-in-from-bottom-10 sm:max-w-lg sm:rounded-lg sm:zoom-in-90 data-[state=open]:sm:slide-in-from-bottom-0",
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
className,
)}
{...props}
@@ -125,6 +110,9 @@ DialogDescription.displayName = DialogPrimitive.Description.displayName;
export {
Dialog,
DialogPortal,
DialogOverlay,
DialogClose,
DialogTrigger,
DialogContent,
DialogHeader,
+2 -2
View File
@@ -47,7 +47,7 @@ const DropdownMenuSubContent = React.forwardRef<
<DropdownMenuPrimitive.SubContent
ref={ref}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md animate-in data-[side=bottom]:slide-in-from-top-1 data-[side=left]:slide-in-from-right-1 data-[side=right]:slide-in-from-left-1 data-[side=top]:slide-in-from-bottom-1",
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className,
)}
{...props}
@@ -65,7 +65,7 @@ const DropdownMenuContent = React.forwardRef<
ref={ref}
sideOffset={sideOffset}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md animate-in data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className,
)}
{...props}
+6 -9
View File
@@ -1,5 +1,3 @@
// https://ui.shadcn.com/docs/forms/react-hook-form
import * as React from "react";
import type * as LabelPrimitive from "@radix-ui/react-label";
import { Slot } from "@radix-ui/react-slot";
@@ -11,8 +9,9 @@ import {
FormProvider,
useFormContext,
} from "react-hook-form";
import { Label } from "@/src/components/ui/label";
import { cn } from "@/src/utils/tailwind";
import { Label } from "@/src/components/ui/label";
const Form = FormProvider;
@@ -115,9 +114,7 @@ const FormControl = React.forwardRef<
ref={ref}
id={formItemId}
aria-describedby={
!error
? `${formDescriptionId}`
: `${formDescriptionId} ${formMessageId}`
!error ? formDescriptionId : `${formDescriptionId} ${formMessageId}`
}
aria-invalid={!!error}
{...props}
@@ -133,7 +130,7 @@ const FormDescription = React.forwardRef<
const { formDescriptionId } = useFormField();
return (
<div
<p
ref={ref}
id={formDescriptionId}
className={cn("text-sm text-muted-foreground", className)}
@@ -155,14 +152,14 @@ const FormMessage = React.forwardRef<
}
return (
<div
<p
ref={ref}
id={formMessageId}
className={cn("text-sm font-medium text-destructive", className)}
{...props}
>
{body}
</div>
</p>
);
});
FormMessage.displayName = "FormMessage";
+5 -3
View File
@@ -1,7 +1,9 @@
import { cn } from "@/src/utils/tailwind";
import * as React from "react";
export type InputProps = React.InputHTMLAttributes<HTMLInputElement>;
import { cn } from "@/src/utils/tailwind";
export interface InputProps
extends React.InputHTMLAttributes<HTMLInputElement> {}
const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ className, type, ...props }, ref) => {
@@ -9,7 +11,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
<input
type={type}
className={cn(
"flex h-10 w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className,
)}
ref={ref}
+1
View File
@@ -3,6 +3,7 @@
import * as React from "react";
import * as LabelPrimitive from "@radix-ui/react-label";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/src/utils/tailwind";
const labelVariants = cva(
+42
View File
@@ -0,0 +1,42 @@
import * as React from "react";
import { cn } from "@/src/utils/tailwind";
import { Eye, EyeOff } from "lucide-react";
export interface PasswordInputProps
extends React.InputHTMLAttributes<HTMLInputElement> {}
const PasswordInput = React.forwardRef<HTMLInputElement, PasswordInputProps>(
({ className, ...props }, ref) => {
const [showPassword, setShowPassword] = React.useState(false);
return (
<div className="relative">
<input
type={showPassword ? "text" : "password"}
className={cn(
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 pr-10 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className,
)}
ref={ref}
{...props}
/>
<button
type="button"
className="absolute right-3 top-1/2 -translate-y-1/2 transform cursor-pointer"
onClick={() => setShowPassword(!showPassword)}
title={showPassword ? "Hide password" : "Show password"}
>
{showPassword ? (
<EyeOff className="h-5 w-5 text-gray-400" />
) : (
<Eye className="h-5 w-5 text-gray-400" />
)}
</button>
</div>
);
},
);
PasswordInput.displayName = "PasswordInput";
export { PasswordInput };
+1 -1
View File
@@ -19,7 +19,7 @@ const PopoverContent = React.forwardRef<
align={align}
sideOffset={sideOffset}
className={cn(
"z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none animate-in data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
"z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className,
)}
{...props}
+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 };
+44 -4
View File
@@ -2,7 +2,7 @@
import * as React from "react";
import * as SelectPrimitive from "@radix-ui/react-select";
import { Check, ChevronDown } from "lucide-react";
import { Check, ChevronDown, ChevronUp } from "lucide-react";
import { cn } from "@/src/utils/tailwind";
@@ -19,7 +19,7 @@ const SelectTrigger = React.forwardRef<
<SelectPrimitive.Trigger
ref={ref}
className={cn(
"flex h-10 w-full items-center justify-between rounded-md border border-input bg-transparent px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
"flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
className,
)}
{...props}
@@ -32,6 +32,41 @@ const SelectTrigger = React.forwardRef<
));
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
const SelectScrollUpButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollUpButton
ref={ref}
className={cn(
"flex cursor-default items-center justify-center py-1",
className,
)}
{...props}
>
<ChevronUp className="h-4 w-4" />
</SelectPrimitive.ScrollUpButton>
));
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName;
const SelectScrollDownButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollDownButton
ref={ref}
className={cn(
"flex cursor-default items-center justify-center py-1",
className,
)}
{...props}
>
<ChevronDown className="h-4 w-4" />
</SelectPrimitive.ScrollDownButton>
));
SelectScrollDownButton.displayName =
SelectPrimitive.ScrollDownButton.displayName;
const SelectContent = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
@@ -40,13 +75,15 @@ const SelectContent = React.forwardRef<
<SelectPrimitive.Content
ref={ref}
className={cn(
"relative z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md animate-in fade-in-80",
position === "popper" && "translate-y-1",
"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className,
)}
position={position}
{...props}
>
<SelectScrollUpButton />
<SelectPrimitive.Viewport
className={cn(
"p-1",
@@ -56,6 +93,7 @@ const SelectContent = React.forwardRef<
>
{children}
</SelectPrimitive.Viewport>
<SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
));
@@ -117,4 +155,6 @@ export {
SelectLabel,
SelectItem,
SelectSeparator,
SelectScrollUpButton,
SelectScrollDownButton,
};
+1 -1
View File
@@ -11,7 +11,7 @@ const Switch = React.forwardRef<
>(({ className, ...props }, ref) => (
<SwitchPrimitives.Root
className={cn(
"peer inline-flex h-[24px] w-[44px] shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
"peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
className,
)}
{...props}
+7 -3
View File
@@ -1,11 +1,12 @@
import { cn } from "@/src/utils/tailwind";
import * as React from "react";
import { cn } from "@/src/utils/tailwind";
const Table = React.forwardRef<
HTMLTableElement,
React.HTMLAttributes<HTMLTableElement>
>(({ className, ...props }, ref) => (
<div className="w-full overflow-auto">
<div className="relative w-full overflow-auto">
<table
ref={ref}
className={cn("w-full caption-bottom text-sm", className)}
@@ -41,7 +42,10 @@ const TableFooter = React.forwardRef<
>(({ className, ...props }, ref) => (
<tfoot
ref={ref}
className={cn("bg-primary font-medium text-primary-foreground", className)}
className={cn(
"border-t bg-muted/50 font-medium [&>tr]:last:border-b-0",
className,
)}
{...props}
/>
));
+1
View File
@@ -2,6 +2,7 @@
import * as React from "react";
import * as TabsPrimitive from "@radix-ui/react-tabs";
import { cn } from "@/src/utils/tailwind";
const Tabs = TabsPrimitive.Root;
+1 -2
View File
@@ -2,7 +2,6 @@ import * as React from "react";
import { cn } from "@/src/utils/tailwind";
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface TextareaProps
extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}
@@ -11,7 +10,7 @@ const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
return (
<textarea
className={cn(
"flex min-h-[80px] w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
"flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className,
)}
ref={ref}
+45
View File
@@ -0,0 +1,45 @@
"use client";
import * as React from "react";
import * as TogglePrimitive from "@radix-ui/react-toggle";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/src/utils/tailwind";
const toggleVariants = cva(
"inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors hover:bg-muted hover:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground",
{
variants: {
variant: {
default: "bg-transparent",
outline:
"border border-input bg-transparent hover:bg-accent hover:text-accent-foreground",
},
size: {
default: "h-10 px-3",
sm: "h-9 px-2.5",
lg: "h-11 px-5",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
},
);
const Toggle = React.forwardRef<
React.ElementRef<typeof TogglePrimitive.Root>,
React.ComponentPropsWithoutRef<typeof TogglePrimitive.Root> &
VariantProps<typeof toggleVariants>
>(({ className, variant, size, ...props }, ref) => (
<TogglePrimitive.Root
ref={ref}
className={cn(toggleVariants({ variant, size, className }))}
{...props}
/>
));
Toggle.displayName = TogglePrimitive.Root.displayName;
export { Toggle, toggleVariants };
+56
View File
@@ -0,0 +1,56 @@
import { useState, useEffect } from "react";
/**
* useLocalStorage is a hook for managing data with the localStorage API.
*
* @param {string} localStorageKey - The key under which the value is stored in localStorage.
* @param {T} initialValue - The initial value of the data to be stored.
*
* Note: The object T should be able to be stringified, as it will be stored in localStorage as a string.
*
* @return An array with three elements:
* value: Current value
* setValue: Function to update the value
* clearValue: Function to remove value from the local storage.
* This function will also reset the value to initial value
*
* @template T - The type of the data to be stored in localStorage. It should be a type that can be stringified.
*
* @throws Will throw an error if the stringifying the value or accessing local storage fails.
*/
function useLocalStorage<T>(localStorageKey: string, initialValue: T) {
const [value, setValue] = useState<T>(() => {
try {
const storedValue = localStorage.getItem(localStorageKey);
return storedValue ? (JSON.parse(storedValue) as T) : initialValue;
} catch (error) {
console.error("Error reading from local storage", error);
return initialValue;
}
});
const clearValue = () => {
try {
localStorage.removeItem(localStorageKey);
setValue(initialValue);
} catch (error) {
console.error("Error clearing local storage", error);
}
};
useEffect(() => {
try {
localStorage.setItem(localStorageKey, JSON.stringify(value));
} catch (error) {
console.error("Error writing to local storage", error);
}
}, [localStorageKey, value]);
return [value, setValue, clearValue] as [
T,
React.Dispatch<React.SetStateAction<T>>,
() => void,
];
}
export default useLocalStorage;
+1 -1
View File
@@ -1 +1 @@
export const VERSION = "v1.19.4";
export const VERSION = "v1.34.1";
+18
View File
@@ -43,11 +43,20 @@ export const env = createEnv({
AUTH_GOOGLE_CLIENT_SECRET: z.string().optional(),
AUTH_GITHUB_CLIENT_ID: z.string().optional(),
AUTH_GITHUB_CLIENT_SECRET: z.string().optional(),
AUTH_AZURE_AD_CLIENT_ID: z.string().optional(),
AUTH_AZURE_AD_CLIENT_SECRET: z.string().optional(),
AUTH_AZURE_AD_TENANT_ID: z.string().optional(),
AUTH_DOMAINS_WITH_SSO_ENFORCEMENT: z.string().optional(),
AUTH_DISABLE_USERNAME_PASSWORD: z.enum(["true", "false"]).optional(),
// EMAIL
EMAIL_FROM_ADDRESS: z.string().optional(),
SMTP_CONNECTION_URL: z.string().optional(),
// S3
S3_ENDPOINT: z.string().optional(),
S3_ACCESS_KEY_ID: z.string().optional(),
S3_SECRET_ACCESS_KEY: z.string().optional(),
S3_BUCKET_NAME: z.string().optional(),
S3_REGION: z.string().optional(),
},
/**
@@ -93,11 +102,20 @@ export const env = createEnv({
AUTH_GOOGLE_CLIENT_SECRET: process.env.AUTH_GOOGLE_CLIENT_SECRET,
AUTH_GITHUB_CLIENT_ID: process.env.AUTH_GITHUB_CLIENT_ID,
AUTH_GITHUB_CLIENT_SECRET: process.env.AUTH_GITHUB_CLIENT_SECRET,
AUTH_AZURE_AD_CLIENT_ID: process.env.AUTH_AZURE_AD_CLIENT_ID,
AUTH_AZURE_AD_CLIENT_SECRET: process.env.AUTH_AZURE_AD_CLIENT_SECRET,
AUTH_AZURE_AD_TENANT_ID: process.env.AUTH_AZURE_AD_TENANT_ID,
AUTH_DOMAINS_WITH_SSO_ENFORCEMENT:
process.env.AUTH_DOMAINS_WITH_SSO_ENFORCEMENT,
AUTH_DISABLE_USERNAME_PASSWORD: process.env.AUTH_DISABLE_USERNAME_PASSWORD,
// Email
EMAIL_FROM_ADDRESS: process.env.EMAIL_FROM_ADDRESS,
SMTP_CONNECTION_URL: process.env.SMTP_CONNECTION_URL,
// S3
S3_ENDPOINT: process.env.S3_ENDPOINT,
S3_ACCESS_KEY_ID: process.env.S3_ACCESS_KEY_ID,
S3_SECRET_ACCESS_KEY: process.env.S3_SECRET_ACCESS_KEY,
S3_BUCKET_NAME: process.env.S3_BUCKET_NAME,
S3_REGION: process.env.S3_REGION,
},
});
@@ -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>,
])}
@@ -84,7 +84,7 @@ export const TracesTimeSeriesChart = ({
/>
{!isEmptyTimeSeries(transformedTraces) ? (
<BaseTimeSeriesChart
className="min-h-80 h-full self-stretch"
className="h-full min-h-80 self-stretch"
agg={agg}
data={transformedTraces}
connectNulls={true}
@@ -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

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