Compare commits

...
102 Commits
Author SHA1 Message Date
Max Deichmann 9b7f046e71 chore: release v2.76.0
CI/CD / lint (push) Waiting to run
CI/CD / test-docker-build (push) Waiting to run
CI/CD / tests-web (node20, pg12) (push) Waiting to run
CI/CD / tests-web (node20, pg15) (push) Waiting to run
CI/CD / tests-worker (node20, pg12) (push) Waiting to run
CI/CD / tests-worker (node20, pg15) (push) Waiting to run
CI/CD / e2e-tests (push) Waiting to run
CI/CD / e2e-server-tests (push) Waiting to run
CI/CD / all-ci-passed (push) Blocked by required conditions
CI/CD / push-docker-image (push) Blocked by required conditions
release.yml / release (push) Waiting to run
2024-09-03 10:48:10 +02:00
Steffen SchmitzandGitHub fe63704508 feat: fallback to synchronous ingestion if queue unavailable (#3196) 2024-09-03 10:42:59 +02:00
marliessophieandGitHub c39279f677 style(prompts): actions on hover (#3135) 2024-09-03 08:12:39 +00:00
Marc KlingenandGitHub 4206f5025d perf(cloud): pull usage data shown in settings from stripe if available (#3206) 2024-09-02 23:51:13 +00:00
marliessophieandGitHub 0260a4e861 style(dataset_items): make table vs item detail resizeable (#3139) 2024-09-02 08:01:43 +00:00
Max DeichmannandGitHub 244adf0dc3 feat: disable legacy ingestion queue if required (#3183) 2024-09-01 14:24:25 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
dc6b98d009 chore(deps-dev): bump tsx from 4.18.0 to 4.19.0 (#3172)
Bumps [tsx](https://github.com/privatenumber/tsx) from 4.18.0 to 4.19.0.
- [Release notes](https://github.com/privatenumber/tsx/releases)
- [Changelog](https://github.com/privatenumber/tsx/blob/master/release.config.cjs)
- [Commits](https://github.com/privatenumber/tsx/compare/v4.18.0...v4.19.0)

---
updated-dependencies:
- dependency-name: tsx
  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-09-01 10:07:44 +02:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
b20fc5a20b chore(deps-dev): bump tailwindcss from 3.4.4 to 3.4.10 (#3173)
Bumps [tailwindcss](https://github.com/tailwindlabs/tailwindcss) from 3.4.4 to 3.4.10.
- [Release notes](https://github.com/tailwindlabs/tailwindcss/releases)
- [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/v3.4.10/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/tailwindcss/compare/v3.4.4...v3.4.10)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-31 22:36:42 +02:00
Max DeichmannandGitHub f59ad5fd60 chore: upload sentry sourcemaps (#3179) 2024-08-31 18:50:48 +02:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
06ef41e372 chore(deps): bump pino-pretty from 10.3.1 to 11.2.2 (#3170)
Bumps [pino-pretty](https://github.com/pinojs/pino-pretty) from 10.3.1 to 11.2.2.
- [Release notes](https://github.com/pinojs/pino-pretty/releases)
- [Commits](https://github.com/pinojs/pino-pretty/compare/v10.3.1...v11.2.2)

---
updated-dependencies:
- dependency-name: pino-pretty
  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-08-31 17:04:49 +02:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
e215a2db99 chore(deps-dev): bump @types/lodash from 4.17.5 to 4.17.7 in the lodash group (#3169)
chore(deps-dev): bump @types/lodash in the lodash group

Bumps the lodash group with 1 update: [@types/lodash](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/lodash).


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

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-31 17:00:36 +02:00
Max DeichmannandGitHub 5e247e88c9 fix: do nothing if invalidating org or project without api keys (#3167)
* also run on main branch

* also run on main branch

* also run on main branch

* also run on main branch

* also run on main branch
2024-08-30 22:57:41 +02:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
8e1e27c868 chore(deps-dev): bump msw from 2.3.1 to 2.4.1 (#3150)
Bumps [msw](https://github.com/mswjs/msw) from 2.3.1 to 2.4.1.
- [Release notes](https://github.com/mswjs/msw/releases)
- [Changelog](https://github.com/mswjs/msw/blob/main/CHANGELOG.md)
- [Commits](https://github.com/mswjs/msw/compare/v2.3.1...v2.4.1)

---
updated-dependencies:
- dependency-name: msw
  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-08-30 21:52:41 +02:00
dependabot[bot]andGitHub 4648a5be8b chore(deps): bump lucide-react from 0.429.0 to 0.436.0 (#3154) 2024-08-30 21:02:28 +02:00
Marc Klingen 4677cb10a9 chore: release v2.75.2
CI/CD / lint (push) Waiting to run
CI/CD / test-docker-build (push) Waiting to run
CI/CD / tests-web (node20, pg12) (push) Waiting to run
CI/CD / tests-web (node20, pg15) (push) Waiting to run
CI/CD / tests-worker (node20, pg12) (push) Waiting to run
CI/CD / tests-worker (node20, pg15) (push) Waiting to run
CI/CD / e2e-tests (push) Waiting to run
CI/CD / e2e-server-tests (push) Waiting to run
CI/CD / all-ci-passed (push) Blocked by required conditions
CI/CD / push-docker-image (push) Blocked by required conditions
release.yml / release (push) Waiting to run
2024-08-30 20:38:04 +02:00
Marc KlingenandGitHub 7ed03a0946 fix(playground): allow for creation of non-openai llm api keys (#3162) 2024-08-30 20:36:55 +02:00
Marc Klingen bbe40a0ba8 chore: release v2.75.1
CI/CD / lint (push) Waiting to run
CI/CD / test-docker-build (push) Waiting to run
CI/CD / tests-web (node20, pg12) (push) Waiting to run
CI/CD / tests-web (node20, pg15) (push) Waiting to run
CI/CD / tests-worker (node20, pg12) (push) Waiting to run
CI/CD / tests-worker (node20, pg15) (push) Waiting to run
CI/CD / e2e-tests (push) Waiting to run
CI/CD / e2e-server-tests (push) Waiting to run
CI/CD / all-ci-passed (push) Blocked by required conditions
CI/CD / push-docker-image (push) Blocked by required conditions
release.yml / release (push) Waiting to run
2024-08-30 20:25:38 +02:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2cf9a75fa5 chore(deps): bump @uiw/codemirror-theme-github from 4.21.25 to 4.23.0 (#3151)
Bumps [@uiw/codemirror-theme-github](https://github.com/uiwjs/react-codemirror) from 4.21.25 to 4.23.0.
- [Release notes](https://github.com/uiwjs/react-codemirror/releases)
- [Commits](https://github.com/uiwjs/react-codemirror/compare/v4.21.25...v4.23.0)

---
updated-dependencies:
- dependency-name: "@uiw/codemirror-theme-github"
  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-08-30 17:59:28 +02:00
Max DeichmannandGitHub 38acccbfac fix: make redis upsert queue backwards compatible (#3165) 2024-08-30 14:31:23 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
8327863176 chore(deps): bump the radix-ui group with 18 updates (#3159)
Bumps the radix-ui group with 18 updates:

| Package | From | To |
| --- | --- | --- |
| [@radix-ui/react-accordion](https://github.com/radix-ui/primitives) | `1.1.2` | `1.2.0` |
| [@radix-ui/react-alert-dialog](https://github.com/radix-ui/primitives) | `1.0.5` | `1.1.1` |
| [@radix-ui/react-avatar](https://github.com/radix-ui/primitives) | `1.0.4` | `1.1.0` |
| [@radix-ui/react-checkbox](https://github.com/radix-ui/primitives) | `1.0.4` | `1.1.1` |
| [@radix-ui/react-dialog](https://github.com/radix-ui/primitives) | `1.0.5` | `1.1.1` |
| [@radix-ui/react-dropdown-menu](https://github.com/radix-ui/primitives) | `2.0.6` | `2.1.1` |
| [@radix-ui/react-hover-card](https://github.com/radix-ui/primitives) | `1.0.7` | `1.1.1` |
| [@radix-ui/react-label](https://github.com/radix-ui/primitives) | `2.0.2` | `2.1.0` |
| [@radix-ui/react-popover](https://github.com/radix-ui/primitives) | `1.0.7` | `1.1.1` |
| [@radix-ui/react-scroll-area](https://github.com/radix-ui/primitives) | `1.0.5` | `1.1.0` |
| [@radix-ui/react-select](https://github.com/radix-ui/primitives) | `2.0.0` | `2.1.1` |
| [@radix-ui/react-separator](https://github.com/radix-ui/primitives) | `1.0.3` | `1.1.0` |
| [@radix-ui/react-slider](https://github.com/radix-ui/primitives) | `1.1.2` | `1.2.0` |
| [@radix-ui/react-slot](https://github.com/radix-ui/primitives) | `1.0.2` | `1.1.0` |
| [@radix-ui/react-switch](https://github.com/radix-ui/primitives) | `1.0.3` | `1.1.0` |
| [@radix-ui/react-tabs](https://github.com/radix-ui/primitives) | `1.0.4` | `1.1.0` |
| [@radix-ui/react-toggle](https://github.com/radix-ui/primitives) | `1.0.3` | `1.1.0` |
| [@radix-ui/react-tooltip](https://github.com/radix-ui/primitives) | `1.0.7` | `1.1.2` |


Updates `@radix-ui/react-accordion` from 1.1.2 to 1.2.0
- [Changelog](https://github.com/radix-ui/primitives/blob/main/release-process.md)
- [Commits](https://github.com/radix-ui/primitives/commits)

Updates `@radix-ui/react-alert-dialog` from 1.0.5 to 1.1.1
- [Changelog](https://github.com/radix-ui/primitives/blob/main/release-process.md)
- [Commits](https://github.com/radix-ui/primitives/commits)

Updates `@radix-ui/react-avatar` from 1.0.4 to 1.1.0
- [Changelog](https://github.com/radix-ui/primitives/blob/main/release-process.md)
- [Commits](https://github.com/radix-ui/primitives/commits)

Updates `@radix-ui/react-checkbox` from 1.0.4 to 1.1.1
- [Changelog](https://github.com/radix-ui/primitives/blob/main/release-process.md)
- [Commits](https://github.com/radix-ui/primitives/commits)

Updates `@radix-ui/react-dialog` from 1.0.5 to 1.1.1
- [Changelog](https://github.com/radix-ui/primitives/blob/main/release-process.md)
- [Commits](https://github.com/radix-ui/primitives/commits)

Updates `@radix-ui/react-dropdown-menu` from 2.0.6 to 2.1.1
- [Changelog](https://github.com/radix-ui/primitives/blob/main/release-process.md)
- [Commits](https://github.com/radix-ui/primitives/commits)

Updates `@radix-ui/react-hover-card` from 1.0.7 to 1.1.1
- [Changelog](https://github.com/radix-ui/primitives/blob/main/release-process.md)
- [Commits](https://github.com/radix-ui/primitives/commits)

Updates `@radix-ui/react-label` from 2.0.2 to 2.1.0
- [Changelog](https://github.com/radix-ui/primitives/blob/main/release-process.md)
- [Commits](https://github.com/radix-ui/primitives/commits)

Updates `@radix-ui/react-popover` from 1.0.7 to 1.1.1
- [Changelog](https://github.com/radix-ui/primitives/blob/main/release-process.md)
- [Commits](https://github.com/radix-ui/primitives/commits)

Updates `@radix-ui/react-scroll-area` from 1.0.5 to 1.1.0
- [Changelog](https://github.com/radix-ui/primitives/blob/main/release-process.md)
- [Commits](https://github.com/radix-ui/primitives/commits)

Updates `@radix-ui/react-select` from 2.0.0 to 2.1.1
- [Changelog](https://github.com/radix-ui/primitives/blob/main/release-process.md)
- [Commits](https://github.com/radix-ui/primitives/commits)

Updates `@radix-ui/react-separator` from 1.0.3 to 1.1.0
- [Changelog](https://github.com/radix-ui/primitives/blob/main/release-process.md)
- [Commits](https://github.com/radix-ui/primitives/commits)

Updates `@radix-ui/react-slider` from 1.1.2 to 1.2.0
- [Changelog](https://github.com/radix-ui/primitives/blob/main/release-process.md)
- [Commits](https://github.com/radix-ui/primitives/commits)

Updates `@radix-ui/react-slot` from 1.0.2 to 1.1.0
- [Changelog](https://github.com/radix-ui/primitives/blob/main/release-process.md)
- [Commits](https://github.com/radix-ui/primitives/commits)

Updates `@radix-ui/react-switch` from 1.0.3 to 1.1.0
- [Changelog](https://github.com/radix-ui/primitives/blob/main/release-process.md)
- [Commits](https://github.com/radix-ui/primitives/commits)

Updates `@radix-ui/react-tabs` from 1.0.4 to 1.1.0
- [Changelog](https://github.com/radix-ui/primitives/blob/main/release-process.md)
- [Commits](https://github.com/radix-ui/primitives/commits)

Updates `@radix-ui/react-toggle` from 1.0.3 to 1.1.0
- [Changelog](https://github.com/radix-ui/primitives/blob/main/release-process.md)
- [Commits](https://github.com/radix-ui/primitives/commits)

Updates `@radix-ui/react-tooltip` from 1.0.7 to 1.1.2
- [Changelog](https://github.com/radix-ui/primitives/blob/main/release-process.md)
- [Commits](https://github.com/radix-ui/primitives/commits)

---
updated-dependencies:
- dependency-name: "@radix-ui/react-accordion"
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: radix-ui
- dependency-name: "@radix-ui/react-alert-dialog"
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: radix-ui
- dependency-name: "@radix-ui/react-avatar"
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: radix-ui
- dependency-name: "@radix-ui/react-checkbox"
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: radix-ui
- dependency-name: "@radix-ui/react-dialog"
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: radix-ui
- dependency-name: "@radix-ui/react-dropdown-menu"
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: radix-ui
- dependency-name: "@radix-ui/react-hover-card"
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: radix-ui
- dependency-name: "@radix-ui/react-label"
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: radix-ui
- dependency-name: "@radix-ui/react-popover"
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: radix-ui
- dependency-name: "@radix-ui/react-scroll-area"
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: radix-ui
- dependency-name: "@radix-ui/react-select"
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: radix-ui
- dependency-name: "@radix-ui/react-separator"
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: radix-ui
- dependency-name: "@radix-ui/react-slider"
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: radix-ui
- dependency-name: "@radix-ui/react-slot"
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: radix-ui
- dependency-name: "@radix-ui/react-switch"
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: radix-ui
- dependency-name: "@radix-ui/react-tabs"
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: radix-ui
- dependency-name: "@radix-ui/react-toggle"
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: radix-ui
- dependency-name: "@radix-ui/react-tooltip"
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: radix-ui
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-30 16:04:22 +02:00
Max DeichmannandGitHub 6045324d2b fix: fix rate limits for orgs with stripe details (#3164) 2024-08-30 13:53:47 +00:00
Marc KlingenandGitHub 87923e5d6c fix(ui): add default llm base urls for reference in llm api key form (#3163)
* ui nit

* fix(ui): add default llm base urls for reference in llm api key form
2024-08-30 13:28:07 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
f584de7577 chore(deps): bump zod-to-json-schema from 3.22.5 to 3.23.2 (#3161)
Bumps [zod-to-json-schema](https://github.com/StefanTerdell/zod-to-json-schema) from 3.22.5 to 3.23.2.
- [Release notes](https://github.com/StefanTerdell/zod-to-json-schema/releases)
- [Changelog](https://github.com/StefanTerdell/zod-to-json-schema/blob/master/changelog.md)
- [Commits](https://github.com/StefanTerdell/zod-to-json-schema/commits)

---
updated-dependencies:
- dependency-name: zod-to-json-schema
  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-08-30 13:55:34 +02:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
d01c403f62 chore(deps): bump kysely from 0.27.3 to 0.27.4 (#3160)
Bumps [kysely](https://github.com/kysely-org/kysely) from 0.27.3 to 0.27.4.
- [Release notes](https://github.com/kysely-org/kysely/releases)
- [Commits](https://github.com/kysely-org/kysely/compare/0.27.3...0.27.4)

---
updated-dependencies:
- dependency-name: kysely
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-30 13:38:28 +02:00
Max DeichmannandGitHub a001e6072c chore: run snyk on pr pushes only (#3158) 2024-08-30 12:17:12 +02:00
Max DeichmannandGitHub d9a3f977bc chore: radix dependabot group (#3157) 2024-08-30 11:57:58 +02:00
Max DeichmannandGitHub 8fae4f3051 feat: add custom rate limits for organisations (#2976) 2024-08-30 11:57:31 +02:00
Max DeichmannandGitHub c36b1ff51d chore: dependabot gorup for lodash (#3155)
smaller fixes
2024-08-30 11:17:51 +02:00
Max DeichmannandGitHub d3cf604f4f chore: increase dependabot limit (#3148) 2024-08-30 10:51:41 +02:00
dependabot[bot]andGitHub cc886917b6 chore(deps): bump @radix-ui/react-toggle-group from 1.0.4 to 1.1.0 (#3146) 2024-08-30 08:51:05 +02:00
Marc KlingenandGitHub b924bf9e09 feat(auth): improve password reset error messages (#3143) 2024-08-29 22:02:57 +00:00
Marc Klingen 895825dd1c chore: release v2.75.0
CI/CD / lint (push) Waiting to run
CI/CD / test-docker-build (push) Waiting to run
CI/CD / tests-web (node20, pg12) (push) Waiting to run
CI/CD / tests-web (node20, pg15) (push) Waiting to run
CI/CD / tests-worker (node20, pg12) (push) Waiting to run
CI/CD / tests-worker (node20, pg15) (push) Waiting to run
CI/CD / e2e-tests (push) Waiting to run
CI/CD / e2e-server-tests (push) Waiting to run
CI/CD / all-ci-passed (push) Blocked by required conditions
CI/CD / push-docker-image (push) Blocked by required conditions
release.yml / release (push) Waiting to run
2024-08-29 18:52:21 +02:00
Max DeichmannandGitHub 6ceccb649f chore: fix worker tests (#3133) 2024-08-29 16:29:56 +02:00
Max DeichmannandGitHub 911021ac5d chore: ensure clickhouse is healthy before migrating (#3128) 2024-08-29 13:58:02 +00:00
marliessophieandGitHub f39778ef9f chore(ui): render cloudflare errors in more user readable format (#3118) 2024-08-29 10:26:55 +00:00
Marc KlingenandGitHub 4d00e39c6b fix(ui): reset to first table page when number of total pages is reduced (#3122) 2024-08-28 21:58:14 +00:00
Marc Klingen 7af49fd649 chore(ui): adjust padding in settings 2024-08-28 23:30:16 +02:00
Max DeichmannandGitHub 662f1917b6 fix: create fast hashed secret key upon key creation (#3117) 2024-08-28 20:14:12 +02:00
Marc KlingenandGitHub d8f047f2a1 chore(ui): improve trpc error types (#3097) 2024-08-28 19:03:27 +02:00
69815a51b9 feat: add trace tags to generations, sessions and score table (#3109)
* feat: Add query to retrieve tags for a specific project

* chore(api): Add "tags" column to scores table definition

* add nullable to score table config

* chore(api): Parallelize filter options queries

* chore(api): Update datetime filter function name to use raw SQL

* chore(api): Update datetime filter function name to use raw SQL

* chore: Update datetime filter function name to use raw SQL

* chore: Update scores router to include timestamp filter in query

* chore: Update datetime filter function name to use raw SQL

* chore: Update datetime filter function name to use raw SQL

* chore: Update datetime filter function name to use raw SQL

* refactor: Use datetimeFilterToPrisma function for start time filter

* refactor: Use datetimeFilterToPrisma function for start time filter

* refactor: Use datetimeFilterToPrisma function for timestamp filter

* refactor: Update datetime filter function name to use raw SQL

* refactor: Add viewOnly prop to TagList component

* refactor: Add viewOnly prop to TagButton component

* feat: Add trace tags to GenerationsTable

* refactor: Add traceTags column to ScoresTable

* feat: Add traceTags column to SessionsTable

* refactor: Add traceTags column to ScoresTable

* feat: Add traceTags to session metrics

* feat: Add traceTags to getAllGenerationsSqlQuery

* revert rename

* push

* add row height to sessions

* wrap if non-s row height

---------

Co-authored-by: Marc Klingen <git@marcklingen.com>
2024-08-28 19:01:49 +02:00
Marc KlingenandGitHub 2bb419213a feat(ui): reduce font size and white-space to shrink UI to fit more content (#3001) 2024-08-28 19:00:55 +02:00
Richard KrümmelandGitHub a330035c40 feat: add tags filter to scores table (#3071) 2024-08-28 17:11:25 +02:00
Max DeichmannandGitHub dd4434fe25 chore: set keepalive on web docker container (#3113) 2024-08-28 15:00:34 +00:00
Max DeichmannandGitHub f898f57bff chore: upgrade dd-trace (#3114)
fix
2024-08-28 14:15:32 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
eda52c7b77 chore(deps-dev): bump prettier-plugin-tailwindcss from 0.5.14 to 0.6.6 (#3103)
Bumps [prettier-plugin-tailwindcss](https://github.com/tailwindlabs/prettier-plugin-tailwindcss) from 0.5.14 to 0.6.6.
- [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.14...v0.6.6)

---
updated-dependencies:
- dependency-name: prettier-plugin-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>
2024-08-28 16:01:30 +02:00
marliessophieandGitHub e5c7686bc7 fix(comments): reference object check for comment creation (#3108) 2024-08-28 08:57:48 +00:00
marliessophieandGitHub 251be1a240 fix(comments): reintroduce old procedure name for backwards compatibility (#3107) 2024-08-28 08:15:42 +00:00
marliessophieandGitHub 66c810e4a7 chore(comments): fetch comment counts by type in trace tree (#3098) 2024-08-27 16:30:02 +00:00
marliessophieandGitHub c3c8a741e4 chore(ui): style TRPC error toast to differentiate between errors and warnings (#3095) 2024-08-27 17:28:32 +02:00
Marc KlingenandGitHub 81c898b4ed feat(ui): jump to page x in table via input in pagination (#3090) 2024-08-27 17:04:46 +02:00
834d0bcc7a feat: add tag filter to generations table (#3022)
* feat(api): add tag filtering to generation filter options query

* chore: add Trace Tags column to observations table

* feat(api): optimize filterOptionsQuery by removing count property

* chore(api): optimize filterOptionsQuery for better performance

* feat: add tags filter to sessions table (#3067)



---------

Co-authored-by: Marc Klingen <git@marcklingen.com>

* refactor: use DISTINCT keyword in filterOptionsQuery

---------

Co-authored-by: Marc Klingen <git@marcklingen.com>
2024-08-27 15:19:10 +02:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
40953d7732 chore(deps): bump pino-http from 9.0.0 to 10.2.0 (#3091)
Bumps [pino-http](https://github.com/pinojs/pino-http) from 9.0.0 to 10.2.0.
- [Release notes](https://github.com/pinojs/pino-http/releases)
- [Commits](https://github.com/pinojs/pino-http/compare/v9.0.0...v10.2.0)

---
updated-dependencies:
- dependency-name: pino-http
  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-08-27 15:05:05 +02:00
Max DeichmannandGitHub d471be8783 fix: fix async instrumentation (#3094) 2024-08-27 12:03:47 +00:00
marliessophieandGitHub 0ca38d8070 chore(evals): display eval config sampling rate in UI (#3093) 2024-08-27 11:13:19 +00:00
Max DeichmannandGitHub 12458e3d71 chore: add instrumentation to auth apis (#3069) 2024-08-27 09:44:32 +00:00
Max DeichmannandGitHub 51575b5491 chore: fix dependabot observability group (#3086)
fix
2024-08-27 10:47:30 +02:00
Max DeichmannandGitHub a9c11f3323 chore: create dependabot observability group (#3085) 2024-08-27 10:42:42 +02:00
Max DeichmannandGitHub 066140c903 fix: fix grouping of 3 months dashboard (#3080) 2024-08-27 08:40:32 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
0b7de157ad chore(deps): bump tailwind-merge from 2.2.2 to 2.5.2 (#3060)
Bumps [tailwind-merge](https://github.com/dcastil/tailwind-merge) from 2.2.2 to 2.5.2.
- [Release notes](https://github.com/dcastil/tailwind-merge/releases)
- [Commits](https://github.com/dcastil/tailwind-merge/compare/v2.2.2...v2.5.2)

---
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>
2024-08-27 10:20:55 +02:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
7dcc718c77 chore(deps-dev): bump tsx from 4.7.2 to 4.18.0 (#3066)
Bumps [tsx](https://github.com/privatenumber/tsx) from 4.7.2 to 4.18.0.
- [Release notes](https://github.com/privatenumber/tsx/releases)
- [Changelog](https://github.com/privatenumber/tsx/blob/master/release.config.cjs)
- [Commits](https://github.com/privatenumber/tsx/compare/v4.7.2...v4.18.0)

---
updated-dependencies:
- dependency-name: tsx
  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-08-26 22:08:43 +02:00
marliessophieandGitHub e098618614 chore(filters): improve filter table type safety (#3078) 2024-08-26 14:45:28 +00:00
marliessophieandGitHub 3e8ba54aa0 chore(tables): persist date range in session storage (#3076) 2024-08-26 15:44:05 +02:00
Marc KlingenandGitHub ea36cd58a4 chore(ui): disable jump to last page while count pages is loading (#3077) 2024-08-26 14:38:05 +02:00
Marc KlingenandGitHub c7b726a73a perf(ui): split totalCount from scores.all to reduce loading time of scores table (#3075) 2024-08-26 14:11:10 +02:00
Marc KlingenandGitHub 112e7a2dcf perf(ui): split totalCount from sessions.all to reduce loading time of sessions table (#3073) 2024-08-26 14:03:14 +02:00
Marc Klingen c1d98aba6a fix(ui): remove artificial wait time used in testing 2024-08-26 13:41:31 +02:00
Marc KlingenandGitHub 75fd1cbd3f perf(ui): split totalCount from generations.all to reduce loading time of generations table (#3072) 2024-08-26 13:40:29 +02:00
Marc KlingenandGitHub 75ec734a45 perf(ui): split total count query on trace table to reduce loading time (#3070) 2024-08-26 13:24:54 +02:00
marliessophieandGitHub 98e951423c chore(filters): persist table and dashboard filter state in session storage (#3036) 2024-08-26 11:05:58 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
8e1dc3ee99 chore(deps): bump core-js from 3.36.1 to 3.38.1 (#3058)
Bumps [core-js](https://github.com/zloirock/core-js/tree/HEAD/packages/core-js) from 3.36.1 to 3.38.1.
- [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.38.1/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-08-26 12:35:47 +02:00
Marc KlingenandGitHub 7668995ec9 perf(ui): increase speed of tables, no nulls first/last modifier when sorting by non-nullable attributes (#3068) 2024-08-26 11:30:31 +02:00
marliessophieandGitHub 06233f878c chore(comments): disable comments count on public traces (#3065) 2024-08-26 09:13:58 +00:00
Max DeichmannandGitHub dae3d4e4e2 fix: remove dd profiling presets (#3063)
fix
2024-08-24 17:36:50 +02:00
Max DeichmannandGitHub 04dca2ad6b perf: use dedicated redis clients for bullmq (#3052) 2024-08-23 16:54:39 +00:00
Marc KlingenandGitHub 48c61ad77f perf(ui): load metrics in trace table independently to reduce initial table load time (#3051) 2024-08-23 17:39:32 +02:00
Marc KlingenandGitHub 34140302a6 perf(ui): load metrics in session table independently to reduce initial table load time (#3048) 2024-08-23 13:28:32 +02:00
Marc KlingenandGitHub 93cc36ecdc fix(cloud): catch crisp chat exception (#3042) 2024-08-23 13:22:36 +02:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
ace30782a0 chore(deps): bump @radix-ui/react-collapsible from 1.0.3 to 1.1.0 (#3031)
Bumps [@radix-ui/react-collapsible](https://github.com/radix-ui/primitives) from 1.0.3 to 1.1.0.
- [Changelog](https://github.com/radix-ui/primitives/blob/main/release-process.md)
- [Commits](https://github.com/radix-ui/primitives/commits)

---
updated-dependencies:
- dependency-name: "@radix-ui/react-collapsible"
  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-08-23 13:04:49 +02:00
Marc KlingenandGitHub 3c94a0cda9 perf(ui): parallelize db query for sessions.totalCount (#3044) 2024-08-23 09:58:58 +00:00
Max DeichmannandGitHub b0fea44556 feat: sentry capture console.error in frontend (#3043)
fix
2024-08-23 08:30:42 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
ed91330c37 chore(deps): bump lucide-react from 0.364.0 to 0.429.0 (#3037)
Bumps [lucide-react](https://github.com/lucide-icons/lucide/tree/HEAD/packages/lucide-react) from 0.364.0 to 0.429.0.
- [Release notes](https://github.com/lucide-icons/lucide/releases)
- [Commits](https://github.com/lucide-icons/lucide/commits/0.429.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-08-23 10:16:58 +02:00
Max DeichmannandGitHub b8ed9d681b fix: free up tokenizer at worker shutdown (#3041) 2024-08-23 08:02:57 +00:00
Max DeichmannandGitHub ea2e1b4873 fix: security policy for posthog (#3040)
fix
2024-08-23 07:32:55 +00:00
Max DeichmannandGitHub 3ddef14e75 fix: add sentry frontend support (#3012) 2024-08-23 06:41:27 +00:00
Marc KlingenandGitHub 4658942701 perf(ui): parallelize db queries in traces.all (#3038) 2024-08-23 01:32:33 +02:00
Marc KlingenandGitHub 51be8f8fb3 perf(ui): parallelize users, sessions, models, score config, scores queries in trpc routes (#3039)
* prompts

* models

* score config

* scores

* sessions
2024-08-22 23:26:59 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
6ede3b7690 chore(deps): bump the next group with 2 updates (#3030)
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.2.3 to 14.2.6
- [Release notes](https://github.com/vercel/next.js/releases)
- [Changelog](https://github.com/vercel/next.js/blob/canary/release.js)
- [Commits](https://github.com/vercel/next.js/compare/v14.2.3...v14.2.6)

Updates `eslint-config-next` from 14.2.3 to 14.2.6
- [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.2.6/packages/eslint-config-next)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-22 22:51:08 +00:00
Max DeichmannandGitHub dee36bbe29 chore: remove instrumentation from prisma dependabot group (#3033) 2024-08-23 00:31:28 +02:00
Max DeichmannandGitHub f71cf2f894 fix: remove dependabot patch group (#3027) 2024-08-22 23:52:35 +02:00
Marc KlingenandGitHub 1a707fe147 fix(ui): layout entitlements (#3026)
* fix(ui): layout entitlements

* push
2024-08-22 21:51:09 +00:00
Marc KlingenandGitHub 8da1640777 perf(ui): parallelize db queries in users.all route (#3018) 2024-08-22 23:24:14 +02:00
Max DeichmannandGitHub 1e56499f00 chore: upgrade bull (#3025)
fix
2024-08-22 21:23:18 +00:00
Max DeichmannandGitHub 2524fb9648 fix: otel error formatting (#3024) 2024-08-22 19:29:31 +00:00
marliessophieandGitHub 91cd86a286 chore(ui): Add free text option to dynamically populated filter options (#3020) 2024-08-22 18:23:42 +02:00
Lucas MagnumandGitHub e6b9d23615 fix(auth): Cannot read properties of undefined, GoogleProfile.hd (#3019) 2024-08-22 17:36:20 +02:00
Marc KlingenandGitHub 8b76feeffa perf(ui): parallelize db queries in filterOptions routes (#3016) 2024-08-22 15:27:35 +00:00
88a53dc72d perf(tables): drop filter option counts to reduce latency (#3013)
* perf(filters): rm counts from prompt filter options

* perf: drop counts from generations filter options

* perf: drop counts from trace filter options

---------

Co-authored-by: Marc Klingen <git@marcklingen.com>
2024-08-22 17:06:15 +02:00
Marc KlingenandGitHub 649a9af91c perf(ui): simplify default traces.byId route to not load observations and scores for table use cases (#3010) 2024-08-22 16:19:02 +02:00
Marc KlingenandGitHub 876917d112 perf(ui): remove unused scores from observations.byId (#3011) 2024-08-22 16:17:51 +02:00
Max DeichmannandGitHub bb6bc79bdc fix: use serverComponentsExternalPackages for dd and otel (#3009) 2024-08-22 13:21:14 +00:00
Marc KlingenandGitHub 7aff2b2e61 perf(ui): parallelize db queries in byId trpc routes (#3006) 2024-08-22 14:45:36 +02:00
Max DeichmannandGitHub 8f742f660a fix: await closure of bull queues (#3007)
fix
2024-08-22 11:15:54 +00:00
225 changed files with 6170 additions and 3466 deletions
+7 -2
View File
@@ -139,12 +139,12 @@ ENCRYPTION_KEY="0000000000000000000000000000000000000000000000000000000000000000
# NEXT_PUBLIC_POSTHOG_KEY=
# Sentry
# NEXT_PUBLIC_LANGFUSE_TRACING_SAMPLE_RATE
# NEXT_PUBLIC_SENTRY_DSN=
# NEXT_SENTRY_ORG=
# NEXT_SENTRY_PROJECT=
# SENTRY_AUTH_TOKEN=
# Used to determine the Sentry sample rate
# LANGFUSE_TRACING_SAMPLE_RATE=
# SENTRY_CSP_REPORT_URI=
# LANGFUSE_WORKER_BETTERSTACK_TOKEN=
@@ -167,6 +167,7 @@ ENCRYPTION_KEY="0000000000000000000000000000000000000000000000000000000000000000
# REDIS_PORT=
# REDIS_AUTH=
# REDIS_CONNECTION_STRING=
# REDIS_ENABLE_AUTO_PIPELINING=
# Cache configuration
# LANGFUSE_CACHE_API_KEY_ENABLED=
@@ -174,6 +175,9 @@ ENCRYPTION_KEY="0000000000000000000000000000000000000000000000000000000000000000
# LANGFUSE_CACHE_PROMPT_ENABLED=
# LANGFUSE_CACHE_PROMPT_TTL_SECONDS=
# Rate limiting
# LANGFUSE_RATE_LIMITS_ENABLED=
# Stripe
# STRIPE_SECRET_KEY=
# STRIPE_WEBHOOK_SIGNING_SECRET=
@@ -200,5 +204,6 @@ ENCRYPTION_KEY="0000000000000000000000000000000000000000000000000000000000000000
# LANGFUSE_LOG_LEVEL=
# LANGFUSE_LEGACY_INGESTION_WORKER_CONCURRENCY=
# LANGFUSE_ASYNC_INGESTION_PROCESSING="true"
# QUEUE_CONSUMER_LEGACY_INGESTION_QUEUE_IS_ENABLED="true"
## END Langfuse V3 Ingestion
+16 -7
View File
@@ -8,6 +8,7 @@ updates:
- package-ecosystem: npm
directory: "/" # Location of package manifests
rebase-strategy: "disabled" # use dependabot-rebase-stale
open-pull-requests-limit: 10
schedule:
interval: "daily"
versioning-strategy: "increase"
@@ -19,17 +20,25 @@ updates:
- dependency-name: "@types/node"
- dependency-name: "@trpc/*"
groups:
sentry:
patterns:
- "@sentry/*"
prisma:
patterns:
- "prisma"
- "@prisma/*"
- "@prisma/client"
next:
patterns:
- "eslint-config-next"
- "next"
patches:
update-types:
- "patch"
lodash:
patterns:
- "lodash"
- "@types/lodash"
observability:
patterns:
- "dd-trace"
- "@opentelemetry/*"
- "@appsignal/opentelemetry-instrumentation-bullmq"
- "@prisma/instrumentation"
- "@sentry/*"
radix-ui:
patterns:
- "@radix-ui/*"
+15 -2
View File
@@ -111,6 +111,10 @@ jobs:
- name: Build
run: pnpm run build
env:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_ORG: ${{ vars.SENTRY_ORG }}
SENTRY_PROJECT: ${{ vars.SENTRY_PROJECT }}
- name: Start Langfuse
run: (pnpm run start&)
@@ -136,6 +140,7 @@ jobs:
- uses: pnpm/action-setup@v3
with:
version: 9.5.0
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with:
@@ -165,18 +170,26 @@ jobs:
sleep 5 # Wait for PostgreSQL to accept connections
docker compose ps
- name: Ensure no unhealthy status
run: |
if docker compose ps | grep "(unhealthy)"; then
echo "One or more services are unhealthy"
exit 1
else
echo "All services are healthy"
fi
- name: Seed DB
run: |
pnpm run --filter=shared ch:up
pnpm run db:migrate
pnpm run db:seed
pnpm run --filter=shared ch:up
- name: Build
run: pnpm --filter=worker... run build
- name: run tests
run: pnpm --filter=worker run test
e2e-tests:
runs-on: ubuntu-latest
steps:
+3 -3
View File
@@ -1,11 +1,11 @@
name: Snyk Container
on:
push:
branches-ignore:
- "dependabot/**"
pull_request:
branches:
- "**"
push:
branches:
- main
jobs:
snyk:
+1 -1
View File
@@ -26,7 +26,7 @@
"dependencies": {
"@langfuse/shared": "workspace:*",
"@opentelemetry/api": ">=1.0.0 <1.9.0",
"next": "^14.2.3",
"next": "^14.2.6",
"next-auth": "^4.24.7",
"zod": "^3.23.8"
},
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "langfuse",
"version": "2.74.0",
"version": "2.76.0",
"author": "engineering@langfuse.com",
"license": "MIT",
"private": true,
+1 -1
View File
@@ -11,7 +11,7 @@
"@typescript-eslint/eslint-plugin": "^7.1.0",
"@typescript-eslint/parser": "^7.12.0",
"@vercel/style-guide": "^6.0.0",
"eslint-config-next": "^14.2.3",
"eslint-config-next": "^14.2.6",
"eslint-config-prettier": "^9.1.0",
"eslint-config-turbo": "^1.13.4",
"eslint-plugin-only-warn": "^1.1.0",
+1
View File
@@ -18,5 +18,6 @@ if [ "$CLICKHOUSE_MIGRATION_SSL" = true ] ; then
else
DATABASE_URL="${CLICKHOUSE_MIGRATION_URL}?username=${CLICKHOUSE_USER}&password=${CLICKHOUSE_PASSWORD}&database=default&x-multi-statement=true&x-migrations-table-engine=MergeTree"
fi
# Execute the up command
migrate -source file://clickhouse/migrations -database "$DATABASE_URL" up
+5 -5
View File
@@ -63,12 +63,12 @@
"@react-email/render": "^0.0.15",
"@types/bcryptjs": "^2.4.6",
"bcryptjs": "^2.4.3",
"bullmq": "^5.4.2",
"bullmq": "^5.12.10",
"dd-trace": "^5.19.0",
"decimal.js": "^10.4.3",
"exponential-backoff": "^3.1.1",
"dd-trace": "^5.19.0",
"ioredis": "^5.4.1",
"kysely": "^0.27.3",
"kysely": "^0.27.4",
"langchain": "^0.2.6",
"lodash": "^4.17.21",
"next-auth": "^4.24.7",
@@ -76,12 +76,12 @@
"prisma-extension-kysely": "^2.1.0",
"uuid": "^9.0.1",
"zod": "^3.23.8",
"zod-to-json-schema": "^3.22.5"
"zod-to-json-schema": "^3.23.2"
},
"devDependencies": {
"@repo/eslint-config": "workspace:*",
"@repo/typescript-config": "workspace:*",
"@types/lodash": "^4.17.5",
"@types/lodash": "^4.17.7",
"@types/node": "^20.11.29",
"@types/nodemailer": "^6.4.15",
"@types/pg": "^8.11.6",
+1
View File
@@ -16,6 +16,7 @@ const EnvSchema = z.object({
.nullable(),
REDIS_AUTH: z.string().nullish(),
REDIS_CONNECTION_STRING: z.string().nullish(),
REDIS_ENABLE_AUTO_PIPELINING: z.enum(["true", "false"]).default("true"),
ENCRYPTION_KEY: z
.string()
.length(
@@ -12,3 +12,6 @@ export const plans = Object.keys(planLabels) as Plan[];
// This function is kept here to ensure consistency when updating plan names in the future.
export const isCloudPlan = (plan: Plan) => plan.startsWith("cloud");
export const isPlan = (value: string): value is Plan =>
plans.includes(value as Plan);
+18 -1
View File
@@ -1,7 +1,8 @@
import { Prisma } from "@prisma/client";
import { ColumnDefinition, type TableNames } from "./tableDefinitions";
import { FilterState } from "./types";
import { filterOperators } from "./interfaces/filters";
import { filterOperators, timeFilter } from "./interfaces/filters";
import { z } from "zod";
const operatorReplacements = {
"any of": "IN",
@@ -179,3 +180,19 @@ export const datetimeFilterToPrismaSql = (
operator
)} ${value}::timestamp with time zone at time zone 'UTC'`;
};
export const datetimeFilterToPrisma = (
timestampFilter: z.infer<typeof timeFilter>
) => {
const prismaTimestampFilter =
timestampFilter.operator === ">="
? { gte: timestampFilter.value }
: timestampFilter.operator === ">"
? { gt: timestampFilter.value }
: timestampFilter.operator === "<="
? { lte: timestampFilter.value }
: timestampFilter.operator === "<"
? { lt: timestampFilter.value }
: {};
return prismaTimestampFilter;
};
+2
View File
@@ -15,6 +15,8 @@ export * from "./utils/zod";
export * from "./utils/json";
export * from "./utils/objects";
export * from "./utils/typeChecks";
export * from "./features/entitlements/plans";
export * from "./interfaces/rate-limits";
export { env } from "./env";
// llm api
@@ -1,4 +1,5 @@
import { z } from "zod";
import { CloudConfigRateLimit } from "./rate-limits";
export const CloudConfigSchema = z.object({
plan: z.enum(["Hobby", "Pro", "Team", "Enterprise"]).optional(),
@@ -13,5 +14,9 @@ export const CloudConfigSchema = z.object({
activeProductId: z.string().optional(),
})
.optional(),
// custom rate limits for an organization
rateLimitOverrides: CloudConfigRateLimit.optional(),
});
export type CloudConfigSchema = z.infer<typeof CloudConfigSchema>;
@@ -0,0 +1,30 @@
import z from "zod";
import { ApiAccessScope } from "../server";
export const RateLimitResource = z.enum([
"ingestion",
"public-api",
"public-api-metrics",
"prompts",
]);
// result of the rate limit check.
export type RateLimitResult = {
resource: z.infer<typeof RateLimitResource>;
points: number;
scope: ApiAccessScope;
// from rate-limiter-flexible
remainingPoints: number;
msBeforeNext: number;
consumedPoints: number;
isFirstInDuration: boolean;
};
export const RateLimitConfig = z.object({
points: z.number().nullish(),
durationInSec: z.number().nullish(),
resource: RateLimitResource,
});
export const CloudConfigRateLimit = z.array(RateLimitConfig);
+33 -1
View File
@@ -18,6 +18,7 @@ export const observationsTableCols: ColumnDefinition[] = [
type: "stringOptions",
internal: 'o."name"',
options: [], // to be added at runtime
nullable: true,
},
{ name: "Trace ID", id: "traceId", type: "string", internal: 't."id"' },
{
@@ -26,8 +27,15 @@ export const observationsTableCols: ColumnDefinition[] = [
type: "stringOptions",
internal: 't."name"',
options: [], // to be added at runtime
nullable: true,
},
{
name: "User ID",
id: "userId",
type: "string",
internal: 't."user_id"',
nullable: true,
},
{ name: "User ID", id: "userId", type: "string", internal: 't."user_id"' },
{
name: "Start Time",
id: "startTime",
@@ -45,6 +53,7 @@ export const observationsTableCols: ColumnDefinition[] = [
id: "timeToFirstToken",
type: "number",
internal: 'o."completion_start_time" - o."start_time"',
nullable: true,
},
{
name: "Latency (s)",
@@ -57,24 +66,28 @@ export const observationsTableCols: ColumnDefinition[] = [
id: "timePerOutputToken",
type: "number",
internal: '"latency" / o."completion_tokens"',
nullable: true,
},
{
name: "Input Cost ($)",
id: "inputCost",
type: "number",
internal: 'o."calculated_input_cost"',
nullable: true,
},
{
name: "Output Cost ($)",
id: "outputCost",
type: "number",
internal: 'o."calculated_output_cost"',
nullable: true,
},
{
name: "Total Cost ($)",
id: "totalCost",
type: "number",
internal: 'o."calculated_total_cost"',
nullable: true,
},
{
name: "Level",
@@ -88,6 +101,7 @@ export const observationsTableCols: ColumnDefinition[] = [
id: "statusMessage",
type: "string",
internal: 'o."status_message"',
nullable: true,
},
{
name: "Model",
@@ -95,24 +109,28 @@ export const observationsTableCols: ColumnDefinition[] = [
type: "stringOptions",
internal: 'o."model"',
options: [], // to be added at runtime
nullable: true,
},
{
name: "Input Tokens",
id: "inputTokens",
type: "number",
internal: 'o."prompt_tokens"',
nullable: true,
},
{
name: "Output Tokens",
id: "outputTokens",
type: "number",
internal: 'o."completion_tokens"',
nullable: true,
},
{
name: "Total Tokens",
id: "totalTokens",
type: "number",
internal: 'o."total_tokens"',
nullable: true,
},
{
name: "Usage",
@@ -137,6 +155,7 @@ export const observationsTableCols: ColumnDefinition[] = [
id: "version",
type: "string",
internal: 'o."version"',
nullable: true,
},
{
name: "Prompt Name",
@@ -144,12 +163,21 @@ export const observationsTableCols: ColumnDefinition[] = [
type: "stringOptions",
internal: "p.name",
options: [], // to be added at runtime
nullable: true,
},
{
name: "Prompt Version",
id: "promptVersion",
type: "number",
internal: "p.version",
nullable: true,
},
{
name: "Trace Tags",
id: "tags",
type: "arrayOptions",
internal: "t.tags",
options: [], // to be added at runtime
},
];
@@ -161,6 +189,7 @@ export type ObservationOptions = {
traceName: Array<OptionsDefinition>;
scores_avg: Array<string>;
promptName: Array<OptionsDefinition>;
tags: Array<OptionsDefinition>;
};
export function observationsTableColsWithOptions(
@@ -182,6 +211,9 @@ export function observationsTableColsWithOptions(
if (col.id === "promptName") {
return { ...col, options: options?.promptName ?? [] };
}
if (col.id === "tags") {
return { ...col, options: options?.tags ?? [] };
}
return col;
});
}
+2 -2
View File
@@ -16,7 +16,7 @@ export function orderByToPrismaSql(
tableColumns: ColumnDefinition[]
): Prisma.Sql {
if (!orderBy) {
return Prisma.sql`ORDER BY t.timestamp DESC NULLS LAST`;
return Prisma.sql`ORDER BY t.timestamp DESC`;
}
// Get column definition to map column to internal name, e.g. "t.id"
const col = tableColumns.find(
@@ -40,6 +40,6 @@ export function orderByToPrismaSql(
// Both column and order are safe, can use raw SQL
return Prisma.raw(
`ORDER BY ${col.internal} ${order.data} ${orderBy.order === "DESC" ? "NULLS LAST" : "NULLS FIRST"}`
`ORDER BY ${col.internal} ${order.data} ${col.nullable ? (orderBy.order === "DESC" ? "NULLS LAST" : "NULLS FIRST") : ""}`
);
}
@@ -16,8 +16,13 @@ const GetSessionTableSQLParamsSchema = z.object({
});
type GetSessionTableSQLParams = z.infer<typeof GetSessionTableSQLParamsSchema>;
export const getSessionTableSQL = (
params: GetSessionTableSQLParams
export const createSessionsAllQuery = (
select: Prisma.Sql,
params: GetSessionTableSQLParams,
options?: {
ignoreOrderBy?: boolean; // used by session.metrics and session.all.totalCount
sessionIdList?: string[]; // used by session.metrics
}
): Prisma.Sql => {
const { projectId, filter, orderBy, page, limit } =
GetSessionTableSQLParamsSchema.parse(params);
@@ -29,22 +34,13 @@ export const getSessionTableSQL = (
);
const orderByCondition = orderByToPrismaSql(orderBy, sessionsViewCols);
const sessionIdFilter = options?.sessionIdList
? Prisma.sql`AND s.id IN (${Prisma.join(options?.sessionIdList)})`
: Prisma.sql``;
const sql = Prisma.sql`
SELECT
s.id,
s. "created_at" AS "createdAt",
s.bookmarked,
s.public,
t. "userIds",
t. "countTraces",
o. "sessionDuration",
o. "totalCost" AS "totalCost",
o. "inputCost" AS "inputCost",
o. "outputCost" AS "outputCost",
o. "promptTokens" AS "promptTokens",
o. "completionTokens" AS "completionTokens",
o. "totalTokens" AS "totalTokens",
(count(*) OVER ())::int AS "totalCount"
${select}
FROM
trace_sessions AS s
LEFT JOIN LATERAL (
@@ -54,9 +50,12 @@ export const getSessionTableSQL = (
MIN(t. "timestamp") AS "min_timestamp",
array_agg(t.id) AS "traceIds",
array_agg(DISTINCT t.user_id) AS "userIds",
count(t.id)::int AS "countTraces"
count(t.id)::int AS "countTraces",
array_agg(DISTINCT u.tag) AS "tags"
FROM
traces t
LEFT JOIN LATERAL (
SELECT DISTINCT UNNEST(t.tags) AS tag) AS u ON TRUE
WHERE
t.project_id = ${projectId}
AND t.session_id = s.id
@@ -79,7 +78,8 @@ export const getSessionTableSQL = (
WHERE
s. "project_id" = ${projectId}
${filterCondition}
${orderByCondition}
${sessionIdFilter}
${options?.ignoreOrderBy ? Prisma.sql`` : orderByCondition}
LIMIT ${limit}
OFFSET ${page * limit}
`;
+1 -1
View File
@@ -1 +1 @@
export { getSessionTableSQL } from "./getSessionTableSQL";
export { createSessionsAllQuery } from "./createSessionsAllQuery";
+30
View File
@@ -1,3 +1,30 @@
import z from "zod";
import { Plan, plans } from "../../features/entitlements/plans";
import { CloudConfigRateLimit } from "../../interfaces/rate-limits";
export const OrgEnrichedApiKey = z.object({
id: z.string(),
note: z.string().nullable(),
publicKey: z.string(),
displaySecretKey: z.string(),
createdAt: z.string().datetime().nullable(),
lastUsedAt: z.string().datetime().nullable(),
expiresAt: z.string().datetime().nullable(),
projectId: z.string(),
fastHashedSecretKey: z.string(),
hashedSecretKey: z.string(),
orgId: z.string(),
plan: z.enum(plans as unknown as [string, ...string[]]),
rateLimitOverrides: CloudConfigRateLimit.nullish(),
});
export const API_KEY_NON_EXISTENT = "api-key-non-existent";
export const CachedApiKey = z.union([
OrgEnrichedApiKey,
z.literal(API_KEY_NON_EXISTENT),
]);
export type AuthHeaderVerificationResult =
| AuthHeaderValidVerificationResult
| {
@@ -13,4 +40,7 @@ export type AuthHeaderValidVerificationResult = {
export type ApiAccessScope = {
projectId: string;
accessLevel: "all" | "scores";
orgId: string;
plan: Plan;
rateLimitOverrides: z.infer<typeof CloudConfigRateLimit>;
};
@@ -12,18 +12,18 @@ import {
sdkLogEvent,
} from "../types";
import { validateAndInflateScore } from "../validateAndInflateScore";
import { ApiAccessScope } from "../../auth/types";
import { Trace, Observation, Score, Prisma, Model } from "@prisma/client";
import { ForbiddenError, LangfuseNotFoundError } from "../../../errors";
import { mergeJson } from "../../../utils/json";
import { jsonSchema } from "../../../utils/zod";
import { prisma } from "../../../db";
import { LegacyIngestionAccessScope } from ".";
export interface EventProcessor {
auth(apiScope: ApiAccessScope): void;
auth(apiScope: LegacyIngestionAccessScope): void;
process(
apiScope: ApiAccessScope
apiScope: LegacyIngestionAccessScope
): Promise<Trace | Observation | Score> | undefined;
}
@@ -46,7 +46,7 @@ export class ObservationProcessor implements EventProcessor {
}
async convertToObservation(
apiScope: ApiAccessScope,
apiScope: LegacyIngestionAccessScope,
existingObservation: Observation | null
): Promise<{
id: string;
@@ -398,12 +398,12 @@ export class ObservationProcessor implements EventProcessor {
};
}
auth(apiScope: ApiAccessScope): void {
auth(apiScope: LegacyIngestionAccessScope): void {
if (apiScope.accessLevel !== "all")
throw new ForbiddenError("Access denied for observation creation");
}
async process(apiScope: ApiAccessScope): Promise<Observation> {
async process(apiScope: LegacyIngestionAccessScope): Promise<Observation> {
this.auth(apiScope);
const existingObservation = this.event.body.id
@@ -441,13 +441,13 @@ export class TraceProcessor implements EventProcessor {
this.event = event;
}
auth(apiScope: ApiAccessScope): void {
auth(apiScope: LegacyIngestionAccessScope): void {
if (apiScope.accessLevel !== "all")
throw new ForbiddenError("Access denied for trace creation");
}
async process(
apiScope: ApiAccessScope
apiScope: LegacyIngestionAccessScope
): Promise<Trace | Observation | Score> {
const { body } = this.event;
@@ -553,7 +553,7 @@ export class ScoreProcessor implements EventProcessor {
this.event = event;
}
auth(apiScope: ApiAccessScope) {
auth(apiScope: LegacyIngestionAccessScope) {
if (apiScope.accessLevel !== "scores" && apiScope.accessLevel !== "all")
throw new ForbiddenError(
`Access denied for score creation, ${apiScope.accessLevel}`
@@ -561,7 +561,7 @@ export class ScoreProcessor implements EventProcessor {
}
async process(
apiScope: ApiAccessScope
apiScope: LegacyIngestionAccessScope
): Promise<Trace | Observation | Score> {
const { body } = this.event;
@@ -613,7 +613,7 @@ export class SdkLogProcessor implements EventProcessor {
this.event = event;
}
auth(apiScope: ApiAccessScope) {
auth(apiScope: LegacyIngestionAccessScope) {
return;
}
@@ -14,7 +14,7 @@ import {
convertTraceUpsertEventsToRedisEvents,
getTraceUpsertQueue,
} from "../../redis/trace-upsert";
import { ApiAccessScope, AuthHeaderVerificationResult } from "../../auth/types";
import { ApiAccessScope } from "../../auth/types";
import { redis } from "../../redis/redis";
import { backOff } from "exponential-backoff";
import { Model } from "../../..";
@@ -31,9 +31,24 @@ type TokenCountInput = {
text: unknown;
};
export type LegacyIngestionAccessScope = Omit<
ApiAccessScope,
"orgId" | "plan" | "rateLimitOverrides"
>;
type LegacyIngestionAuthHeaderVerificationResult =
| {
validKey: true;
scope: LegacyIngestionAccessScope;
}
| {
validKey: false;
error: string;
};
export const handleBatch = async (
events: z.infer<typeof ingestionApiSchema>["batch"],
authCheck: AuthHeaderVerificationResult,
authCheck: LegacyIngestionAuthHeaderVerificationResult,
calculateTokenDelegate: (p: TokenCountInput) => number | undefined
) => {
console.log(`handling ingestion ${events.length} events`);
@@ -103,7 +118,7 @@ async function retry<T>(request: () => Promise<T>): Promise<T> {
const handleSingleEvent = async (
event: z.infer<typeof ingestionEvent>,
apiScope: ApiAccessScope,
apiScope: LegacyIngestionAccessScope,
calculateTokenDelegate: (p: {
model: Model;
text: unknown;
@@ -10,12 +10,38 @@ export type SpanCtx = {
traceScope?: string;
};
type CallbackFn<T> = () => T | Promise<T>;
type AsyncCallbackFn<T> = () => Promise<T>;
export function instrument<T>(
export async function instrumentAsync<T>(
ctx: SpanCtx,
callback: CallbackFn<T>
): T extends Promise<any> ? Promise<T> : T {
callback: AsyncCallbackFn<T>
): Promise<T> {
return await getTracer(ctx.traceScope ?? callback.name).startActiveSpan(
ctx.name,
{
root: ctx.rootSpan,
kind: ctx.spanKind,
},
async (span) => {
try {
const result = await callback();
span.end();
return result;
} catch (ex) {
traceException(ex as opentelemetry.Exception, span);
span.end();
throw ex;
}
}
);
}
type SyncCallbackFn<T> = () => T;
export function instrumentSync<T>(
ctx: SpanCtx,
callback: SyncCallbackFn<T>
): T {
return getTracer(ctx.traceScope ?? callback.name).startActiveSpan(
ctx.name,
{
@@ -23,30 +49,14 @@ export function instrument<T>(
kind: ctx.spanKind,
},
(span) => {
const handleResult = (result: T) => {
try {
const result = callback();
span.end();
return result;
};
const handleError = (ex: unknown) => {
} catch (ex) {
traceException(ex as opentelemetry.Exception, span);
span.end();
throw ex;
};
try {
const result = callback();
if (result instanceof Promise) {
return result
.then(handleResult)
.catch(handleError) as T extends Promise<any> ? Promise<T> : T;
} else {
return handleResult(result) as T extends Promise<any>
? Promise<T>
: T;
}
} catch (ex) {
return handleError(ex) as T extends Promise<any> ? Promise<T> : T;
}
}
);
@@ -67,9 +77,24 @@ export const traceException = (
const exception = {
code: code,
message: ex instanceof Error ? ex.message : String(ex),
name: ex instanceof Error ? ex.name : "Error",
stack: ex instanceof Error ? ex.stack : undefined,
message:
ex instanceof Error
? ex.message
: typeof ex === "object" && ex !== null && "message" in ex
? JSON.stringify(ex.message)
: JSON.stringify(ex),
name:
ex instanceof Error
? ex.name
: typeof ex === "object" && ex !== null && "name" in ex
? JSON.stringify(ex.name)
: "Error",
stack:
ex instanceof Error
? JSON.stringify(ex.stack)
: typeof ex === "object" && ex !== null && "stack" in ex
? JSON.stringify(ex.stack)
: undefined,
};
// adds an otel event
@@ -1,6 +1,6 @@
import { Queue } from "bullmq";
import { QueueName, TQueueJobTypes } from "../queues";
import { redis } from "./redis";
import { createNewRedisInstance } from "./redis";
let batchExportQueue: Queue<TQueueJobTypes[QueueName.BatchExport]> | null =
null;
@@ -8,9 +8,11 @@ let batchExportQueue: Queue<TQueueJobTypes[QueueName.BatchExport]> | null =
export const getBatchExportQueue = () => {
if (batchExportQueue) return batchExportQueue;
batchExportQueue = redis
const connection = createNewRedisInstance();
batchExportQueue = connection
? new Queue<TQueueJobTypes[QueueName.BatchExport]>(QueueName.BatchExport, {
connection: redis,
connection: connection,
defaultJobOptions: {
removeOnComplete: true,
removeOnFail: 100,
@@ -1,7 +1,7 @@
import { Queue } from "bullmq";
import { env } from "../../env";
import { redis } from "../redis/redis";
import { createNewRedisInstance } from "../redis/redis";
import { QueueName } from "../queues";
export type IngestionFlushQueue = Queue<null>;
@@ -11,9 +11,11 @@ let ingestionFlushQueue: IngestionFlushQueue | null = null;
export const getIngestionFlushQueue = () => {
if (ingestionFlushQueue) return ingestionFlushQueue;
ingestionFlushQueue = redis
const connection = createNewRedisInstance();
ingestionFlushQueue = connection
? new Queue<null>(QueueName.IngestionFlushQueue, {
connection: redis,
connection: connection,
defaultJobOptions: {
removeOnComplete: true, // Important: If not true, new jobs for that ID would be ignored as jobs in the complete set are still considered as part of the queue
removeOnFail: 1_000,
@@ -1,27 +1,33 @@
import { Queue } from "bullmq";
import { QueueName, TQueueJobTypes } from "../queues";
import { redis } from "./redis";
import { createNewRedisInstance } from "./redis";
let legacyIngestionQueue: Queue<
TQueueJobTypes[QueueName.LegacyIngestionQueue]
> | null = null;
export class LegacyIngestionQueue {
private static instance: Queue<
TQueueJobTypes[QueueName.LegacyIngestionQueue]
> | null = null;
export const getLegacyIngestionQueue = () => {
if (legacyIngestionQueue) return legacyIngestionQueue;
public static getInstance(): Queue<
TQueueJobTypes[QueueName.LegacyIngestionQueue]
> | null {
if (LegacyIngestionQueue.instance) return LegacyIngestionQueue.instance;
legacyIngestionQueue = redis
? new Queue<TQueueJobTypes[QueueName.LegacyIngestionQueue]>(
QueueName.LegacyIngestionQueue,
{
connection: redis,
defaultJobOptions: {
removeOnComplete: true,
removeOnFail: 100,
attempts: 5,
const newRedis = createNewRedisInstance({ enableOfflineQueue: false });
LegacyIngestionQueue.instance = newRedis
? new Queue<TQueueJobTypes[QueueName.LegacyIngestionQueue]>(
QueueName.LegacyIngestionQueue,
{
connection: newRedis,
defaultJobOptions: {
removeOnComplete: true,
removeOnFail: 100,
attempts: 5,
},
},
}
)
: null;
)
: null;
return legacyIngestionQueue;
};
return LegacyIngestionQueue.instance;
}
}
+23 -11
View File
@@ -1,18 +1,30 @@
import Redis from "ioredis";
import Redis, { RedisOptions } from "ioredis";
import { env } from "../../env";
export const createNewRedisInstance = (
additionalOptions: Partial<RedisOptions> = {},
) => {
return env.REDIS_CONNECTION_STRING
? new Redis(env.REDIS_CONNECTION_STRING, {
maxRetriesPerRequest: null,
enableAutoPipelining: env.REDIS_ENABLE_AUTO_PIPELINING === "true",
...additionalOptions,
})
: env.REDIS_HOST
? new Redis({
host: String(env.REDIS_HOST),
port: Number(env.REDIS_PORT),
password: String(env.REDIS_AUTH),
maxRetriesPerRequest: null, // Set to `null` to disable retrying
enableAutoPipelining: env.REDIS_ENABLE_AUTO_PIPELINING === "true",
...additionalOptions,
})
: null;
};
const createRedisClient = () => {
try {
return env.REDIS_CONNECTION_STRING
? new Redis(env.REDIS_CONNECTION_STRING, { maxRetriesPerRequest: null })
: env.REDIS_HOST
? new Redis({
host: String(env.REDIS_HOST),
port: Number(env.REDIS_PORT),
password: String(env.REDIS_AUTH),
maxRetriesPerRequest: null, // Set to `null` to disable retrying
})
: null;
return createNewRedisInstance();
} catch (e) {
console.error(e, "Failed to connect to redis");
return null;
@@ -6,7 +6,7 @@ import {
TraceUpsertEventType,
} from "../queues";
import { Queue } from "bullmq";
import { redis } from "./redis";
import { createNewRedisInstance } from "./redis";
let traceUpsertQueue: Queue<TQueueJobTypes[QueueName.TraceUpsert]> | null =
null;
@@ -14,9 +14,11 @@ let traceUpsertQueue: Queue<TQueueJobTypes[QueueName.TraceUpsert]> | null =
export const getTraceUpsertQueue = () => {
if (traceUpsertQueue) return traceUpsertQueue;
traceUpsertQueue = redis
const connection = createNewRedisInstance();
traceUpsertQueue = connection
? new Queue<TQueueJobTypes[QueueName.TraceUpsert]>(QueueName.TraceUpsert, {
connection: redis,
connection: connection,
defaultJobOptions: {
removeOnComplete: 100, // Important: If not true, new jobs for that ID would be ignored as jobs in the complete set are still considered as part of the queue
removeOnFail: 1_000,
@@ -17,6 +17,7 @@ export const sessionsViewCols: ColumnDefinition[] = [
type: "arrayOptions",
internal: 't."userIds"',
options: [], // to be filled in at runtime
nullable: true,
},
{
name: "Session Duration (s)",
@@ -78,10 +79,18 @@ export const sessionsViewCols: ColumnDefinition[] = [
type: "number",
internal: 'o."totalTokens"',
},
{
name: "Trace Tags",
id: "tags",
type: "arrayOptions",
internal: 't."tags"',
options: [], // to be filled in at runtime
},
];
export type SessionOptions = {
userIds: Array<OptionsDefinition>;
tags: Array<OptionsDefinition>;
};
export function sessionsTableColsWithOptions(
@@ -91,6 +100,9 @@ export function sessionsTableColsWithOptions(
if (col.id === "userIds") {
return { ...col, options: options?.userIds ?? [] };
}
if (col.id === "tags") {
return { ...col, options: options?.tags ?? [] };
}
return col;
});
}
@@ -9,6 +9,7 @@ export type ColumnDefinition =
id: string;
type: "number" | "string" | "datetime" | "boolean";
internal: string;
nullable?: boolean;
}
| {
name: string;
@@ -16,6 +17,7 @@ export type ColumnDefinition =
type: "stringOptions";
options: Array<OptionsDefinition>;
internal: string;
nullable?: boolean;
}
| {
name: string;
@@ -23,6 +25,7 @@ export type ColumnDefinition =
type: "arrayOptions";
options: Array<OptionsDefinition>;
internal: string;
nullable?: boolean;
}
| {
name: string;
@@ -30,6 +33,7 @@ export type ColumnDefinition =
type: "stringObject" | "numberObject";
internal: string;
keyOptions?: Array<string>;
nullable?: boolean;
};
export const tableNames = [
+18 -1
View File
@@ -15,6 +15,7 @@ const tracesOnlyCols: ColumnDefinition[] = [
type: "stringOptions",
internal: 't."name"',
options: [], // to be filled in at runtime
nullable: true,
},
{
name: "Timestamp",
@@ -22,12 +23,19 @@ const tracesOnlyCols: ColumnDefinition[] = [
type: "datetime",
internal: 't."timestamp"',
},
{ name: "User ID", id: "userId", type: "string", internal: 't."user_id"' },
{
name: "User ID",
id: "userId",
type: "string",
internal: 't."user_id"',
nullable: true,
},
{
name: "Session ID",
id: "sessionId",
type: "string",
internal: 't."session_id"',
nullable: true,
},
{
name: "Metadata",
@@ -40,12 +48,14 @@ const tracesOnlyCols: ColumnDefinition[] = [
id: "version",
type: "string",
internal: 't."version"',
nullable: true,
},
{
name: "Release",
id: "release",
type: "string",
internal: 't."release"',
nullable: true,
},
{
name: "Level",
@@ -69,24 +79,28 @@ export const tracesTableCols: ColumnDefinition[] = [
id: "inputTokens",
type: "number",
internal: 'tm."promptTokens"',
nullable: true,
},
{
name: "Output Tokens",
id: "outputTokens",
type: "number",
internal: 'tm."completionTokens"',
nullable: true,
},
{
name: "Total Tokens",
id: "totalTokens",
type: "number",
internal: 'tm."totalTokens"',
nullable: true,
},
{
name: "Usage",
id: "usage",
type: "number",
internal: 'tm."totalTokens"',
nullable: true,
},
{
@@ -106,18 +120,21 @@ export const tracesTableCols: ColumnDefinition[] = [
id: "inputCost",
type: "number",
internal: '"calculatedInputCost"',
nullable: true,
},
{
name: "Output Cost ($)",
id: "outputCost",
type: "number",
internal: '"calculatedOutputCost"',
nullable: true,
},
{
name: "Total Cost ($)",
id: "totalCost",
type: "number",
internal: '"calculatedTotalCost"',
nullable: true,
},
];
+1612 -1298
View File
File diff suppressed because it is too large Load Diff
+3 -1
View File
@@ -41,6 +41,8 @@ ARG NEXT_PUBLIC_TURNSTILE_SITE_KEY
ARG NEXT_PUBLIC_POSTHOG_KEY
ARG NEXT_PUBLIC_POSTHOG_HOST
ARG NEXT_PUBLIC_CRISP_WEBSITE_ID
ARG NEXT_PUBLIC_SENTRY_DSN
ARG NEXT_LANGFUSE_TRACING_SAMPLE_RATE
# Copy source code of isolated subworkspace
COPY --from=pruner /app/out/full/ .
@@ -100,4 +102,4 @@ ENV PORT 3000
ENTRYPOINT ["dumb-init", "--", "./web/entrypoint.sh"]
# startup command
CMD ["node", "./web/server.js"]
CMD ["node", "./web/server.js", "--keepAliveTimeout", "110000"]
+76 -49
View File
@@ -12,25 +12,47 @@ import { env } from "./src/env.mjs";
*/
const cspHeader = `
default-src 'self' https://*.langfuse.com https://*.posthog.com https://*.sentry.io wss://*.crisp.chat https://*.crisp.chat;
script-src 'self' 'unsafe-eval' https://*.langfuse.com https://*.crisp.chat https://challenges.cloudflare.com https://*.sentry.io https://ph.langfuse.com https://static.cloudflareinsights.com https://*.stripe.com;
style-src 'self' 'unsafe-inline' https://*.crisp.chat;
img-src 'self' https: blob: data:;
font-src 'self' https://*.crisp.chat;
frame-src 'self' https://challenges.cloudflare.com https://*.stripe.com;
script-src 'self' 'unsafe-eval' 'unsafe-inline' https://*.langfuse.com https://client.crisp.chat https://settings.crisp.chat https://challenges.cloudflare.com https://*.sentry.io https://ph.langfuse.com https://static.cloudflareinsights.com https://*.stripe.com;
style-src 'self' 'unsafe-inline' https://client.crisp.chat;
img-src 'self' https: blob: data: https://client.crisp.chat https://image.crisp.chat https://storage.crisp.chat;
font-src 'self' https://client.crisp.chat;
frame-src 'self' https://challenges.cloudflare.com https://*.stripe.com https://game.crisp.chat;
worker-src 'self' blob:;
object-src 'none';
base-uri 'self';
form-action 'self';
frame-ancestors 'none';
connect-src 'self' https://client.crisp.chat https://storage.crisp.chat wss://client.relay.crisp.chat wss://stream.relay.crisp.chat https://*.ingest.us.sentry.io https://ph.langfuse.com;
media-src 'self' https://client.crisp.chat;
${env.LANGFUSE_CSP_ENFORCE_HTTPS === "true" ? "upgrade-insecure-requests; block-all-mixed-content;" : ""}
${env.SENTRY_CSP_REPORT_URI ? `report-uri ${env.SENTRY_CSP_REPORT_URI}; report-to csp-endpoint;` : ""}
`;
const reportToHeader = {
key: "Report-To",
value: JSON.stringify({
group: "csp-endpoint",
max_age: 10886400,
endpoints: [
{
url: env.SENTRY_CSP_REPORT_URI,
},
],
include_subdomains: true,
}),
};
/** @type {import("next").NextConfig} */
const nextConfig = {
transpilePackages: ["@langfuse/shared"],
reactStrictMode: true,
experimental: {
instrumentationHook: true,
serverComponentsExternalPackages: [
"dd-trace",
"@opentelemetry/auto-instrumentations-node",
"@opentelemetry/api",
],
},
poweredByHeader: false,
@@ -67,6 +89,7 @@ const nextConfig = {
key: "Permissions-Policy",
value: "autoplay=*, fullscreen=*, microphone=*",
},
...(env.SENTRY_CSP_REPORT_URI ? [reportToHeader] : []),
],
},
{
@@ -81,22 +104,22 @@ const nextConfig = {
// Required to check authentication status from langfuse.com
...(env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION !== undefined
? [
{
source: "/api/auth/session",
headers: [
{
key: "Access-Control-Allow-Origin",
value: "https://langfuse.com",
},
{ key: "Access-Control-Allow-Credentials", value: "true" },
{ key: "Access-Control-Allow-Methods", value: "GET,POST" },
{
key: "Access-Control-Allow-Headers",
value: "Content-Type, Authorization",
},
],
},
]
{
source: "/api/auth/session",
headers: [
{
key: "Access-Control-Allow-Origin",
value: "https://langfuse.com",
},
{ key: "Access-Control-Allow-Credentials", value: "true" },
{ key: "Access-Control-Allow-Methods", value: "GET,POST" },
{
key: "Access-Control-Allow-Headers",
value: "Content-Type, Authorization",
},
],
},
]
: []),
// all files in /public/generated are public and can be accessed from any origin, e.g. to render an API reference based on our openapi schema
{
@@ -117,10 +140,6 @@ const nextConfig = {
// webassembly support for @dqbd/tiktoken
webpack(config, { isServer }) {
if (isServer) {
// https://github.com/open-telemetry/opentelemetry-js/issues/4173
config.ignoreWarnings = [{ module: /opentelemetry/ }];
}
config.experiments = {
asyncWebAssembly: true,
layers: true,
@@ -130,36 +149,44 @@ const nextConfig = {
},
};
const sentryOptions = {
// Additional config options for the Sentry Webpack plugin. Keep in mind that
// the following options are set automatically, and overriding them is not
// recommended:
// release, url, authToken, configFile, stripPrefix,
// urlPrefix, include, ignore
export default withSentryConfig(nextConfig, {
// For all available options, see:
// https://github.com/getsentry/sentry-webpack-plugin#options
org: process.env.SENTRY_ORG,
project: process.env.SENTRY_PROJECT,
silent: true, // Suppresses all logs
authToken: env.SENTRY_AUTH_TOKEN,
// Only print logs for uploading source maps in CI
silent: !process.env.CI,
// For all available options, see:
// https://github.com/getsentry/sentry-webpack-plugin#options.
// https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/
// See the sections below for information on the following options:
// 'Configure Source Maps':
// - disableServerWebpackPlugin
// - disableClientWebpackPlugin
// - hideSourceMaps
hideSourceMaps: true,
// - widenClientFileUpload
// 'Configure Legacy Browser Support':
// - transpileClientSDK
// 'Configure Serverside Auto-instrumentation':
// - autoInstrumentServerFunctions
// - excludeServerRoutes
// 'Configure Tunneling':
// - tunnelRoute
// Upload a larger set of source maps for prettier stack traces (increases build time)
widenClientFileUpload: true,
// Automatically annotate React components to show their full name in breadcrumbs and session replay
reactComponentAnnotation: {
enabled: true,
},
// Route browser requests to Sentry through a Next.js rewrite to circumvent ad-blockers.
// This can increase your server load as well as your hosting bill.
// Note: Check that the configured route will not match with your Next.js middleware, otherwise reporting of client-
// side errors will fail.
tunnelRoute: "/api/monitoring-tunnel",
};
export default withSentryConfig(nextConfig, sentryOptions);
// Hides source maps from generated client bundles
hideSourceMaps: true,
// Automatically tree-shake Sentry logger statements to reduce bundle size
disableLogger: true,
// Enables automatic instrumentation of Vercel Cron Monitors. (Does not yet work with App Router route handlers.)
// See the following for more information:
// https://docs.sentry.io/product/crons/
// https://vercel.com/docs/cron-jobs
automaticVercelMonitors: false,
});
+36 -35
View File
@@ -1,6 +1,6 @@
{
"name": "web",
"version": "2.74.0",
"version": "2.76.0",
"private": true,
"license": "MIT",
"engines": {
@@ -54,32 +54,31 @@
"@opentelemetry/sdk-trace-node": "^1.25.1",
"@opentelemetry/semantic-conventions": "^1.25.1",
"@prisma/instrumentation": "^5.13.0",
"@radix-ui/react-accordion": "^1.1.2",
"@radix-ui/react-alert-dialog": "^1.0.5",
"@radix-ui/react-avatar": "^1.0.4",
"@radix-ui/react-checkbox": "^1.0.4",
"@radix-ui/react-collapsible": "^1.0.3",
"@radix-ui/react-dialog": "^1.0.5",
"@radix-ui/react-dropdown-menu": "^2.0.6",
"@radix-ui/react-hover-card": "^1.0.7",
"@radix-ui/react-accordion": "^1.2.0",
"@radix-ui/react-alert-dialog": "^1.1.1",
"@radix-ui/react-avatar": "^1.1.0",
"@radix-ui/react-checkbox": "^1.1.1",
"@radix-ui/react-collapsible": "^1.1.0",
"@radix-ui/react-dialog": "^1.1.1",
"@radix-ui/react-dropdown-menu": "^2.1.1",
"@radix-ui/react-hover-card": "^1.1.1",
"@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",
"@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-switch": "^1.0.3",
"@radix-ui/react-tabs": "^1.0.4",
"@radix-ui/react-toggle": "^1.0.3",
"@radix-ui/react-toggle-group": "^1.0.4",
"@radix-ui/react-tooltip": "^1.0.7",
"@radix-ui/react-label": "^2.1.0",
"@radix-ui/react-popover": "^1.1.1",
"@radix-ui/react-scroll-area": "^1.1.0",
"@radix-ui/react-select": "^2.1.1",
"@radix-ui/react-separator": "^1.1.0",
"@radix-ui/react-slider": "^1.2.0",
"@radix-ui/react-slot": "^1.1.0",
"@radix-ui/react-switch": "^1.1.0",
"@radix-ui/react-tabs": "^1.1.0",
"@radix-ui/react-toggle": "^1.1.0",
"@radix-ui/react-toggle-group": "^1.1.0",
"@radix-ui/react-tooltip": "^1.1.2",
"@remixicon/react": "^4.2.0",
"@repo/eslint-config": "workspace:*",
"@repo/typescript-config": "workspace:*",
"@sentry/nextjs": "8.13.0",
"@sentry/node": "8.13.0",
"@sentry/nextjs": "8.26.0",
"@t3-oss/env-nextjs": "^0.10.1",
"@tailwindcss/container-queries": "^0.1.1",
"@tanstack/react-query": "^4.36.1",
@@ -89,16 +88,16 @@
"@trpc/next": "^10.45.0",
"@trpc/react-query": "^10.45.0",
"@trpc/server": "^10.45.0",
"@uiw/codemirror-theme-github": "^4.21.25",
"@uiw/codemirror-theme-github": "^4.23.0",
"@uiw/codemirror-theme-tokyo-night": "^4.22.2",
"@uiw/react-codemirror": "^4.21.25",
"ai": "^3.0.23",
"bcryptjs": "^2.4.3",
"bullmq": "^5.4.2",
"bullmq": "^5.12.10",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"cmdk": "^1.0.0",
"core-js": "^3.36.1",
"core-js": "^3.38.1",
"cors": "^2.8.5",
"date-fns": "^3.3.1",
"dd-trace": "^5.21.0",
@@ -108,11 +107,11 @@
"ioredis": "^5.4.1",
"ip-address": "^9.0.5",
"js-tiktoken": "^1.0.12",
"kysely": "^0.27.3",
"kysely": "^0.27.4",
"langchain": "^0.2.6",
"lodash": "^4.17.21",
"lucide-react": "^0.364.0",
"next": "^14.2.3",
"lucide-react": "^0.436.0",
"next": "^14.2.6",
"next-auth": "^4.24.7",
"next-query-params": "^5.0.0",
"next-themes": "^0.3.0",
@@ -120,12 +119,14 @@
"posthog-node": "^3.6.3",
"prexit": "^2.2.0",
"prisma": "^5.13.0",
"rate-limiter-flexible": "^5.0.3",
"react": "18.2.0",
"react-day-picker": "^8.10.1",
"react-dom": "18.2.0",
"react-hook-form": "^7.51.5",
"react-icons": "^5.2.1",
"react-markdown": "^9.0.1",
"react-resizable-panels": "^2.1.1",
"react-responsive": "^10.0.0",
"react-syntax-highlighter": "^15.5.0",
"react18-json-view": "^0.2.8-canary.6",
@@ -134,7 +135,7 @@
"sonner": "^1.4.41",
"stripe": "^16.8.0",
"superjson": "2.2.1",
"tailwind-merge": "^2.2.1",
"tailwind-merge": "^2.5.2",
"tailwindcss-animate": "^1.0.7",
"use-query-params": "^2.2.1",
"uuid": "^9.0.1",
@@ -153,7 +154,7 @@
"@types/dompurify": "^3.0.5",
"@types/eslint": "^8.56.7",
"@types/jest": "^29.5.12",
"@types/lodash": "^4.17.5",
"@types/lodash": "^4.17.7",
"@types/node": "20.10.5",
"@types/react": "^18.2.79",
"@types/react-dom": "^18.2.25",
@@ -164,17 +165,17 @@
"autoprefixer": "^10.4.19",
"dotenv-cli": "^7.4.2",
"eslint": "^8.56.0",
"eslint-config-next": "^14.2.3",
"eslint-config-next": "^14.2.6",
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
"node-mocks-http": "^1.14.1",
"postcss": "^8.4.38",
"prettier": "^3.2.5",
"prettier-plugin-tailwindcss": "^0.5.14",
"tailwindcss": "^3.4.4",
"prettier-plugin-tailwindcss": "^0.6.6",
"tailwindcss": "^3.4.10",
"ts-node": "^10.9.2",
"tsconfig-paths": "^4.2.0",
"tsx": "^4.7.2",
"tsx": "^4.19.0",
"typescript": "^5.4.5",
"wait-for-expect": "^3.0.2"
},
@@ -3,19 +3,28 @@ import * as Sentry from "@sentry/nextjs";
Sentry.init({
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
// Replay may only be enabled for the client-side
integrations: [Sentry.replayIntegration()],
integrations: [
Sentry.replayIntegration(),
// Sentry.debugIntegration(),
Sentry.captureConsoleIntegration({
levels: ["error"],
}),
],
// Set tracesSampleRate to 1.0 to capture 100%
// of transactions for performance monitoring.
// We recommend adjusting this value in production
tracesSampleRate: process.env.NEXT_LANGFUSE_TRACING_SAMPLE_RATE
? Number(process.env.NEXT_LANGFUSE_TRACING_SAMPLE_RATE)
: 0.1,
tracesSampleRate: process.env.NEXT_PUBLIC_LANGFUSE_TRACING_SAMPLE_RATE
? Number(process.env.NEXT_PUBLIC_LANGFUSE_TRACING_SAMPLE_RATE)
: 0.5,
// Capture Replay for 100% of all sessions,
// plus for 100% of sessions with an error
replaysSessionSampleRate: 0,
replaysSessionSampleRate: process.env.NEXT_PUBLIC_LANGFUSE_TRACING_SAMPLE_RATE
? Number(process.env.NEXT_PUBLIC_LANGFUSE_TRACING_SAMPLE_RATE)
: 0.5,
replaysOnErrorSampleRate: 1.0,
debug: false,
beforeSend(event, _hint) {
// Check if it is an exception, and if so, show the report dialog
+96 -4
View File
@@ -1,8 +1,7 @@
import { v4 } from "uuid";
import { JobExecutionStatus, prisma } from "@langfuse/shared/src/db";
import { redis } from "@langfuse/shared/src/server";
import { JobExecutionStatus, Prisma, prisma } from "@langfuse/shared/src/db";
import { OrgEnrichedApiKey, redis } from "@langfuse/shared/src/server";
import waitForExpect from "wait-for-expect";
import { ApiKeyZod } from "@/src/features/public-api/server/apiAuth";
const generateAuth = (username: string, password: string) => {
const auth = Buffer.from(`${username}:${password}`).toString("base64");
@@ -38,6 +37,14 @@ describe("Health endpoints", () => {
});
describe("Ingestion Pipeline", () => {
beforeEach(async () => {
// clear the redis cache
const keys = await redis?.keys("*");
if (keys && keys.length > 0) {
await redis?.del(keys);
}
});
it("ingest a trace", async () => {
const traceId = v4();
const spanId = v4();
@@ -118,7 +125,7 @@ describe("Ingestion Pipeline", () => {
expect(redisKeys?.length).toBe(1);
const redisValue = await redis?.get(redisKeys![0]);
const llmApiKey = ApiKeyZod.parse(JSON.parse(redisValue!));
const llmApiKey = OrgEnrichedApiKey.parse(JSON.parse(redisValue!));
expect(llmApiKey.projectId).toBe("7a88fb47-b4e2-43b8-a06c-a5ce950dc53a");
});
@@ -143,6 +150,91 @@ describe("Ingestion Pipeline", () => {
expect(response.status).toBe(207);
}, 25000);
it("rate limit ingestion", async () => {
// update the org in the database and set the rate limit to 1 for ingestion
const org = await prisma.organization.findUnique({
where: {
id: "seed-org-id",
},
});
await prisma.organization.update({
where: {
id: "seed-org-id",
},
data: {
cloudConfig: {
...(typeof org?.cloudConfig === "object" ? org.cloudConfig : {}),
rateLimitOverrides: [
{
resource: "ingestion",
points: 1,
durationInSec: 60,
},
],
},
},
});
const traceId = v4();
const spanId = v4();
const event = {
batch: [
{
id: v4(),
type: "trace-create",
timestamp: new Date().toISOString(),
body: {
name: "test trace",
id: traceId,
userId: "user-1", // triggers the eval
},
},
{
id: v4(),
type: "span-create",
timestamp: new Date().toISOString(),
body: {
id: spanId,
traceId: traceId,
name: "test span",
},
},
],
};
// Arrange
const url = "http://localhost:3000/api/public/ingestion";
// Act
let responses = [];
for (let i = 0; i < 10; i++) {
responses.push(
await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: userApiKeyAuth,
},
body: JSON.stringify(event),
}),
);
}
// check that at least one of the responses is a 429
const rateLimitedResponse = responses.find((r) => r.status === 429);
expect(rateLimitedResponse).not.toBeNull();
// revert the rate limit on the org
await prisma.organization.update({
where: {
id: "seed-org-id",
},
data: {
cloudConfig: org?.cloudConfig ?? Prisma.JsonNull,
},
});
});
});
describe("Prompts endpoint", () => {
+321 -8
View File
@@ -1,14 +1,12 @@
import {
getDisplaySecretKey,
hashSecretKey,
OrgEnrichedApiKey,
} from "@langfuse/shared/src/server";
import {
ApiAuthService,
ApiKeyZod,
} from "@/src/features/public-api/server/apiAuth";
import { type PrismaClient, prisma } from "@langfuse/shared/src/db";
import { Prisma, type PrismaClient, prisma } from "@langfuse/shared/src/db";
import { Redis } from "ioredis";
import { env } from "@/src/env.mjs";
import { ApiAuthService } from "@/src/features/public-api/server/apiAuth";
describe("Authenticate API calls", () => {
beforeEach(async () => {
@@ -68,6 +66,118 @@ describe("Authenticate API calls", () => {
expect(auth2.validKey).toBe(true);
});
it("should create new api key with stripe data and succeed with new key", async () => {
await createAPIKey();
await prisma.organization.update({
where: {
id: "seed-org-id",
},
data: {
cloudConfig: {
stripe: {
customerId: "cus_test123",
activeSubscriptionId: "sub_test123",
activeProductId: "prod_test123",
},
},
},
});
await new ApiAuthService(prisma, null).verifyAuthHeaderAndReturnScope(
"Basic cGstbGYtMTIzNDU2Nzg5MDpzay1sZi0xMjM0NTY3ODkw",
);
const auth = await new ApiAuthService(
prisma,
null,
).verifyAuthHeaderAndReturnScope(
"Basic cGstbGYtMTIzNDU2Nzg5MDpzay1sZi0xMjM0NTY3ODkw",
);
expect(auth.validKey).toBe(true);
if (auth.validKey) {
expect(auth.scope.orgId).toBe("seed-org-id");
expect(auth.scope.plan).toBe("cloud:hobby");
expect(auth.scope.rateLimitOverrides).toEqual([]);
}
const apiKey = await prisma.apiKey.findUnique({
where: { publicKey: "pk-lf-1234567890" },
});
expect(apiKey).not.toBeNull();
expect(apiKey?.fastHashedSecretKey).not.toBeNull();
await prisma.organization.update({
where: {
id: "seed-org-id",
},
data: {
cloudConfig: Prisma.JsonNull,
},
});
});
it("should create new api key with custom rate limits and succeed with new key", async () => {
await createAPIKey();
await prisma.organization.update({
where: {
id: "seed-org-id",
},
data: {
cloudConfig: {
rateLimitOverrides: [
{
resource: "ingestion",
points: 100,
durationInSec: 60,
},
],
},
},
});
await new ApiAuthService(prisma, null).verifyAuthHeaderAndReturnScope(
"Basic cGstbGYtMTIzNDU2Nzg5MDpzay1sZi0xMjM0NTY3ODkw",
);
const auth = await new ApiAuthService(
prisma,
null,
).verifyAuthHeaderAndReturnScope(
"Basic cGstbGYtMTIzNDU2Nzg5MDpzay1sZi0xMjM0NTY3ODkw",
);
expect(auth.validKey).toBe(true);
if (auth.validKey) {
expect(auth.scope.orgId).toBe("seed-org-id");
expect(auth.scope.plan).toBe("cloud:hobby");
expect(auth.scope.rateLimitOverrides).toEqual([
{
resource: "ingestion",
points: 100,
durationInSec: 60,
},
]);
}
const apiKey = await prisma.apiKey.findUnique({
where: { publicKey: "pk-lf-1234567890" },
});
expect(apiKey).not.toBeNull();
expect(apiKey?.fastHashedSecretKey).not.toBeNull();
await prisma.organization.update({
where: {
id: "seed-org-id",
},
data: {
cloudConfig: Prisma.JsonNull,
},
});
});
it("should fail on wrong api key with new key", async () => {
await createAPIKey();
const auth = await new ApiAuthService(
@@ -149,6 +259,25 @@ describe("Authenticate API calls", () => {
it("should create new api key and read from cache", async () => {
await createAPIKey();
// update the organization with a cloud config
await prisma.organization.update({
where: { id: "seed-org-id" },
data: {
cloudConfig: {
rateLimitOverrides: [
{
resource: "public-api",
points: 1000,
durationInSec: 60,
},
{
resource: "ingestion",
},
],
},
},
});
// first auth will generate the fast hashed api key
await new ApiAuthService(prisma, redis).verifyAuthHeaderAndReturnScope(
"Basic cGstbGYtMTIzNDU2Nzg5MDpzay1sZi0xMjM0NTY3ODkw",
@@ -177,12 +306,31 @@ describe("Authenticate API calls", () => {
expect(cachedKey2).not.toBeNull();
const parsed = ApiKeyZod.parse(JSON.parse(cachedKey2!));
const parsed = OrgEnrichedApiKey.parse(JSON.parse(cachedKey2!));
expect(parsed).toEqual({
...apiKey,
orgId: "seed-org-id",
plan: "cloud:hobby",
rateLimitOverrides: [
{
resource: "public-api",
points: 1000,
durationInSec: 60,
},
{
resource: "ingestion",
},
],
createdAt: apiKey?.createdAt.toISOString(),
});
await prisma.organization.update({
where: { id: "seed-org-id" },
data: {
cloudConfig: Prisma.JsonNull,
},
});
});
it("searching for non-existing key stores flag in redis and fails auth", async () => {
@@ -275,7 +423,7 @@ describe("Authenticate API calls", () => {
);
expect(cachedKey).not.toBeNull();
const parsed = ApiKeyZod.parse(JSON.parse(cachedKey!));
const parsed = OrgEnrichedApiKey.parse(JSON.parse(cachedKey!));
expect(parsed).toEqual({
id: expect.any(String),
@@ -288,6 +436,8 @@ describe("Authenticate API calls", () => {
lastUsedAt: null,
expiresAt: null,
projectId: expect.any(String),
orgId: "seed-org-id",
plan: "cloud:hobby",
});
});
@@ -372,10 +522,12 @@ describe("Authenticate API calls", () => {
);
expect(cachedKey).not.toBeNull();
const parsed = ApiKeyZod.parse(JSON.parse(cachedKey!));
const parsed = OrgEnrichedApiKey.parse(JSON.parse(cachedKey!));
expect(parsed).toEqual({
...apiKey,
orgId: "seed-org-id",
plan: "cloud:hobby",
createdAt: apiKey?.createdAt.toISOString(),
});
@@ -396,6 +548,167 @@ describe("Authenticate API calls", () => {
});
});
describe("invalidates api keys in redis", () => {
const redis = new Redis("redis://:myredissecret@127.0.0.1:6379", {
maxRetriesPerRequest: null,
});
beforeEach(async () => {
// if we do not remove the key, it will remain in the cache and
// calling the test twice will not add the key to the cache
const keys = await redis.keys("api-key*");
console.log("before each deleting keys", keys);
if (keys.length > 0) {
console.log("before each deleting keys. actually deleting", keys);
await redis.del(keys);
}
});
afterEach(async () => {
// if we do not remove the key, it will remain in the cache and
// calling the test twice will not add the key to the cache
const keys = await redis.keys("api-key*");
console.log("after each deleting keys", keys);
if (keys.length > 0) {
await redis.del(keys);
}
});
afterAll(async () => {
redis.disconnect();
});
it("should invalidate organization API keys in redis", async () => {
await createAPIKey();
// put keys into cache
await new ApiAuthService(prisma, redis).verifyAuthHeaderAndReturnScope(
"Basic cGstbGYtMTIzNDU2Nzg5MDpzay1sZi0xMjM0NTY3ODkw",
);
await new ApiAuthService(prisma, redis).verifyAuthHeaderAndReturnScope(
"Basic cGstbGYtMTIzNDU2Nzg5MDpzay1sZi0xMjM0NTY3ODkw",
);
const apiKey = await prisma.apiKey.findUnique({
where: { publicKey: "pk-lf-1234567890" },
});
expect(apiKey).not.toBeNull();
const cachedKey = await redis.get(
`api-key:${apiKey?.fastHashedSecretKey}`,
);
expect(cachedKey).not.toBeNull();
await new ApiAuthService(prisma, redis).invalidateOrgApiKeys(
"seed-org-id",
);
const invalidatedCachedKey = await redis.get(
`api-key:${apiKey?.fastHashedSecretKey}`,
);
expect(invalidatedCachedKey).toBeNull();
});
it("if no keys in redis, invalidating org keys should do nothing", async () => {
await createAPIKey();
await prisma.apiKey.update({
where: { publicKey: "pk-lf-1234567890" },
data: {
fastHashedSecretKey: Math.random().toString(36).substring(2, 15),
},
});
await new ApiAuthService(prisma, redis).invalidateOrgApiKeys(
"seed-org-id",
);
const keys = await redis.keys("api-key*");
expect(keys.length).toBe(0);
});
it("if no keys in redis, invalidating org keys without fast hash should do nothing", async () => {
await createAPIKey();
await new ApiAuthService(prisma, redis).invalidateOrgApiKeys(
"seed-org-id",
);
const keys = await redis.keys("api-key*");
expect(keys.length).toBe(0);
});
it("should invalidate project API keys in redis", async () => {
await createAPIKey();
// put keys into cache
await new ApiAuthService(prisma, redis).verifyAuthHeaderAndReturnScope(
"Basic cGstbGYtMTIzNDU2Nzg5MDpzay1sZi0xMjM0NTY3ODkw",
);
await new ApiAuthService(prisma, redis).verifyAuthHeaderAndReturnScope(
"Basic cGstbGYtMTIzNDU2Nzg5MDpzay1sZi0xMjM0NTY3ODkw",
);
const apiKey = await prisma.apiKey.findUnique({
where: { publicKey: "pk-lf-1234567890" },
});
expect(apiKey).not.toBeNull();
const cachedKey = await redis.get(
`api-key:${apiKey?.fastHashedSecretKey}`,
);
expect(cachedKey).not.toBeNull();
await new ApiAuthService(prisma, redis).invalidateProjectApiKeys(
"7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
);
const invalidatedCachedKey = await redis.get(
`api-key:${apiKey?.fastHashedSecretKey}`,
);
expect(invalidatedCachedKey).toBeNull();
});
it("if no keys in redis, invalidating project keys should do nothing", async () => {
await createAPIKey();
await prisma.apiKey.update({
where: { publicKey: "pk-lf-1234567890" },
data: {
fastHashedSecretKey: Math.random().toString(36).substring(2, 15),
},
});
await new ApiAuthService(prisma, redis).invalidateProjectApiKeys(
"7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
);
const keys = await redis.keys("api-key*");
expect(keys.length).toBe(0);
});
it("if no keys in redis, invalidating project keys without fast hash should do nothing", async () => {
await createAPIKey();
await prisma.apiKey.update({
where: { publicKey: "pk-lf-1234567890" },
data: {
fastHashedSecretKey: Math.random().toString(36).substring(2, 15),
},
});
await new ApiAuthService(prisma, redis).invalidateProjectApiKeys(
"7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
);
const keys = await redis.keys("api-key*");
expect(keys.length).toBe(0);
});
});
const createAPIKey = async () => {
const seedApiKey = {
id: "seed-api-key",
+269
View File
@@ -0,0 +1,269 @@
import { RateLimitService } from "@/src/features/public-api/server/RateLimitService";
import { Redis } from "ioredis";
describe("RateLimitService", () => {
const orgId = "seed-org-id";
let redis: Redis;
beforeAll(() => {
redis = new Redis("redis://:myredissecret@127.0.0.1:6379", {
maxRetriesPerRequest: null,
});
});
afterAll(async () => {
redis.disconnect();
});
beforeEach(async () => {
expect(redis).toBeDefined();
const keys = await redis?.keys("rate-limit*");
if (keys && keys?.length > 0) {
await redis?.del(keys);
}
});
afterEach(async () => {
const keys = await redis?.keys("rate-limit*");
if (keys && keys.length > 0) {
await redis?.del(keys);
}
});
it("should rate limit", async () => {
const scope = {
orgId: orgId,
plan: "cloud:hobby" as const,
projectId: "test-project-id",
accessLevel: "all" as const,
rateLimitOverrides: [],
};
const rateLimitService = new RateLimitService(redis!);
const result = await rateLimitService.rateLimitRequest(scope, "public-api");
expect(result?.res).toEqual({
scope: scope,
resource: "public-api",
points: 1000,
remainingPoints: 999,
msBeforeNext: expect.any(Number),
consumedPoints: 1,
isFirstInDuration: true,
});
expect(result?.isRateLimited()).toBe(false);
// check redis for the rate limit key
const value = await redis.get("rate-limit:public-api:seed-org-id");
expect(value).toBeDefined();
expect(parseInt(value ?? "0")).toBeGreaterThan(0);
});
it("should increment the rate limit count", async () => {
const scope = {
orgId: orgId,
plan: "cloud:hobby" as const,
projectId: "test-project-id",
accessLevel: "all" as const,
rateLimitOverrides: [],
};
const rateLimitService = new RateLimitService(redis!);
await rateLimitService.rateLimitRequest(scope, "public-api");
const result = await rateLimitService.rateLimitRequest(scope, "public-api");
expect(result?.res).toEqual({
scope: scope,
resource: "public-api",
points: 1000,
remainingPoints: 998,
msBeforeNext: expect.any(Number),
consumedPoints: 2,
isFirstInDuration: false,
});
expect(result?.isRateLimited()).toBe(false);
});
it("should reset the rate limit count after the window expires", async () => {
const scope = {
orgId: orgId,
plan: "cloud:hobby" as const,
projectId: "test-project-id",
accessLevel: "all" as const,
rateLimitOverrides: [
{ resource: "public-api" as const, points: 100, durationInSec: 2 },
],
};
const rateLimitService = new RateLimitService(redis!);
await rateLimitService.rateLimitRequest(scope, "public-api");
const firstResult = await rateLimitService.rateLimitRequest(
scope,
"public-api",
);
expect(firstResult?.isRateLimited()).toBe(false);
expect(firstResult?.res).toEqual({
scope: scope,
resource: "public-api",
points: 100,
remainingPoints: 98,
msBeforeNext: expect.any(Number),
consumedPoints: 2,
isFirstInDuration: false,
});
await new Promise((resolve) => setTimeout(resolve, 3000));
const secondResult = await rateLimitService.rateLimitRequest(
scope,
"public-api",
);
expect(secondResult?.res).toEqual({
scope: scope,
resource: "public-api",
points: 100,
remainingPoints: 99,
msBeforeNext: expect.any(Number),
consumedPoints: 1,
isFirstInDuration: true,
});
expect(secondResult?.isRateLimited()).toBe(false);
});
it("should return false when rate limit is exceeded", async () => {
const scope = {
orgId: orgId,
plan: "cloud:hobby" as const,
projectId: "test-project-id",
accessLevel: "all" as const,
rateLimitOverrides: [
{ resource: "public-api" as const, points: 5, durationInSec: 60 },
],
};
const rateLimitService = new RateLimitService(redis!);
for (let i = 0; i < 5; i++) {
await rateLimitService.rateLimitRequest(scope, "public-api");
}
const result = await rateLimitService.rateLimitRequest(scope, "public-api");
expect(result?.res).toEqual({
scope: scope,
resource: "public-api",
points: 5,
remainingPoints: 0,
msBeforeNext: expect.any(Number),
consumedPoints: 6,
isFirstInDuration: false,
});
expect(result?.isRateLimited()).toBe(true);
});
it("should apply rate limits with override for specific resource", async () => {
const scope = {
orgId: orgId,
plan: "cloud:hobby" as const,
projectId: "test-project-id",
accessLevel: "all" as const,
rateLimitOverrides: [
{ resource: "public-api" as const, points: 5, durationInSec: 10 },
],
};
const rateLimitService = new RateLimitService(redis!);
const result = await rateLimitService.rateLimitRequest(scope, "public-api");
expect(result?.res).toEqual({
scope: scope,
resource: "public-api",
points: 5,
remainingPoints: 4,
msBeforeNext: expect.any(Number),
consumedPoints: 1,
isFirstInDuration: true,
});
});
it("should not apply rate limits for resource prompts", async () => {
const scope = {
orgId: orgId,
plan: "cloud:hobby" as const,
projectId: "test-project-id",
accessLevel: "all" as const,
rateLimitOverrides: [
{ resource: "public-api" as const, points: 5, durationInSec: 10 },
],
};
const rateLimitService = new RateLimitService(redis!);
const result = await rateLimitService.rateLimitRequest(scope, "prompts");
expect(result?.res).toBeUndefined();
expect(result?.isRateLimited()).toBe(false);
});
it("should not apply rate limits for ingestion when overridden to null in API key", async () => {
const scope = {
orgId: orgId,
plan: "cloud:hobby" as const,
projectId: "test-project-id",
accessLevel: "all" as const,
rateLimitOverrides: [
{ resource: "ingestion" as const, points: null, durationInSec: null },
],
};
const rateLimitService = new RateLimitService(redis!);
const result = await rateLimitService.rateLimitRequest(scope, "ingestion");
expect(result?.res).toBeUndefined();
});
it("should not apply rate limits when redis is not defined", async () => {
const scope = {
orgId: orgId,
plan: "cloud:hobby" as const,
projectId: "test-project-id",
accessLevel: "all" as const,
rateLimitOverrides: [
{ resource: "public-api" as const, points: 5, durationInSec: 10 },
],
};
const rateLimitService = new RateLimitService(null);
const result = await rateLimitService.rateLimitRequest(scope, "public-api");
expect(result?.res).toBeUndefined();
expect(result?.isRateLimited()).toBe(false);
});
it("should not apply rate limits for OSS plan", async () => {
const scope = {
orgId: orgId,
plan: "oss" as const,
projectId: "test-project-id",
accessLevel: "all" as const,
rateLimitOverrides: [],
};
const rateLimitService = new RateLimitService(redis!);
const result = await rateLimitService.rateLimitRequest(scope, "public-api");
expect(result?.res).toBeUndefined();
expect(result?.isRateLimited()).toBe(false);
});
});
@@ -64,7 +64,7 @@ export const ChatMessageComponent: React.FC<ChatMessageProps> = ({
<Textarea
ref={textAreaRef}
id={message.id}
className="height-[auto] min-h-8 w-full pt-3 font-mono text-xs focus:outline-none"
className="height-[auto] min-h-6 w-full font-mono text-xs focus:outline-none"
placeholder={placeholder}
value={message.content}
onChange={handleContentChange}
+17 -9
View File
@@ -24,13 +24,21 @@ export const ChatMessages: React.FC<ChatMessagesProps> = (props) => {
}, [scrollAreaRef, messages.length]);
return (
<div className="h-full overflow-auto scroll-smooth" ref={scrollAreaRef}>
<div className="mb-4 flex-1 space-y-4">
{props.messages.map((message) => {
return (
<ChatMessageComponent {...{ message, ...props }} key={message.id} />
);
})}
<div className="flex h-full flex-col">
<div className="mb-2 font-semibold">Messages</div>
<div className="flex-1 overflow-auto scroll-smooth" ref={scrollAreaRef}>
<div className="mb-4 flex-1 space-y-3">
{props.messages.map((message) => {
return (
<ChatMessageComponent
{...{ message, ...props }}
key={message.id}
/>
);
})}
</div>
</div>
<div className="py-3">
<AddMessageButton {...props} />
</div>
</div>
@@ -52,10 +60,10 @@ const AddMessageButton: React.FC<AddMessageButtonProps> = ({
<Button
type="button" // prevents submitting a form if this button is inside a form
variant="outline"
className="w-full space-x-2 py-6"
className="w-full"
onClick={() => addMessage(nextMessageRole)}
>
<PlusCircleIcon size={16} />
<PlusCircleIcon size={14} className="mr-2" />
<p>Add message</p>
</Button>
);
@@ -47,7 +47,7 @@ export const LLMApiKeyComponent = (p: {
<Label className="text-xs font-semibold">API key</Label>
<div>
{apiKey ? (
<Link href={`/project/${p.projectId}/settings#llm-api-keys`}>
<Link href={`/project/${p.projectId}/settings/api-keys`}>
<span className="mr-2 rounded-sm bg-input p-1 text-xs">
{apiKey.displaySecretKey}
</span>
+8 -5
View File
@@ -31,12 +31,15 @@ export type ModelParamsContext = {
formDisabled?: boolean;
};
export const ModelParameters: React.FC<ModelParamsContext> = ({
export const ModelParameters: React.FC<
ModelParamsContext & { evalModelsOnly: boolean }
> = ({
modelParams,
availableProviders,
availableModels,
updateModelParamValue,
setModelParamEnabled,
evalModelsOnly,
formDisabled = false,
}) => {
const projectId = useProjectIdFromURL();
@@ -48,11 +51,11 @@ export const ModelParameters: React.FC<ModelParamsContext> = ({
<p className="font-semibold">Model</p>
{availableProviders.length === 0 ? (
<>
<p className="text-sm">
No LLM API key set in project. For evals, we only support OpenAI so
far.
<p className="text-xs">
No LLM API key set in project.{" "}
{evalModelsOnly && "For evals, only OpenAI models are supported."}
</p>
<CreateLLMApiKeyDialog evalModelsOnly={true} />
<CreateLLMApiKeyDialog evalModelsOnly={evalModelsOnly} />
</>
) : (
<div className="space-y-4">
@@ -49,7 +49,7 @@ export const PagedSettingsContainer = ({
return (
<main className="flex flex-1 flex-col gap-4 py-4 md:gap-8">
<div className="grid w-full items-start gap-4 md:grid-cols-[180px_1fr] lg:grid-cols-[220px_1fr]">
<div className="grid w-full items-start gap-4 md:grid-cols-[150px_1fr] lg:grid-cols-[220px_1fr]">
<nav className="block md:hidden">
<Select
onValueChange={(slug) => {
+2 -2
View File
@@ -11,11 +11,11 @@ export const SettingsDangerZone: React.FC<{
return (
<div className="space-y-3">
<Header title="Danger Zone" level="h3" />
<div className="rounded border">
<div className="rounded-lg border">
{items.map((item, index) => (
<div
key={index}
className="flex items-center justify-between gap-4 border-b p-4 last:border-b-0"
className="flex items-center justify-between gap-4 border-b p-3 last:border-b-0"
>
<div>
<h4 className="font-semibold">{item.title}</h4>
+12 -15
View File
@@ -9,15 +9,14 @@ import {
} from "@/src/components/ui/select";
import {
DEFAULT_AGGREGATION_SELECTION,
tableDateRangeAggregationSettings,
dashboardDateRangeAggregationSettings,
DASHBOARD_AGGREGATION_PLACEHOLDER,
type DashboardDateRangeOptions,
type TableDateRangeOptions,
DASHBOARD_AGGREGATION_OPTIONS,
type DashboardDateRange,
TABLE_RANGE_DROPDOWN_OPTIONS,
TABLE_AGGREGATION_OPTIONS,
getDateFromOption,
} from "@/src/utils/date-range-utils";
import { Clock } from "lucide-react";
@@ -101,24 +100,22 @@ export const TableDateRangeDropdown: React.FC<TableDateRangeDropdownProps> = ({
setDateRangeAndOption,
}) => {
const onDropDownSelection = (value: TableDateRangeOptions) => {
if (value === DEFAULT_AGGREGATION_SELECTION) {
setDateRangeAndOption(DEFAULT_AGGREGATION_SELECTION, undefined);
return;
}
const setting =
tableDateRangeAggregationSettings[
value as keyof typeof tableDateRangeAggregationSettings
];
setDateRangeAndOption(value, {
from: addMinutes(new Date(), -setting),
to: new Date(),
const dateFromOption = getDateFromOption({
filterSource: "TABLE",
option: value,
});
const initialDateRange = !!dateFromOption
? { from: dateFromOption, to: new Date() }
: undefined;
setDateRangeAndOption(value, initialDateRange);
};
return (
<BaseDateRangeDropdown
selectedOption={selectedOption}
options={TABLE_RANGE_DROPDOWN_OPTIONS}
options={TABLE_AGGREGATION_OPTIONS}
onSelectionChange={onDropDownSelection}
/>
);
+1 -1
View File
@@ -45,7 +45,7 @@ export function JsonEditor({
if (onChange) onChange(c);
setLinterEnabled(c !== "");
}}
className={cn("overflow-hidden rounded-md border", className)}
className={cn("overflow-hidden rounded-md border text-xs", className)}
editable={editable}
/>
);
@@ -1,8 +1,19 @@
export const FullScreenPage: React.FC<React.PropsWithChildren<{}>> = ({
children,
}) => {
import { cn } from "@/src/utils/tailwind";
export const FullScreenPage: React.FC<
React.PropsWithChildren<{
mobile?: boolean;
className?: string;
}>
> = ({ children, mobile = true, className }) => {
return (
<div className="flex h-[calc(100dvh-6rem)] flex-col overflow-hidden lg:h-[calc(100dvh-2rem)]">
<div
className={cn(
"flex flex-col overflow-hidden lg:h-[calc(100dvh-1.5rem)]",
mobile && "h-[calc(100dvh-6rem)]",
className,
)}
>
{children}
</div>
);
+2 -2
View File
@@ -212,7 +212,7 @@ const BreadcrumbComponent = ({
asChild
variant="ghost"
size="xs"
className="-my-1 ml-4 mr-1 hover:bg-background"
className="-my-1 ml-4 hover:bg-background"
>
<div
onClick={(e) => {
@@ -299,7 +299,7 @@ const BreadcrumbComponent = ({
asChild
variant="ghost"
size="xs"
className="-my-1 ml-4 mr-1 hover:bg-background"
className="-my-1 ml-4 hover:bg-background"
>
<div
onClick={(e) => {
+15 -11
View File
@@ -118,6 +118,10 @@ export default function Layout(props: PropsWithChildren) {
const uiCustomization = useUiCustomization();
const cloudAdmin =
env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION !== undefined &&
session.data?.user?.admin === true;
// project info based on projectId in the URL
const { project, organization } = useQueryProjectOrOrganization();
@@ -134,7 +138,7 @@ export default function Layout(props: PropsWithChildren) {
if (
route.featureFlag !== undefined &&
!enableExperimentalFeatures &&
session.data?.user?.admin !== true &&
!cloudAdmin &&
session.data?.user?.featureFlags[route.featureFlag] !== true
)
return null;
@@ -143,14 +147,14 @@ export default function Layout(props: PropsWithChildren) {
if (
route.entitlement !== undefined &&
!entitlements.includes(route.entitlement) &&
session.data?.user?.admin !== true
!cloudAdmin
)
return null;
// RBAC
if (
route.projectRbacScope !== undefined &&
session.data?.user?.admin !== true &&
!cloudAdmin &&
(!project ||
!organization ||
!hasProjectAccess({
@@ -346,7 +350,7 @@ export default function Layout(props: PropsWithChildren) {
</Transition.Root>
{/* Static sidebar for desktop */}
<div className="hidden lg:fixed lg:inset-y-0 lg:z-50 lg:flex lg:w-56 lg:flex-col">
<div className="hidden lg:fixed lg:inset-y-0 lg:z-50 lg:flex lg:w-52 lg:flex-col">
{/* Sidebar component, swap this element with another sidebar if you like */}
<div className="flex h-screen grow flex-col border-r border-border bg-background">
<nav className="flex h-full flex-1 flex-col overflow-y-auto px-4 py-3">
@@ -418,7 +422,7 @@ export default function Layout(props: PropsWithChildren) {
>
<Menu.Items className="absolute -top-full bottom-1 right-0 z-10 overflow-hidden rounded-md bg-background py-2 shadow-lg ring-1 ring-border focus:outline-none">
<span
className="block max-w-56 overflow-hidden truncate border-b px-3 pb-2 text-sm leading-6 text-muted-foreground"
className="block max-w-52 overflow-hidden truncate border-b px-3 pb-2 text-sm leading-6 text-muted-foreground"
title={session.data?.user?.email ?? undefined}
>
{session.data?.user?.email}
@@ -486,7 +490,7 @@ export default function Layout(props: PropsWithChildren) {
>
<Menu.Items className="absolute right-0 z-10 mt-2.5 rounded-md bg-background py-2 pb-1 shadow-lg ring-1 ring-border focus:outline-none">
<span
className="mb-1 block max-w-56 overflow-hidden truncate border-b px-3 pb-2 text-sm leading-6 text-muted-foreground"
className="mb-1 block max-w-52 overflow-hidden truncate border-b px-3 pb-2 text-sm leading-6 text-muted-foreground"
title={session.data?.user?.email ?? undefined}
>
{session.data?.user?.email}
@@ -511,7 +515,7 @@ export default function Layout(props: PropsWithChildren) {
</Transition>
</Menu>
</div>
<div className="lg:pl-56">
<div className="lg:pl-52">
{env.NEXT_PUBLIC_DEMO_ORG_ID &&
env.NEXT_PUBLIC_DEMO_PROJECT_ID &&
routerProjectId === env.NEXT_PUBLIC_DEMO_PROJECT_ID &&
@@ -532,7 +536,7 @@ export default function Layout(props: PropsWithChildren) {
</Button>
</div>
) : null}
<main className="p-4">{props.children}</main>
<main className="p-3">{props.children}</main>
<Toaster visibleToasts={1} />
</div>
</div>
@@ -634,7 +638,7 @@ const MainNavigation: React.FC<{
{item.label && (
<span
className={cn(
"-my-0.5 self-center whitespace-nowrap break-keep rounded-sm border px-1 py-0.5 text-xs",
"-my-0.5 self-center whitespace-nowrap break-keep rounded-sm border px-1 text-xs",
item.current
? "border-primary-accent text-primary-accent"
: "border-border text-muted-foreground group-hover:border-primary-accent group-hover:text-primary-accent",
@@ -648,7 +652,7 @@ const MainNavigation: React.FC<{
open
? "rotate-90 text-muted-foreground"
: "text-muted-foreground",
"ml-auto h-5 w-5 shrink-0",
"ml-auto h-4 w-4 shrink-0",
)}
aria-hidden="true"
/>
@@ -663,7 +667,7 @@ const MainNavigation: React.FC<{
subItem.current
? "bg-primary-foreground text-primary-accent"
: "text-primary hover:bg-primary-foreground hover:text-primary-accent",
"ml-0.5 flex w-full items-center gap-x-3 rounded-md p-1.5 pl-7 pr-2 text-sm",
"ml-0.5 flex w-full items-center gap-x-3 rounded-md p-1 pl-7 pr-2 text-sm",
)}
target={subItem.newTab ? "_blank" : undefined}
>
+2 -2
View File
@@ -53,10 +53,10 @@ export const SessionPage: React.FC<{
string[]
>("emptySelectedConfigIds", []);
const commentCounts = api.comments.getCountsByObjectIds.useQuery(
const commentCounts = api.comments.getCountByObjectId.useQuery(
{
projectId,
objectIds: [sessionId],
objectId: sessionId,
objectType: "SESSION",
},
{ enabled: session.isSuccess },
+26 -15
View File
@@ -13,13 +13,13 @@ export function StarToggle({
value,
disabled = false,
onClick,
size = "sm",
size = "icon",
isLoading,
}: {
value: boolean;
disabled?: boolean;
onClick: (value: boolean) => Promise<unknown>;
size?: "sm" | "xs";
size?: "icon" | "icon-xs";
isLoading: boolean;
}) {
return (
@@ -40,18 +40,19 @@ export function StarToggle({
);
}
// use by the trace table
export function StarTraceToggle({
tracesFilter,
projectId,
traceId,
value,
size = "sm",
size = "icon",
}: {
tracesFilter: RouterInput["traces"]["all"];
projectId: string;
traceId: string;
value: boolean;
size?: "sm" | "xs";
size?: "icon" | "icon-xs";
}) {
const utils = api.useUtils();
const hasAccess = useHasProjectAccess({
@@ -89,7 +90,6 @@ export function StarTraceToggle({
tracesFilter,
(oldQueryData: RouterOutput["traces"]["all"] | undefined) => {
return {
totalCount: oldQueryData?.totalCount,
traces: oldQueryData?.traces
? oldQueryData.traces.map((trace) => {
return {
@@ -130,16 +130,17 @@ export function StarTraceToggle({
);
}
// use by the single trace view
export function StarTraceDetailsToggle({
projectId,
traceId,
value,
size = "sm",
size = "icon",
}: {
projectId: string;
traceId: string;
value: boolean;
size?: "sm" | "xs";
size?: "icon" | "icon-xs";
}) {
const utils = api.useUtils();
const hasAccess = useHasProjectAccess({
@@ -153,12 +154,15 @@ export function StarTraceDetailsToggle({
onMutate: async () => {
// Cancel any outgoing refetches
// (so they don't overwrite our optimistic update)
await utils.traces.byId.cancel();
await utils.traces.byIdWithObservationsAndScores.cancel();
setIsLoading(true);
// Snapshot the previous value
const prevData = utils.traces.byId.getData({ traceId, projectId });
const prevData = utils.traces.byIdWithObservationsAndScores.getData({
traceId,
projectId,
});
return { prevData };
},
@@ -166,14 +170,21 @@ export function StarTraceDetailsToggle({
setIsLoading(false);
trpcErrorToast(err);
// Rollback to the previous value if mutation fails
utils.traces.byId.setData({ traceId, projectId }, context?.prevData);
utils.traces.byIdWithObservationsAndScores.setData(
{ traceId, projectId },
context?.prevData,
);
},
onSettled: () => {
setIsLoading(false);
utils.traces.byId.setData(
utils.traces.byIdWithObservationsAndScores.setData(
{ traceId, projectId },
(oldQueryData: RouterOutput["traces"]["byId"] | undefined) => {
(
oldQueryData:
| RouterOutput["traces"]["byIdWithObservationsAndScores"]
| undefined,
) => {
return oldQueryData
? {
...oldQueryData,
@@ -182,7 +193,7 @@ export function StarTraceDetailsToggle({
: undefined;
},
);
void utils.traces.byId.invalidate();
void utils.traces.byIdWithObservationsAndScores.invalidate();
void utils.traces.all.invalidate();
},
});
@@ -212,12 +223,12 @@ export function StarSessionToggle({
projectId,
sessionId,
value,
size = "sm",
size = "icon",
}: {
projectId: string;
sessionId: string;
value: boolean;
size?: "sm" | "xs";
size?: "icon" | "icon-xs";
}) {
const utils = api.useUtils();
const hasAccess = useHasProjectAccess({
@@ -15,6 +15,9 @@ import {
SelectValue,
} from "@/src/components/ui/select";
import { usePostHogClientCapture } from "@/src/features/posthog-analytics/usePostHogClientCapture";
import { LoaderCircle } from "lucide-react";
import { Input } from "@/src/components/ui/input";
import { useEffect, useState } from "react";
interface DataTablePaginationProps<TData> {
table: Table<TData>;
@@ -26,6 +29,22 @@ export function DataTablePagination<TData>({
paginationOptions = [10, 20, 30, 40, 50],
}: DataTablePaginationProps<TData>) {
const capture = usePostHogClientCapture();
const currentPage = table.getState().pagination.pageIndex + 1;
const [inputState, setInputState] = useState<number | string>(currentPage);
useEffect(() => {
setInputState(currentPage);
}, [currentPage]);
const pageCount = table.getPageCount();
const setPageIndex = table.setPageIndex;
useEffect(() => {
if (currentPage > pageCount && pageCount > 0) {
setPageIndex(0);
}
}, [currentPage, pageCount, setPageIndex]);
return (
<div className="flex items-center justify-between">
<div className="flex-1 text-sm text-muted-foreground">
@@ -61,10 +80,60 @@ export function DataTablePagination<TData>({
</SelectContent>
</Select>
</div>
<div className="flex items-center justify-center whitespace-nowrap text-sm font-medium">
Page {table.getState().pagination.pageIndex + 1} of{" "}
{Math.max(table.getPageCount(), 1)}
<div className="flex items-center justify-center gap-1 whitespace-nowrap text-sm font-medium">
{table.getPageCount() !== -1 ? (
<>
Page
<Input
type="number"
min={1}
max={pageCount}
value={inputState} // Ensure the value is within bounds
onChange={(e) => {
setInputState(e.target.value);
}}
onBlur={(e) => {
const newValue = e.target.value;
if (newValue === "") {
table.setPageIndex(0);
setInputState(1);
return;
}
// if nan, reset to current page
if (isNaN(Number(newValue))) {
setInputState(currentPage);
return;
}
const newPageIndex = Number(newValue) - 1;
if (newPageIndex < 0 || newPageIndex >= pageCount) {
setInputState(currentPage);
return;
}
table.setPageIndex(newPageIndex);
setInputState(newPageIndex + 1);
}}
className="h-8 appearance-none"
style={{
width: `${3 + Math.max(1, pageCount.toString().length)}ch`,
}}
/>
</>
) : (
`Page ${currentPage}`
)}
{pageCount !== -1 ? (
<span>of {pageCount}</span>
) : (
<span>
of{" "}
<LoaderCircle className="ml-1 inline-block h-3 w-3 animate-spin text-muted-foreground" />
</span>
)}
</div>
<div className="flex items-center space-x-2">
<Button
variant="outline"
@@ -112,12 +181,12 @@ export function DataTablePagination<TData>({
variant="outline"
className="hidden h-8 w-8 p-0 lg:flex"
onClick={() => {
table.setPageIndex(table.getPageCount() - 1);
table.setPageIndex(pageCount - 1);
capture("table:pagination_button_click", {
type: "lastPage",
});
}}
disabled={!table.getCanNextPage()}
disabled={!table.getCanNextPage() || pageCount === -1}
>
<span className="sr-only">Go to last page</span>
<DoubleArrowRightIcon className="h-4 w-4" />
@@ -31,7 +31,9 @@ interface DataTableToolbarProps<TData, TValue> {
searchConfig?: SearchConfig;
actionButtons?: React.ReactNode;
filterState?: FilterState;
setFilterState?: Dispatch<SetStateAction<FilterState>>;
setFilterState?:
| Dispatch<SetStateAction<FilterState>>
| ((newState: FilterState) => void);
columnVisibility?: VisibilityState;
setColumnVisibility?: Dispatch<SetStateAction<VisibilityState>>;
rowHeight?: RowHeight;
@@ -79,7 +81,7 @@ export function DataTableToolbar<TData, TValue>({
searchConfig.updateQuery(searchString);
}
}}
className="h-10 w-[150px] rounded-r-none @6xl:w-[250px]"
className="w-[150px] rounded-r-none @6xl:w-[250px]"
/>
<Button
variant="outline"
+8 -2
View File
@@ -36,7 +36,7 @@ interface DataTableProps<TData, TValue> {
columns: LangfuseColumnDef<TData, TValue>[];
data: AsyncTableData<TData[]>;
pagination?: {
pageCount: number;
totalCount: number | null; // null if loading
onChange: OnChangeFn<PaginationState>;
state: PaginationState;
options?: number[];
@@ -90,7 +90,13 @@ export function DataTable<TData extends object, TValue>({
getFilteredRowModel: getFilteredRowModel(),
getCoreRowModel: getCoreRowModel(),
manualPagination: pagination !== undefined,
pageCount: pagination?.pageCount ?? 0,
pageCount:
pagination?.totalCount === null ||
pagination?.state.pageSize === undefined
? -1
: Math.ceil(
Number(pagination?.totalCount) / pagination?.state.pageSize,
),
onPaginationChange: pagination?.onChange,
onRowSelectionChange: setRowSelection,
onColumnVisibilityChange: onColumnVisibilityChange,
+1 -1
View File
@@ -17,7 +17,7 @@ export default function TableLink({
return (
<Link
className={cn(
"inline-block max-w-full overflow-hidden text-ellipsis text-nowrap rounded bg-primary-accent/20 px-2 py-1 text-xs font-semibold text-accent-dark-blue shadow-sm hover:bg-accent-light-blue/45",
"inline-block max-w-full overflow-hidden text-ellipsis text-nowrap rounded bg-primary-accent/20 px-2 py-0.5 text-xs font-semibold text-accent-dark-blue shadow-sm hover:bg-accent-light-blue/45",
className,
)}
href={path}
@@ -49,6 +49,7 @@ import { useTableDateRange } from "@/src/hooks/useTableDateRange";
import { useDebounce } from "@/src/hooks/useDebounce";
import { type ScoreAggregate } from "@/src/features/scores/lib/types";
import { useIndividualScoreColumns } from "@/src/features/scores/hooks/useIndividualScoreColumns";
import TagList from "@/src/features/tag/components/TagList";
export type GenerationsTableRow = {
id: string;
@@ -80,6 +81,7 @@ export type GenerationsTableRow = {
promptId?: string;
promptName?: string;
promptVersion?: string;
traceTags?: string[];
};
export type GenerationsTableProps = {
@@ -113,11 +115,12 @@ export default function GenerationsTable({
);
const { selectedOption, dateRange, setDateRangeAndOption } =
useTableDateRange();
useTableDateRange(projectId);
const [inputFilterState, setInputFilterState] = useQueryFilterState(
[],
"generations",
projectId,
);
const [orderByState, setOrderByState] = useOrderByState({
@@ -164,16 +167,26 @@ export default function GenerationsTable({
...promptVersionFilter,
]);
const generations = api.generations.all.useQuery({
page: paginationState.pageIndex,
limit: paginationState.pageSize,
const getCountPayload = {
projectId,
filter: filterState,
orderBy: orderByState,
searchQuery,
});
page: 0,
limit: 0,
orderBy: null,
};
const totalCount = generations.data?.totalCount ?? 0;
const getAllPayload = {
...getCountPayload,
page: paginationState.pageIndex,
limit: paginationState.pageSize,
orderBy: orderByState,
};
const generations = api.generations.all.useQuery(getAllPayload);
const totalCountQuery = api.generations.countAll.useQuery(getCountPayload);
const totalCount = totalCountQuery.data?.totalCount ?? null;
const startTimeFilter = filterState.find((f) => f.column === "Start Time");
const filterOptions = api.generations.filterOptions.useQuery(
@@ -653,6 +666,30 @@ export default function GenerationsTable({
);
},
},
{
accessorKey: "traceTags",
id: "traceTags",
header: "Trace Tags",
size: 250,
enableHiding: true,
defaultHidden: true,
cell: ({ row }) => {
const traceTags: string[] | undefined = row.getValue("traceTags");
console.log(traceTags);
return (
traceTags && (
<div
className={cn(
"flex gap-x-2 gap-y-1",
rowHeight !== "s" && "flex-wrap",
)}
>
<TagList selectedTags={traceTags} isLoading={false} viewOnly />
</div>
)
);
},
},
];
const [columnVisibility, setColumnVisibilityState] =
@@ -692,6 +729,7 @@ export default function GenerationsTable({
promptId: generation.promptId ?? undefined,
promptName: generation.promptName ?? undefined,
promptVersion: generation.promptVersion ?? undefined,
traceTags: generation.traceTags ?? undefined,
};
})
: [];
@@ -709,6 +747,7 @@ export default function GenerationsTable({
updateQuery: setSearchQuery,
currentQuery: searchQuery ?? undefined,
}}
columnsWithCustomSelect={["model", "name", "traceName", "promptName"]}
columnVisibility={columnVisibility}
setColumnVisibility={setColumnVisibilityState}
rowHeight={rowHeight}
@@ -766,7 +805,7 @@ export default function GenerationsTable({
}
}
pagination={{
pageCount: Math.ceil(totalCount / paginationState.pageSize),
totalCount,
onChange: setPaginationState,
state: paginationState,
}}
@@ -63,7 +63,7 @@ export default function ModelTable({ projectId }: { projectId: string }) {
limit: paginationState.pageSize,
projectId,
});
const totalCount = models.data?.totalCount ?? 0;
const totalCount = models.data?.totalCount ?? null;
const [rowHeight, setRowHeight] = useRowHeightLocalStorage("models", "s");
@@ -303,7 +303,7 @@ export default function ModelTable({ projectId }: { projectId: string }) {
}
}
pagination={{
pageCount: Math.ceil(totalCount / paginationState.pageSize),
totalCount,
onChange: setPaginationState,
state: paginationState,
}}
@@ -97,7 +97,7 @@ export function ScoreConfigsTable({ projectId }: { projectId: string }) {
limit: paginationState.pageSize,
});
const totalCount = configs.data?.totalCount ?? 0;
const totalCount = configs.data?.totalCount ?? null;
const columns: LangfuseColumnDef<ScoreConfigTableRow>[] = [
{
@@ -272,7 +272,7 @@ export function ScoreConfigsTable({ projectId }: { projectId: string }) {
}
}
pagination={{
pageCount: Math.ceil(totalCount / paginationState.pageSize),
totalCount,
onChange: setPaginationState,
state: paginationState,
}}
+49 -7
View File
@@ -24,6 +24,8 @@ import {
type ScoreDataType,
} from "@langfuse/shared";
import { useQueryParams, withDefault, NumberParam } from "use-query-params";
import TagList from "@/src/features/tag/components/TagList";
import { cn } from "@/src/utils/tailwind";
export type ScoresTableRow = {
id: string;
@@ -43,6 +45,7 @@ export type ScoresTableRow = {
traceName?: string;
userId?: string;
jobConfigurationId?: string;
traceTags?: string[];
};
export type ScoreFilterInput = Omit<
@@ -90,11 +93,12 @@ export default function ScoresTable({
const [rowHeight, setRowHeight] = useRowHeightLocalStorage("scores", "s");
const { selectedOption, dateRange, setDateRangeAndOption } =
useTableDateRange();
useTableDateRange(projectId);
const [userFilterState, setUserFilterState] = useQueryFilterState(
[],
"scores",
projectId,
);
const dateRangeFilter: FilterState = dateRange
@@ -120,18 +124,32 @@ export default function ScoresTable({
order: "DESC",
});
const scores = api.scores.all.useQuery({
page: paginationState.pageIndex,
limit: paginationState.pageSize,
const getCountPayload = {
projectId,
filter: filterState,
page: 0,
limit: 1,
orderBy: null,
};
const getAllPayload = {
...getCountPayload,
page: paginationState.pageIndex,
limit: paginationState.pageSize,
orderBy: orderByState,
});
const totalCount = scores.data?.totalCount ?? 0;
};
const scores = api.scores.all.useQuery(getAllPayload);
const totalScoreCountQuery = api.scores.countAll.useQuery(getCountPayload);
const totalCount = totalScoreCountQuery.data?.totalCount ?? null;
const filterOptions = api.scores.filterOptions.useQuery(
{
projectId,
timestampFilter:
dateRangeFilter[0]?.type === "datetime"
? dateRangeFilter[0]
: undefined,
},
{
trpc: {
@@ -323,6 +341,29 @@ export default function ScoresTable({
) : undefined;
},
},
{
accessorKey: "traceTags",
id: "traceTags",
header: "Trace Tags",
size: 250,
enableHiding: true,
defaultHidden: true,
cell: ({ row }) => {
const traceTags: string[] | undefined = row.getValue("traceTags");
return (
traceTags && (
<div
className={cn(
"flex gap-x-2 gap-y-1",
rowHeight !== "s" && "flex-wrap",
)}
>
<TagList selectedTags={traceTags} isLoading={false} viewOnly />
</div>
)
);
},
},
];
const columns = rawColumns.filter(
@@ -358,6 +399,7 @@ export default function ScoresTable({
traceName: score.traceName ?? undefined,
userId: score.traceUserId ?? undefined,
jobConfigurationId: score.jobConfigurationId ?? undefined,
traceTags: score.traceTags ?? undefined,
};
};
@@ -401,7 +443,7 @@ export default function ScoresTable({
}
}
pagination={{
pageCount: Math.ceil(totalCount / paginationState.pageSize),
totalCount,
onChange: setPaginationState,
state: paginationState,
}}
+160 -56
View File
@@ -23,20 +23,26 @@ import { NumberParam, useQueryParams, withDefault } from "use-query-params";
import { BatchExportTableButton } from "@/src/components/BatchExportTableButton";
import { useTableDateRange } from "@/src/hooks/useTableDateRange";
import { useDebounce } from "@/src/hooks/useDebounce";
import { joinTableCoreAndMetrics } from "@/src/components/table/utils/joinTableCoreAndMetrics";
import { Skeleton } from "@/src/components/ui/skeleton";
import TagList from "@/src/features/tag/components/TagList";
import { useRowHeightLocalStorage } from "@/src/components/table/data-table-row-height-switch";
import { cn } from "@/src/utils/tailwind";
export type SessionTableRow = {
id: string;
createdAt: string;
userIds: string[];
countTraces: number;
bookmarked: boolean;
sessionDuration: number | null;
inputCost: Decimal;
outputCost: Decimal;
totalCost: Decimal;
inputTokens: number;
outputTokens: number;
totalTokens: number;
userIds: string[] | undefined;
countTraces: number | undefined;
sessionDuration: number | null | undefined;
inputCost: Decimal | undefined;
outputCost: Decimal | undefined;
totalCost: Decimal | undefined;
inputTokens: number | undefined;
outputTokens: number | undefined;
totalTokens: number | undefined;
traceTags: string[] | undefined;
};
export type SessionTableProps = {
@@ -52,11 +58,12 @@ export default function SessionsTable({
}: SessionTableProps) {
const { setDetailPageList } = useDetailPageLists();
const { selectedOption, dateRange, setDateRangeAndOption } =
useTableDateRange();
useTableDateRange(projectId);
const [userFilterState, setUserFilterState] = useQueryFilterState(
[],
"sessions",
projectId,
);
const userIdFilter: FilterState = userId
@@ -88,22 +95,56 @@ export default function SessionsTable({
pageSize: withDefault(NumberParam, 50),
});
const [rowHeight, setRowHeight] = useRowHeightLocalStorage("sessions", "s");
const [orderByState, setOrderByState] = useOrderByState({
column: "createdAt",
order: "DESC",
});
const sessions = api.sessions.all.useQuery({
page: paginationState.pageIndex,
limit: paginationState.pageSize,
const payloadCount = {
projectId,
filter: filterState,
orderBy: null,
page: 0,
limit: 1,
};
const payloadGetAll = {
...payloadCount,
orderBy: orderByState,
});
page: paginationState.pageIndex,
limit: paginationState.pageSize,
};
const sessions = api.sessions.all.useQuery(payloadGetAll);
const sessionCountQuery = api.sessions.countAll.useQuery(payloadCount);
const sessionMetrics = api.sessions.metrics.useQuery(
{
projectId,
sessionIds: sessions.data?.sessions.map((s) => s.id) ?? [],
},
{
enabled: sessions.data !== undefined,
},
);
type SessionCoreOutput = RouterOutput["sessions"]["all"]["sessions"][number];
type SessionMetricOutput = RouterOutput["sessions"]["metrics"][number];
const sessionRowData = joinTableCoreAndMetrics<
SessionCoreOutput,
SessionMetricOutput
>(sessions.data?.sessions, sessionMetrics.data);
const filterOptions = api.sessions.filterOptions.useQuery(
{
projectId,
timestampFilter:
dateRangeFilter[0]?.type === "datetime"
? dateRangeFilter[0]
: undefined,
},
{
trpc: {
@@ -114,36 +155,17 @@ export default function SessionsTable({
},
);
const totalCount = sessions.data?.slice(1)[0]?.totalCount ?? 0;
const totalCount = sessionCountQuery.data?.totalCount ?? null;
useEffect(() => {
if (sessions.isSuccess) {
setDetailPageList(
"sessions",
sessions.data.map((t) => t.id),
sessions.data.sessions.map((t) => t.id),
);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [sessions.isSuccess, sessions.data]);
const convertToTableRow = (
session: RouterOutput["sessions"]["all"][0],
): SessionTableRow => {
return {
id: session.id,
createdAt: session.createdAt.toLocaleString(),
userIds: session.userIds,
countTraces: session.countTraces,
bookmarked: session.bookmarked,
sessionDuration: session.sessionDuration,
inputCost: session.inputCost,
outputCost: session.outputCost,
totalCost: session.totalCost,
inputTokens: session.promptTokens,
outputTokens: session.completionTokens,
totalTokens: session.totalTokens,
};
};
const columns: LangfuseColumnDef<SessionTableRow>[] = [
{
accessorKey: "bookmarked",
@@ -151,8 +173,9 @@ export default function SessionsTable({
header: undefined,
size: 50,
cell: ({ row }) => {
const bookmarked = row.getValue("bookmarked");
const sessionId = row.getValue("id");
const bookmarked: SessionTableRow["bookmarked"] =
row.getValue("bookmarked");
const sessionId: SessionTableRow["id"] = row.getValue("id");
return typeof sessionId === "string" &&
typeof bookmarked === "boolean" ? (
@@ -160,7 +183,7 @@ export default function SessionsTable({
sessionId={sessionId}
projectId={projectId}
value={bookmarked}
size="xs"
size="icon-xs"
/>
) : undefined;
},
@@ -172,7 +195,7 @@ export default function SessionsTable({
header: "ID",
size: 200,
cell: ({ row }) => {
const value = row.getValue("id");
const value: SessionTableRow["id"] = row.getValue("id");
return value && typeof value === "string" ? (
<TableLink
path={`/project/${projectId}/sessions/${encodeURIComponent(value)}`}
@@ -197,7 +220,11 @@ export default function SessionsTable({
size: 130,
enableHiding: true,
cell: ({ row }) => {
const value = row.getValue("sessionDuration");
const value: SessionTableRow["sessionDuration"] =
row.getValue("sessionDuration");
if (!sessionMetrics.isSuccess) {
return <Skeleton className="h-3 w-1/2" />;
}
return value && typeof value === "number"
? formatIntervalSeconds(value)
: undefined;
@@ -212,7 +239,10 @@ export default function SessionsTable({
size: 200,
enableHiding: true,
cell: ({ row }) => {
const value = row.getValue("userIds");
const value: SessionTableRow["userIds"] = row.getValue("userIds");
if (!sessionMetrics.isSuccess) {
return <Skeleton className="h-3 w-1/2" />;
}
return value && Array.isArray(value) ? (
<div className="flex gap-1">
{(value as string[]).map((user) => (
@@ -236,6 +266,14 @@ export default function SessionsTable({
},
enableHiding: true,
enableSorting: true,
cell: ({ row }) => {
const value: SessionTableRow["countTraces"] =
row.getValue("countTraces");
if (!sessionMetrics.isSuccess) {
return <Skeleton className="h-3 w-1/2" />;
}
return value ? <span>{numberFormatter(value, 0)}</span> : undefined;
},
},
{
accessorKey: "inputCost",
@@ -246,7 +284,10 @@ export default function SessionsTable({
defaultHidden: true,
enableSorting: true,
cell: ({ row }) => {
const value: Decimal | null | undefined = row.getValue("inputCost");
const value: SessionTableRow["inputCost"] = row.getValue("inputCost");
if (!sessionMetrics.isSuccess) {
return <Skeleton className="h-3 w-1/2" />;
}
return value ? (
<span>{usdFormatter(value.toNumber())}</span>
) : undefined;
@@ -261,8 +302,10 @@ export default function SessionsTable({
enableSorting: true,
defaultHidden: true,
cell: ({ row }) => {
const value: Decimal | null | undefined = row.getValue("outputCost");
const value: SessionTableRow["outputCost"] = row.getValue("outputCost");
if (!sessionMetrics.isSuccess) {
return <Skeleton className="h-3 w-1/2" />;
}
return value ? (
<span>{usdFormatter(value.toNumber())}</span>
) : undefined;
@@ -276,8 +319,10 @@ export default function SessionsTable({
enableHiding: true,
enableSorting: true,
cell: ({ row }) => {
const value: Decimal | null | undefined = row.getValue("totalCost");
const value: SessionTableRow["totalCost"] = row.getValue("totalCost");
if (!sessionMetrics.isSuccess) {
return <Skeleton className="h-3 w-1/2" />;
}
return value ? (
<span>{usdFormatter(value.toNumber())}</span>
) : undefined;
@@ -292,8 +337,11 @@ export default function SessionsTable({
defaultHidden: true,
enableSorting: true,
cell: ({ row }) => {
const value: number | undefined = row.getValue("inputTokens");
const value: SessionTableRow["inputTokens"] =
row.getValue("inputTokens");
if (!sessionMetrics.isSuccess) {
return <Skeleton className="h-3 w-1/2" />;
}
return value ? (
<span>{numberFormatter(Number(value), 0)}</span>
) : undefined;
@@ -308,8 +356,11 @@ export default function SessionsTable({
defaultHidden: true,
enableSorting: true,
cell: ({ row }) => {
const value = row.getValue("outputTokens");
const value: SessionTableRow["outputTokens"] =
row.getValue("outputTokens");
if (!sessionMetrics.isSuccess) {
return <Skeleton className="h-3 w-1/2" />;
}
return value ? (
<span>{numberFormatter(Number(value), 0)}</span>
) : undefined;
@@ -324,7 +375,11 @@ export default function SessionsTable({
defaultHidden: true,
enableSorting: true,
cell: ({ row }) => {
const value = row.getValue("totalTokens");
const value: SessionTableRow["totalTokens"] =
row.getValue("totalTokens");
if (!sessionMetrics.isSuccess) {
return <Skeleton className="h-3 w-1/2" />;
}
return value ? (
<span>{numberFormatter(Number(value), 0)}</span>
) : undefined;
@@ -338,9 +393,15 @@ export default function SessionsTable({
enableHiding: true,
enableSorting: true,
cell: ({ row }) => {
const promptTokens = row.getValue("inputTokens");
const completionTokens = row.getValue("outputTokens");
const totalTokens = row.getValue("totalTokens");
const promptTokens: SessionTableRow["inputTokens"] =
row.getValue("inputTokens");
const completionTokens: SessionTableRow["outputTokens"] =
row.getValue("outputTokens");
const totalTokens: SessionTableRow["totalTokens"] =
row.getValue("totalTokens");
if (!sessionMetrics.isSuccess) {
return <Skeleton className="h-3 w-1/2" />;
}
return (
<TokenUsageBadge
promptTokens={Number(promptTokens)}
@@ -351,6 +412,32 @@ export default function SessionsTable({
);
},
},
{
accessorKey: "traceTags",
id: "traceTags",
header: "Trace Tags",
size: 250,
enableHiding: true,
defaultHidden: true,
cell: ({ row }) => {
const value: SessionTableRow["traceTags"] = row.getValue("traceTags");
if (!sessionMetrics.isSuccess) {
return <Skeleton className="h-3 w-1/2" />;
}
return (
value && (
<div
className={cn(
"flex gap-x-2 gap-y-1",
rowHeight !== "s" && "flex-wrap",
)}
>
<TagList selectedTags={value} isLoading={false} viewOnly />
</div>
)
);
},
},
];
const transformFilterOptions = () => {
@@ -381,6 +468,8 @@ export default function SessionsTable({
selectedOption={selectedOption}
setDateRangeAndOption={setDateRangeAndOption}
columnsWithCustomSelect={["userIds"]}
rowHeight={rowHeight}
setRowHeight={setRowHeight}
/>
<DataTable
columns={columns}
@@ -396,11 +485,25 @@ export default function SessionsTable({
: {
isLoading: false,
isError: false,
data: sessions.data.map((t) => convertToTableRow(t)),
data: sessionRowData.rows?.map((session) => ({
id: session.id,
createdAt: session.createdAt.toLocaleString(),
userIds: session.userIds,
countTraces: session.countTraces,
bookmarked: session.bookmarked,
sessionDuration: session.sessionDuration,
inputCost: session.inputCost,
outputCost: session.outputCost,
totalCost: session.totalCost,
inputTokens: session.promptTokens,
outputTokens: session.completionTokens,
totalTokens: session.totalTokens,
traceTags: session.traceTags,
})),
}
}
pagination={{
pageCount: Math.ceil(totalCount / paginationState.pageSize),
totalCount,
onChange: setPaginationState,
state: paginationState,
}}
@@ -413,6 +516,7 @@ export default function SessionsTable({
"A session is a collection of related traces, such as a conversation or thread. To begin, add a sessionId to the trace.",
href: "https://langfuse.com/docs/tracing-features/sessions",
}}
rowHeight={rowHeight}
/>
</>
);
+80 -26
View File
@@ -11,7 +11,7 @@ import useColumnVisibility from "@/src/features/column-visibility/hooks/useColum
import { useQueryFilterState } from "@/src/features/filters/hooks/useFilterState";
import { api } from "@/src/utils/api";
import { formatIntervalSeconds } from "@/src/utils/dates";
import { type RouterInput } from "@/src/utils/types";
import { type RouterOutput, type RouterInput } from "@/src/utils/types";
import { type RowSelectionState } from "@tanstack/react-table";
import { useEffect, useMemo, useState } from "react";
import {
@@ -44,6 +44,8 @@ import { useTableDateRange } from "@/src/hooks/useTableDateRange";
import { useDebounce } from "@/src/hooks/useDebounce";
import { type ScoreAggregate } from "@/src/features/scores/lib/types";
import { useIndividualScoreColumns } from "@/src/features/scores/hooks/useIndividualScoreColumns";
import { joinTableCoreAndMetrics } from "@/src/components/table/utils/joinTableCoreAndMetrics";
import { Skeleton } from "@/src/components/ui/skeleton";
export type TracesTableRow = {
bookmarked: boolean;
@@ -51,10 +53,10 @@ export type TracesTableRow = {
timestamp: string;
name: string;
userId: string;
level: ObservationLevel;
observationCount: number;
level?: ObservationLevel;
observationCount?: number;
// scores holds grouped column with individual scores
scores: ScoreAggregate;
scores?: ScoreAggregate;
latency?: number;
release?: string;
version?: string;
@@ -65,9 +67,9 @@ export type TracesTableRow = {
metadata?: unknown;
tags: string[];
usage: {
promptTokens: bigint;
completionTokens: bigint;
totalTokens: bigint;
promptTokens?: bigint;
completionTokens?: bigint;
totalTokens?: bigint;
};
inputCost?: Decimal;
outputCost?: Decimal;
@@ -96,10 +98,11 @@ export default function TracesTable({
);
const { selectedOption, dateRange, setDateRangeAndOption } =
useTableDateRange();
useTableDateRange(projectId);
const [userFilterState, setUserFilterState] = useQueryFilterState(
[],
"traces",
projectId,
);
const [orderByState, setOrderByState] = useOrderByState({
column: "timestamp",
@@ -133,17 +136,44 @@ export default function TracesTable({
pageSize: withDefault(NumberParam, 50),
});
const tracesAllQueryFilter = {
page: paginationState.pageIndex,
limit: paginationState.pageSize,
const tracesAllCountFilter = {
projectId,
filter: filterState,
searchQuery,
// "empty" values as they do not matter for total count
page: 0,
limit: 0,
orderBy: null,
};
const tracesAllQueryFilter = {
...tracesAllCountFilter,
page: paginationState.pageIndex,
limit: paginationState.pageSize,
orderBy: orderByState,
};
const traces = api.traces.all.useQuery(tracesAllQueryFilter);
const totalCountQuery = api.traces.countAll.useQuery(tracesAllCountFilter);
const traceMetrics = api.traces.metrics.useQuery(
{
projectId,
traceIds: traces.data?.traces.map((t) => t.id) ?? [],
},
{
enabled: traces.data !== undefined,
},
);
type TracesCoreOutput = RouterOutput["traces"]["all"]["traces"][number];
type TraceMetricOutput = RouterOutput["traces"]["metrics"][number];
const traceRowData = joinTableCoreAndMetrics<
TracesCoreOutput,
TraceMetricOutput
>(traces.data?.traces, traceMetrics.data);
const totalCount = totalCountQuery.data?.totalCount ?? null;
const totalCount = traces.data?.totalCount ?? 0;
useEffect(() => {
if (traces.isSuccess) {
setDetailPageList(
@@ -188,6 +218,7 @@ export default function TracesTable({
projectId,
scoreColumnKey: "scores",
selectedFilterOption: selectedOption,
cellsLoading: !traceMetrics.data,
});
const columns: LangfuseColumnDef<TracesTableRow>[] = [
@@ -239,7 +270,7 @@ export default function TracesTable({
traceId={traceId}
projectId={projectId}
value={bookmarked}
size="xs"
size="icon-xs"
/>
) : undefined;
},
@@ -328,6 +359,7 @@ export default function TracesTable({
// add seconds to the end of the latency
cell: ({ row }) => {
const value: TracesTableRow["latency"] = row.getValue("latency");
if (!traceMetrics.data) return <Skeleton className="h-3 w-1/2" />;
return value !== undefined ? formatIntervalSeconds(value) : undefined;
},
enableHiding: true,
@@ -340,6 +372,7 @@ export default function TracesTable({
size: 110,
cell: ({ row }) => {
const value: TracesTableRow["usage"] = row.getValue("usage");
if (!traceMetrics.data) return <Skeleton className="h-3 w-1/2" />;
return <span>{numberFormatter(value.promptTokens, 0)}</span>;
},
enableHiding: true,
@@ -353,6 +386,7 @@ export default function TracesTable({
size: 110,
cell: ({ row }) => {
const value: TracesTableRow["usage"] = row.getValue("usage");
if (!traceMetrics.data) return <Skeleton className="h-3 w-1/2" />;
return <span>{numberFormatter(value.completionTokens, 0)}</span>;
},
enableHiding: true,
@@ -366,6 +400,7 @@ export default function TracesTable({
size: 110,
cell: ({ row }) => {
const value: TracesTableRow["usage"] = row.getValue("usage");
if (!traceMetrics.data) return <Skeleton className="h-3 w-1/2" />;
return <span>{numberFormatter(value.totalTokens, 0)}</span>;
},
enableHiding: true,
@@ -379,11 +414,12 @@ export default function TracesTable({
size: 220,
cell: ({ row }) => {
const value: TracesTableRow["usage"] = row.getValue("usage");
if (!traceMetrics.data) return <Skeleton className="h-3 w-1/2" />;
return (
<TokenUsageBadge
promptTokens={value.promptTokens}
completionTokens={value.completionTokens}
totalTokens={value.totalTokens}
promptTokens={value.promptTokens ?? 0}
completionTokens={value.completionTokens ?? 0}
totalTokens={value.totalTokens ?? 0}
inline
/>
);
@@ -391,7 +427,10 @@ export default function TracesTable({
enableSorting: true,
enableHiding: true,
},
{ ...getScoreGroupColumnProps(isColumnLoading), columns: scoreColumns },
{
...getScoreGroupColumnProps(isColumnLoading || !traceMetrics.data),
columns: scoreColumns,
},
{
accessorKey: "inputCost",
id: "inputCost",
@@ -399,6 +438,7 @@ export default function TracesTable({
size: 100,
cell: ({ row }) => {
const cost: TracesTableRow["inputCost"] = row.getValue("inputCost");
if (!traceMetrics.data) return <Skeleton className="h-3 w-1/2" />;
return (
<div>
{cost ? (
@@ -420,6 +460,7 @@ export default function TracesTable({
size: 100,
cell: ({ row }) => {
const cost: TracesTableRow["outputCost"] = row.getValue("outputCost");
if (!traceMetrics.data) return <Skeleton className="h-3 w-1/2" />;
return (
<div>
{cost ? (
@@ -441,6 +482,7 @@ export default function TracesTable({
size: 100,
cell: ({ row }) => {
const cost: TracesTableRow["totalCost"] = row.getValue("totalCost");
if (!traceMetrics.data) return <Skeleton className="h-3 w-1/2" />;
return (
<div>
{cost ? (
@@ -521,7 +563,8 @@ export default function TracesTable({
size: 75,
cell: ({ row }) => {
const value: TracesTableRow["level"] = row.getValue("level");
return (
if (!traceMetrics.data) return <Skeleton className="h-3 w-1/2" />;
return value ? (
<span
className={cn(
"rounded-sm p-0.5 text-xs",
@@ -531,6 +574,8 @@ export default function TracesTable({
>
{value}
</span>
) : (
<span>-</span>
);
},
enableHiding: true,
@@ -547,6 +592,12 @@ export default function TracesTable({
},
enableHiding: true,
defaultHidden: true,
cell: ({ row }) => {
const value: TracesTableRow["observationCount"] =
row.getValue("observationCount");
if (!traceMetrics.data) return <Skeleton className="h-3 w-1/2" />;
return <span>{value}</span>;
},
},
{
accessorKey: "version",
@@ -629,7 +680,7 @@ export default function TracesTable({
const rows = useMemo(() => {
return traces.isSuccess
? traces.data.traces.map((trace) => {
? traceRowData?.rows?.map((trace) => {
return {
bookmarked: trace.bookmarked,
id: trace.id,
@@ -648,17 +699,19 @@ export default function TracesTable({
completionTokens: trace.completionTokens,
totalTokens: trace.totalTokens,
},
scores: verifyAndPrefixScoreDataAgainstKeys(
scoreKeysAndProps,
trace.scores,
),
scores: trace.scores
? verifyAndPrefixScoreDataAgainstKeys(
scoreKeysAndProps,
trace.scores,
)
: undefined,
inputCost: trace.calculatedInputCost ?? undefined,
outputCost: trace.calculatedOutputCost ?? undefined,
totalCost: trace.calculatedTotalCost ?? undefined,
};
})
}) ?? []
: [];
}, [traces, scoreKeysAndProps]);
}, [traces, traceRowData, scoreKeysAndProps]);
return (
<>
@@ -672,6 +725,7 @@ export default function TracesTable({
}}
filterState={userFilterState}
setFilterState={useDebounce(setUserFilterState)}
columnsWithCustomSelect={["name", "tags"]}
actionButtons={
Object.keys(selectedRows).filter((traceId) =>
traces.data?.traces.map((t) => t.id).includes(traceId),
@@ -713,7 +767,7 @@ export default function TracesTable({
}
}
pagination={{
pageCount: Math.ceil(Number(totalCount) / paginationState.pageSize),
totalCount,
onChange: setPaginationState,
state: paginationState,
}}
@@ -186,7 +186,6 @@ export const ObservationPreview = (props: {
source="generation"
generation={observationWithInputAndOutput.data}
analyticsEventName="trace_detail:test_in_playground_button_click"
fullWidth
/>
)}
{observationWithInputAndOutput.data ? (
+5 -3
View File
@@ -84,7 +84,9 @@ const ObservationTreeTraceNode = (props: {
onClick={() => props.setCurrentObservationId(undefined)}
>
<div className="flex gap-2">
<span className={cn("rounded-sm bg-input p-1 text-xs")}>TRACE</span>
<span className={cn("rounded-sm bg-input px-1 py-0.5 text-xs")}>
TRACE
</span>
<span className="break-all text-sm">{props.trace.name}</span>
{props.comments ? (
<CommentCountIcon count={props.comments.get(props.trace.id)} />
@@ -191,7 +193,7 @@ const ObservationTreeNode = (props: {
variant="default"
pressed={collapsed}
size="xs"
className="w-7"
className="-m-1 h-6 w-6"
title={
collapsed ? "Expand children" : "Collapse children"
}
@@ -284,7 +286,7 @@ const ColorCodedObservationType = (props: {
return (
<span
className={cn(
"self-start rounded-sm p-1 text-xs",
"self-start rounded-sm px-1 py-0.5 text-xs",
treeItemColors.get(props.observationType),
)}
>
+17 -12
View File
@@ -30,6 +30,7 @@ import { TracePreview } from "@/src/components/trace/TracePreview";
import { ObservationPreview } from "@/src/components/trace/ObservationPreview";
import useSessionStorage from "@/src/components/useSessionStorage";
import { api } from "@/src/utils/api";
import { useSession } from "next-auth/react";
// Fixed widths for styling for v1
const SCALE_WIDTH = 800;
@@ -45,14 +46,12 @@ const PREDEFINED_STEP_SIZES = [
const getNestedObservationKeys = (
observations: NestedObservation[],
): { keys: string[]; ids: string[] } => {
): string[] => {
const keys: string[] = [];
const ids: string[] = [];
const collectKeys = (obs: NestedObservation[]) => {
obs.forEach((observation) => {
keys.push(`observation-${observation.id}`);
ids.push(observation.id);
if (observation.children) {
collectKeys(observation.children);
}
@@ -60,7 +59,7 @@ const getNestedObservationKeys = (
};
collectKeys(observations);
return { keys, ids };
return keys;
};
const calculateStepSize = (latency: number, scaleWidth: number) => {
@@ -97,7 +96,7 @@ function TreeItemInner({
const customLabelWidth = cardWidth - SCALE_WIDTH - CARD_PADDING;
return (
<div className="group my-1 grid w-full min-w-fit grid-cols-[1fr,auto] items-center">
<div className="group my-0.5 grid w-full min-w-fit grid-cols-[1fr,auto] items-center">
<div
className="flex flex-row items-center gap-2"
style={{
@@ -106,7 +105,10 @@ function TreeItemInner({
}}
>
<span
className={cn("rounded-sm p-1 text-xs", treeItemColors.get(type))}
className={cn(
"rounded-sm px-1 py-0.5 text-xs",
treeItemColors.get(type),
)}
>
{type}
</span>
@@ -305,15 +307,16 @@ export function TraceTimelineView({
() => nestObservations(observations),
[observations],
);
const { keys: nestedObservationKeys, ids: nestedObservationIds } = useMemo(
const nestedObservationKeys = useMemo(
() => getNestedObservationKeys(nestedObservations),
[nestedObservations],
);
const observationCommentCounts = api.comments.getCountsByObjectIds.useQuery(
const session = useSession();
const observationCommentCounts = api.comments.getCountByObjectType.useQuery(
{
projectId: trace.projectId,
objectIds: nestedObservationIds,
objectType: "OBSERVATION",
},
{
@@ -323,13 +326,14 @@ export function TraceTimelineView({
},
},
refetchOnMount: false, // prevents refetching loops
enabled: session.status === "authenticated",
},
);
const traceCommentCounts = api.comments.getCountsByObjectIds.useQuery(
const traceCommentCounts = api.comments.getCountByObjectId.useQuery(
{
projectId: trace.projectId,
objectIds: [trace.id],
objectId: trace.id,
objectType: "TRACE",
},
{
@@ -339,6 +343,7 @@ export function TraceTimelineView({
},
},
refetchOnMount: false, // prevents refetching loops
enabled: session.status === "authenticated",
},
);
@@ -360,7 +365,7 @@ export function TraceTimelineView({
minWidth: `${MIN_LABEL_WIDTH}px`,
}}
>
<h3 className="text-2xl font-semibold tracking-tight">
<h3 className="text-xl font-semibold tracking-tight">
Trace Timeline
</h3>
<div className="flex h-full items-center">
+13 -13
View File
@@ -27,13 +27,14 @@ import {
} from "lucide-react";
import { usdFormatter } from "@/src/utils/numbers";
import Decimal from "decimal.js";
import { useCallback, useMemo, useState } from "react";
import { useCallback, useState } from "react";
import { DeleteButton } from "@/src/components/deleteButton";
import { usePostHogClientCapture } from "@/src/features/posthog-analytics/usePostHogClientCapture";
import { Tabs, TabsList, TabsTrigger } from "@/src/components/ui/tabs";
import { TraceTimelineView } from "@/src/components/trace/TraceTimelineView";
import { type APIScore } from "@langfuse/shared";
import { useSession } from "next-auth/react";
import { FullScreenPage } from "@/src/components/layouts/full-screen-page";
export function Trace(props: {
observations: Array<ObservationReturnType>;
@@ -57,14 +58,11 @@ export function Trace(props: {
[],
);
const observationObjectIds: string[] = useMemo(() => {
return props.observations.map(({ id }) => id);
}, [props.observations]);
const session = useSession();
const observationCommentCounts = api.comments.getCountsByObjectIds.useQuery(
const observationCommentCounts = api.comments.getCountByObjectType.useQuery(
{
projectId: props.trace.projectId,
objectIds: observationObjectIds,
objectType: "OBSERVATION",
},
{
@@ -74,13 +72,14 @@ export function Trace(props: {
},
},
refetchOnMount: false, // prevents refetching loops
enabled: session.status === "authenticated",
},
);
const traceCommentCounts = api.comments.getCountsByObjectIds.useQuery(
const traceCommentCounts = api.comments.getCountByObjectId.useQuery(
{
projectId: props.trace.projectId,
objectIds: [props.trace.id],
objectId: props.trace.id,
objectType: "TRACE",
},
{
@@ -90,6 +89,7 @@ export function Trace(props: {
},
},
refetchOnMount: false, // prevents refetching loops
enabled: session.status === "authenticated",
},
);
@@ -173,7 +173,7 @@ export function Trace(props: {
});
setScoresOnObservationTree(e);
}}
size="sm"
size="xs"
title="Show scores"
>
<Award className="h-4 w-4" />
@@ -186,7 +186,7 @@ export function Trace(props: {
});
setMetricsOnObservationTree(e);
}}
size="sm"
size="xs"
title="Show metrics"
>
{metricsOnObservationTree ? (
@@ -223,7 +223,7 @@ export function TracePage({ traceId }: { traceId: string }) {
const router = useRouter();
const utils = api.useUtils();
const session = useSession();
const trace = api.traces.byId.useQuery(
const trace = api.traces.byIdWithObservationsAndScores.useQuery(
{ traceId, projectId: router.query.projectId as string },
{
retry(failureCount, error) {
@@ -264,7 +264,7 @@ export function TracePage({ traceId }: { traceId: string }) {
return <ErrorPage message="You do not have access to this trace." />;
if (!trace.data) return <div>loading...</div>;
return (
<div className="flex flex-col overflow-hidden 2xl:container md:h-[calc(100vh-2rem)]">
<FullScreenPage mobile={false} className="2xl:container">
<Header
title="Trace Detail"
breadcrumb={[
@@ -397,7 +397,7 @@ export function TracePage({ traceId }: { traceId: string }) {
/>
</div>
)}
</div>
</FullScreenPage>
);
}
+9 -4
View File
@@ -56,7 +56,7 @@ export function JSONView(props: {
title="Enable Markdown"
variant="ghost"
type="button"
size="xs"
size="icon-xs"
onClick={() => {
setIsMarkdownEnabled(true);
capture("trace_detail:io_pretty_format_toggle_group", {
@@ -71,10 +71,10 @@ export function JSONView(props: {
<Button
title="Copy to clipboard"
variant="ghost"
size="xs"
size="icon-xs"
type="button"
onClick={handleCopy}
className="hover:bg-border"
className="-mr-2 hover:bg-border"
>
{isCopied ? (
<Check className="h-3 w-3" />
@@ -164,7 +164,12 @@ export function CodeView(props: {
)}
</Button>
) : undefined}
<Button variant="secondary" size="xs" onClick={handleCopy}>
<Button
variant="secondary"
size="icon-xs"
onClick={handleCopy}
className=""
>
{isCopied ? (
<Check className="h-3 w-3" />
) : (
+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 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground",
"relative w-full rounded-lg border p-3 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground",
{
variants: {
variant: {
+1 -1
View File
@@ -12,7 +12,7 @@ const Avatar = React.forwardRef<
<AvatarPrimitive.Root
ref={ref}
className={cn(
"relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full",
"relative flex h-8 w-8 shrink-0 overflow-hidden rounded-full",
className,
)}
{...props}
+6 -5
View File
@@ -25,11 +25,12 @@ const buttonVariants = cva(
"bg-destructive-foreground/90 text-destructive hover:bg-destructive-foreground/80",
},
size: {
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",
default: "h-8 px-3 py-1",
xs: "h-4 px-1 rounded-sm",
sm: "h-6 rounded-md px-3",
lg: "h-9 rounded-md px-8",
icon: "h-8 w-8",
"icon-xs": "h-6 w-6",
},
},
defaultVariants: {
+3 -3
View File
@@ -23,7 +23,7 @@ const CardHeader = React.forwardRef<
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex flex-col space-y-1.5 p-6", className)}
className={cn("flex flex-col space-y-1 p-4", className)}
{...props}
/>
));
@@ -60,7 +60,7 @@ const CardContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
<div ref={ref} className={cn("p-4 pt-0", className)} {...props} />
));
CardContent.displayName = "CardContent";
@@ -70,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-4 pt-0", className)}
{...props}
/>
));
+2 -5
View File
@@ -41,15 +41,12 @@ const CommandInput = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Input>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
>(({ className, ...props }, ref) => (
<div
className="flex items-center rounded-md border px-3"
cmdk-input-wrapper=""
>
<div className="flex items-center rounded 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 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 ",
"flex h-8 w-full rounded 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}
+1 -1
View File
@@ -88,7 +88,7 @@ const DialogTitle = React.forwardRef<
<DialogPrimitive.Title
ref={ref}
className={cn(
"text-lg font-semibold leading-none tracking-tight",
"text-xl font-semibold leading-none tracking-tight",
className,
)}
{...props}
+1 -1
View File
@@ -18,7 +18,7 @@ const HoverCardContent = React.forwardRef<
align={align}
sideOffset={sideOffset}
className={cn(
"z-50 w-64 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",
"z-50 w-64 rounded-md border bg-popover p-3 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}
+1 -1
View File
@@ -11,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-background px-3 py-2 text-sm file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus:ring-0 focus-visible:ring-offset-0 disabled:cursor-not-allowed disabled:opacity-50",
"flex h-8 w-full rounded-md border border-input bg-background px-2 py-1 text-sm file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus:ring-0 focus-visible:ring-offset-0 disabled:cursor-not-allowed disabled:opacity-50",
className,
)}
ref={ref}
+1 -1
View File
@@ -14,7 +14,7 @@ const PasswordInput = React.forwardRef<HTMLInputElement, PasswordInputProps>(
<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",
"flex h-8 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}
+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 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",
"z-50 w-72 rounded-md border bg-popover p-3 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}
+45
View File
@@ -0,0 +1,45 @@
"use client"
import { GripVertical } from "lucide-react"
import * as ResizablePrimitive from "react-resizable-panels"
import { cn } from "@/src/utils/tailwind"
const ResizablePanelGroup = ({
className,
...props
}: React.ComponentProps<typeof ResizablePrimitive.PanelGroup>) => (
<ResizablePrimitive.PanelGroup
className={cn(
"flex h-full w-full data-[panel-group-direction=vertical]:flex-col",
className
)}
{...props}
/>
)
const ResizablePanel = ResizablePrimitive.Panel
const ResizableHandle = ({
withHandle,
className,
...props
}: React.ComponentProps<typeof ResizablePrimitive.PanelResizeHandle> & {
withHandle?: boolean
}) => (
<ResizablePrimitive.PanelResizeHandle
className={cn(
"relative flex w-px items-center justify-center bg-border after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-1 data-[panel-group-direction=vertical]:h-px data-[panel-group-direction=vertical]:w-full data-[panel-group-direction=vertical]:after:left-0 data-[panel-group-direction=vertical]:after:h-1 data-[panel-group-direction=vertical]:after:w-full data-[panel-group-direction=vertical]:after:-translate-y-1/2 data-[panel-group-direction=vertical]:after:translate-x-0 [&[data-panel-group-direction=vertical]>div]:rotate-90",
className
)}
{...props}
>
{withHandle && (
<div className="z-10 flex h-4 w-3 items-center justify-center rounded-sm border bg-border">
<GripVertical className="h-2.5 w-2.5" />
</div>
)}
</ResizablePrimitive.PanelResizeHandle>
)
export { ResizablePanelGroup, ResizablePanel, ResizableHandle }
+1 -1
View File
@@ -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-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",
"flex h-8 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}
+2 -2
View File
@@ -11,7 +11,7 @@ const Switch = React.forwardRef<
>(({ className, ...props }, ref) => (
<SwitchPrimitives.Root
className={cn(
"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 dark:data-[state=unchecked]:bg-muted-foreground",
"peer inline-flex h-5 w-9 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 dark:data-[state=unchecked]:bg-muted-foreground",
className,
)}
{...props}
@@ -19,7 +19,7 @@ const Switch = React.forwardRef<
>
<SwitchPrimitives.Thumb
className={cn(
"pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0",
"pointer-events-none block h-4 w-4 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0",
)}
/>
</SwitchPrimitives.Root>
+1 -1
View File
@@ -31,7 +31,7 @@ const TableBody = React.forwardRef<
>(({ className, ...props }, ref) => (
<tbody
ref={ref}
className={cn("[&_tr:last-child]:border-0", className)}
className={cn("text-xs [&_tr:last-child]:border-0", className)}
{...props}
/>
));
+2 -2
View File
@@ -14,7 +14,7 @@ const TabsList = React.forwardRef<
<TabsPrimitive.List
ref={ref}
className={cn(
"inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground",
"inline-flex h-8 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground",
className,
)}
{...props}
@@ -29,7 +29,7 @@ const TabsTrigger = React.forwardRef<
<TabsPrimitive.Trigger
ref={ref}
className={cn(
"inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all 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=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm",
"inline-flex h-6 items-center justify-center whitespace-nowrap rounded-sm px-2 py-0.5 text-sm font-medium ring-offset-background transition-all 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=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm",
className,
)}
{...props}
+1 -1
View File
@@ -16,7 +16,7 @@ const toggleVariants = cva(
"border border-input bg-transparent hover:bg-accent hover:text-accent-foreground",
},
size: {
default: "h-10 px-3",
default: "h-8 px-3",
xs: "h-6 px-1.5",
sm: "h-9 px-2.5",
lg: "h-11 px-5",
+1 -1
View File
@@ -1 +1 @@
export const VERSION = "v2.74.0";
export const VERSION = "v2.76.0";
-1
View File
@@ -16,7 +16,6 @@ if (!process.env.VERCEL && process.env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION) {
opentelemetry.context.setGlobalContextManager(contextManager);
const tracer = dd.init({
profiling: false,
runtimeMetrics: true,
});
@@ -16,10 +16,7 @@ import { useQueryOrganization } from "@/src/features/organizations/hooks";
import { Card } from "@/src/components/ui/card";
import { numberFormatter, compactNumberFormatter } from "@/src/utils/numbers";
import { useHasOrgEntitlement } from "@/src/features/entitlements/hooks";
import {
type Plan,
planLabels,
} from "@/src/features/entitlements/constants/plans";
import { type Plan, planLabels } from "@langfuse/shared";
import { stripeProducts } from "@/src/ee/features/billing/utils/stripeProducts";
import { useRouter } from "next/router";
import {
@@ -52,7 +49,7 @@ export const BillingSettings = () => {
</Alert>
);
return (
<div className="p-4">
<div>
<Header title="Usage & Billing" level="h3" />
<OrganizationUsageChart />
</div>
@@ -81,12 +78,12 @@ const OrganizationUsageChart = () => {
return (
<div>
<Card className="p-4">
<Card className="p-3">
{usage.data !== undefined ? (
<>
<Text>
{usage.data.billingPeriod
? `Observations in billing period`
? `Observations in current billing period`
: "Observations / last 30d"}
</Text>
<Metric>{numberFormatter(usage.data.countObservations, 0)}</Metric>
@@ -107,7 +104,9 @@ const OrganizationUsageChart = () => {
)}
</>
) : (
"Loading (might take a moment) ..."
<span className="text-sm text-muted-foreground">
Loading (might take a moment) ...
</span>
)}
</Card>
<div className="mt-2 flex flex-col gap-1 text-sm text-muted-foreground">
@@ -40,7 +40,7 @@ export const cloudBillingRouter = createTRPCRouter({
});
if (!org) {
throw new TRPCError({
code: "NOT_FOUND",
code: "INTERNAL_SERVER_ERROR",
message: "Organization not found",
});
}
@@ -48,7 +48,7 @@ export const cloudBillingRouter = createTRPCRouter({
const parsedOrg = parseDbOrg(org);
if (parsedOrg.cloudConfig?.plan)
throw new TRPCError({
code: "FORBIDDEN",
code: "INTERNAL_SERVER_ERROR",
message:
"Cannot initialize stripe checkout for orgs that have a manual/legacy plan",
});
@@ -65,7 +65,7 @@ export const cloudBillingRouter = createTRPCRouter({
if (stripeActiveSubscriptionId) {
// If the org has a customer ID, do not return checkout options, should use the billing portal instead
throw new TRPCError({
code: "BAD_REQUEST",
code: "INTERNAL_SERVER_ERROR",
message: "Organization already has an active subscription",
});
}
@@ -78,7 +78,7 @@ export const cloudBillingRouter = createTRPCRouter({
)
)
throw new TRPCError({
code: "BAD_REQUEST",
code: "INTERNAL_SERVER_ERROR",
message: "Invalid stripe product id",
});
@@ -87,7 +87,7 @@ export const cloudBillingRouter = createTRPCRouter({
);
if (!product.default_price) {
throw new TRPCError({
code: "BAD_REQUEST",
code: "INTERNAL_SERVER_ERROR",
message: "Product does not have a default price in Stripe",
});
}
@@ -168,7 +168,7 @@ export const cloudBillingRouter = createTRPCRouter({
});
if (!org) {
throw new TRPCError({
code: "NOT_FOUND",
code: "INTERNAL_SERVER_ERROR",
message: "Organization not found",
});
}
@@ -214,18 +214,6 @@ export const cloudBillingRouter = createTRPCRouter({
session: ctx.session,
});
const thirtyDaysAgo = new Date();
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
thirtyDaysAgo.setHours(0, 0, 0, 0);
let billingPeriod: {
start: Date;
end: Date;
} | null = null;
let upcomingInvoice: {
usdAmount: number;
date: Date;
} | null = null;
const organization = await ctx.prisma.organization.findUnique({
where: {
id: input.orgId,
@@ -233,7 +221,7 @@ export const cloudBillingRouter = createTRPCRouter({
});
if (!organization) {
throw new TRPCError({
code: "NOT_FOUND",
code: "INTERNAL_SERVER_ERROR",
message: "Organization not found",
});
}
@@ -249,35 +237,49 @@ export const cloudBillingRouter = createTRPCRouter({
parsedOrg.cloudConfig.stripe.activeSubscriptionId,
);
if (subscription) {
billingPeriod = {
const billingPeriod = {
start: new Date(subscription.current_period_start * 1000),
end: new Date(subscription.current_period_end * 1000),
};
const stripeInvoice = await stripeClient.invoices.retrieveUpcoming({
subscription: parsedOrg.cloudConfig.stripe.activeSubscriptionId,
});
upcomingInvoice = {
const upcomingInvoice = {
usdAmount: stripeInvoice.amount_due / 100,
date: new Date(stripeInvoice.period_end * 1000),
};
const usage = stripeInvoice.lines.data.reduce((acc, line) => {
if (line.quantity) {
return acc + line.quantity;
}
return acc;
}, 0);
return {
countObservations: usage,
billingPeriod,
upcomingInvoice,
};
}
}
// For non-Stripe subscriptions, we can only get usage from the LangFuse API
const thirtyDaysAgo = new Date();
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
thirtyDaysAgo.setHours(0, 0, 0, 0);
const usage = await ctx.prisma.observation.count({
where: {
project: {
orgId: input.orgId,
},
startTime: {
gte: billingPeriod?.start ?? thirtyDaysAgo,
gte: thirtyDaysAgo,
},
},
});
return {
countObservations: usage,
billingPeriod,
upcomingInvoice,
};
}),
});
@@ -8,7 +8,8 @@ import { prisma } from "@langfuse/shared/src/db";
import { stripeClient } from "@/src/ee/features/billing/utils/stripe";
import type Stripe from "stripe";
import { CloudConfigSchema, parseDbOrg } from "@langfuse/shared";
import { traceException } from "@langfuse/shared/src/server";
import { traceException, redis } from "@langfuse/shared/src/server";
import { ApiAuthService } from "@/src/features/public-api/server/apiAuth";
/*
* Sign-up endpoint (email/password users), creates user in database.
@@ -227,5 +228,8 @@ async function handleSubscriptionChanged(
});
}
// need to update the plan in the api keys
await new ApiAuthService(prisma, redis).invalidateOrgApiKeys(parsedOrg.id);
return;
}
@@ -1,5 +1,5 @@
import { env } from "@/src/env.mjs";
import { type Plan } from "@/src/features/entitlements/constants/plans";
import { type Plan } from "@langfuse/shared";
type StripeProduct = {
stripeProductId: string;
@@ -514,8 +514,11 @@ export const InnerEvalConfigForm = (props: {
<span>0%</span>
<span>100%</span>
</FormDescription>
<FormDescription>
Percentage of traces to evaluate.
<FormDescription className="mt-1 flex flex-row gap-1">
<span>Percentage of traces to evaluate.</span>
<span>
Currently set to {(field.value * 100).toFixed(0)}%.
</span>
</FormDescription>
</div>
<FormMessage />
@@ -30,7 +30,7 @@ export default function EvalConfigTable({ projectId }: { projectId: string }) {
limit: paginationState.pageSize,
projectId,
});
const totalCount = templates.data?.totalCount ?? 0;
const totalCount = templates.data?.totalCount ?? null;
const columnHelper = createColumnHelper<EvalConfigRow>();
const columns = [
@@ -120,7 +120,7 @@ export default function EvalConfigTable({ projectId }: { projectId: string }) {
}
}
pagination={{
pageCount: Math.ceil(totalCount / paginationState.pageSize),
totalCount,
onChange: setPaginationState,
state: paginationState,
}}
@@ -41,7 +41,7 @@ export default function EvalLogTable({
jobConfigurationId,
projectId,
});
const totalCount = logs.data?.totalCount ?? 0;
const totalCount = logs.data?.totalCount ?? null;
const columnHelper = createColumnHelper<JobExecutionRow>();
const columns = [
@@ -199,7 +199,7 @@ export default function EvalLogTable({
}
}
pagination={{
pageCount: Math.ceil(totalCount / paginationState.pageSize),
totalCount,
onChange: setPaginationState,
state: paginationState,
}}
@@ -28,7 +28,7 @@ export default function EvalsTemplateTable({
page: paginationState.pageIndex,
limit: paginationState.pageSize,
});
const totalCount = templates.data?.totalCount ?? 0;
const totalCount = templates.data?.totalCount ?? null;
const columnHelper = createColumnHelper<EvalsTemplateRow>();
@@ -104,7 +104,7 @@ export default function EvalsTemplateTable({
}
}
pagination={{
pageCount: Math.ceil(totalCount / paginationState.pageSize),
totalCount,
onChange: setPaginationState,
state: paginationState,
}}
@@ -427,6 +427,7 @@ export const InnerEvalTemplateForm = (props: {
updateModelParamValue: updateModelParamValue,
setModelParamEnabled,
}}
evalModelsOnly
formDisabled={!props.isEditing}
/>
</div>
@@ -74,9 +74,7 @@ export const GenerationOutput = () => {
<p className="flex-1 text-xs font-semibold">Output</p>
{copyButton}
</div>
<pre
className={`whitespace-break-spaces break-words ${isJson ? "text-xs" : ""}`}
>
<pre className="whitespace-break-spaces break-words text-xs">
{isJson ? outputJson : output}
</pre>
</div>
@@ -32,7 +32,7 @@ type JumpToPlaygroundButtonProps = (
analyticsEventName: "trace_detail:test_in_playground_button_click";
}
) & {
fullWidth?: boolean;
variant?: "outline" | "secondary";
};
export const JumpToPlaygroundButton: React.FC<JumpToPlaygroundButtonProps> = (
@@ -61,17 +61,14 @@ export const JumpToPlaygroundButton: React.FC<JumpToPlaygroundButtonProps> = (
return (
<Button
variant={props.fullWidth ? "secondary" : "outline"}
variant={props.variant ?? "secondary"}
title="Test in LLM playground"
size={!props.fullWidth ? "icon" : undefined}
onClick={handleClick}
asChild
>
<Link href={`/project/${projectId}/playground`}>
<Terminal className="h-5 w-5" />
{props.fullWidth ? (
<span className="ml-2">Test in playground</span>
) : null}
<Terminal className="h-4 w-4" />
<span className="ml-2">Test in playground</span>
</Link>
</Button>
);
@@ -4,13 +4,28 @@ import { usePlaygroundContext } from "@/src/ee/features/playground/page/context"
import { GenerationOutput } from "./GenerationOutput";
import { ChatMessages } from "@/src/components/ChatMessages";
import { type MessagesContext } from "@/src/components/ChatMessages/types";
import {
ResizableHandle,
ResizablePanel,
ResizablePanelGroup,
} from "@/src/components/ui/resizable";
export const Messages: React.FC<MessagesContext> = (props) => {
return (
<div className="flex h-full flex-col space-y-4 pr-4">
<p className="font-semibold">Messages</p>
<ChatMessages {...props} />
<GenerationOutput />
<ResizablePanelGroup direction="vertical">
<ResizablePanel minSize={10}>
<ChatMessages {...props} />
</ResizablePanel>
<ResizableHandle withHandle className="bg-transparent" />
<ResizablePanel
minSize={10}
defaultSize={20}
className="flex flex-col space-y-4"
>
<GenerationOutput />
</ResizablePanel>
</ResizablePanelGroup>
<SubmitButton />
</div>
);
@@ -21,8 +36,7 @@ const SubmitButton = () => {
return (
<Button
variant="default"
className="h-[88px] w-full space-x-2 py-3"
className="flex-0"
onClick={() => {
handleSubmit().catch((err) => console.error(err));
}}
@@ -24,7 +24,7 @@ export const ResetPlaygroundButton: React.FC = () => {
title="Reset playground state"
onClick={handleClick}
>
<ListRestartIcon className="mr-1 h-5 w-5" />
<ListRestartIcon className="mr-1 h-4 w-4" />
<span>Reset playground</span>
</Button>
);
@@ -91,7 +91,7 @@ export const SaveToPromptButton: React.FC = () => {
<PopoverTrigger asChild>
<Button variant={"outline"} title="Save to prompt" asChild>
<Link href={`/project/${projectId}/playground`}>
<FileInput className="mr-1 h-5 w-5" />
<FileInput className="mr-1 h-4 w-4" />
<span>Save as prompt</span>
</Link>
</Button>
@@ -155,7 +155,7 @@ export const SaveToPromptButton: React.FC = () => {
export function Divider() {
return (
<div className="my-6 flex flex-row justify-center align-middle">
<div className="my-3 flex flex-row justify-center align-middle">
<div className="flex flex-1 flex-col">
<div className="flex-1 border-b-2 border-gray-200" />
<div className="flex-1" />
@@ -1,3 +1,4 @@
import { FullScreenPage } from "@/src/components/layouts/full-screen-page";
import Header from "@/src/components/layouts/header";
import { ResetPlaygroundButton } from "@/src/ee/features/playground/page/components/ResetPlaygroundButton";
import { SaveToPromptButton } from "@/src/ee/features/playground/page/components/SaveToPromptButton";
@@ -10,7 +11,7 @@ export default function PlaygroundPage() {
if (!available) return null;
return (
<PlaygroundProvider>
<div className="flex h-[95vh] flex-col">
<FullScreenPage>
<Header
title="Playground"
help={{
@@ -27,7 +28,7 @@ export default function PlaygroundPage() {
<div className="flex-1 overflow-auto">
<Playground />
</div>
</div>
</FullScreenPage>
</PlaygroundProvider>
);
}

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