Compare commits

...
32 Commits
Author SHA1 Message Date
Marc Klingen 63df788908 chore: release v1.34.0
CI/CD / push-docker-image (push) Blocked by required conditions
CI/CD / tests (18) (push) Failing after 47s
CI/CD / tests (20) (push) Failing after 44s
release.yml / release (push) Failing after 2s
CI/CD / e2e-tests (push) Failing after 41s
CI/CD / test-docker-build (push) Successful in 3m14s
CI/CD / lint (push) Successful in 5m59s
CI/CD / all-ci-passed (push) Waiting to run
2024-01-23 22:59:17 +01:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
fb463cbf98 chore(deps): bump the sentry group with 2 updates (#931)
Bumps the sentry group with 2 updates: [@sentry/nextjs](https://github.com/getsentry/sentry-javascript) and [@sentry/profiling-node](https://github.com/getsentry/profiling-node).


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

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

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

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


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

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

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

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

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-23 15:52:08 +01:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
d044ae51d1 chore(deps-dev): bump @typescript-eslint/parser from 6.16.0 to 6.19.1 (#922)
Bumps [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) from 6.16.0 to 6.19.1.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/parser/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v6.19.1/packages/parser)

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

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

---
updated-dependencies:
- dependency-name: posthog-js
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Max Deichmann <m.deichmann@tum.de>
2024-01-22 11:35:24 +01:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Max Deichmann
59edc9851a chore(deps): bump the prisma group with 2 updates (#884)
Bumps the prisma group with 2 updates: [@prisma/client](https://github.com/prisma/prisma/tree/HEAD/packages/client) and [prisma](https://github.com/prisma/prisma/tree/HEAD/packages/cli).


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

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

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Max Deichmann <m.deichmann@tum.de>
2024-01-22 10:17:09 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
0e31dcef99 chore(deps-dev): bump @typescript-eslint/eslint-plugin from 6.17.0 to 6.19.0 (#887)
chore(deps-dev): bump @typescript-eslint/eslint-plugin

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

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-22 11:12:11 +01:00
Max DeichmannandGitHub a67235ab8a feat: support ordering traces on the GET API (#913) 2024-01-19 13:37:31 +00:00
Max Deichmann eb88c9bdf8 chore: release v1.33.8
CI/CD / push-docker-image (push) Blocked by required conditions
CI/CD / tests (18) (push) Failing after 51s
CI/CD / tests (20) (push) Failing after 44s
release.yml / release (push) Failing after 3s
CI/CD / e2e-tests (push) Failing after 48s
CI/CD / lint (push) Successful in 4m17s
CI/CD / test-docker-build (push) Successful in 4m5s
CI/CD / all-ci-passed (push) Waiting to run
2024-01-19 11:58:20 +01:00
Richard KrümmelandGitHub 77e4791cbe feat(ui): add option to keep tracing side menu open (#912) 2024-01-19 11:54:44 +01:00
Max DeichmannandGitHub 5d922dfae7 fix: filter generations metadata correctly (#911) 2024-01-18 15:35:41 +00:00
Max DeichmannandGitHub c704c84451 fix: fix jsonl export to correctly append output (#909) 2024-01-18 13:14:37 +00:00
Max DeichmannandGitHub 6f23488438 fix: sentry reports only unknown errors (#908) 2024-01-18 12:14:54 +00:00
Max Deichmann ca50abb290 chore: release v1.33.7
CI/CD / push-docker-image (push) Blocked by required conditions
CI/CD / tests (18) (push) Failing after 46s
CI/CD / tests (20) (push) Failing after 42s
CI/CD / e2e-tests (push) Failing after 42s
release.yml / release (push) Failing after 2s
CI/CD / test-docker-build (push) Successful in 3m34s
CI/CD / lint (push) Successful in 4m6s
CI/CD / all-ci-passed (push) Waiting to run
2024-01-17 21:18:44 +01:00
Max DeichmannandGitHub 15067114a2 feat: support array of strings in model parameters (#905) 2024-01-17 20:13:12 +00:00
Max DeichmannandGitHub 645f14535b fic: use encoding once (#904) 2024-01-17 16:58:25 +00:00
Max Deichmann 250a5b03d2 chore: release v1.33.6
CI/CD / push-docker-image (push) Blocked by required conditions
CI/CD / tests (18) (push) Failing after 58s
CI/CD / tests (20) (push) Failing after 1m7s
release.yml / release (push) Failing after 3s
CI/CD / test-docker-build (push) Successful in 3m6s
CI/CD / e2e-tests (push) Failing after 48s
CI/CD / lint (push) Successful in 4m1s
CI/CD / all-ci-passed (push) Waiting to run
2024-01-17 16:25:56 +01:00
Max DeichmannandGitHub 5ee060dfc6 refactor: remove external id uniqueness constraint (#903) 2024-01-17 15:24:31 +00:00
Max DeichmannandGitHub f08502cde4 fix: upsert traces on ingestion only on the id (#900) 2024-01-17 13:55:32 +00:00
Max Deichmann 90ecb88249 chore: release v1.33.5
CI/CD / push-docker-image (push) Blocked by required conditions
CI/CD / tests (18) (push) Failing after 48s
CI/CD / tests (20) (push) Failing after 44s
release.yml / release (push) Failing after 2s
CI/CD / e2e-tests (push) Failing after 44s
CI/CD / test-docker-build (push) Successful in 2m47s
CI/CD / lint (push) Successful in 4m15s
CI/CD / all-ci-passed (push) Waiting to run
2024-01-17 14:05:47 +01:00
Max DeichmannandGitHub 16e60f74da fix: capture ingestion errors in sentry (#899) 2024-01-17 12:52:56 +00:00
27 changed files with 937 additions and 662 deletions
+1
View File
@@ -129,6 +129,7 @@ types:
- optional<string>
- optional<integer>
- optional<boolean>
- optional<list<string>>
DatasetStatus:
enum:
- ACTIVE
+6
View File
@@ -26,6 +26,9 @@ service:
limit: optional<integer>
userId: optional<string>
name: optional<string>
orderBy:
type: string
docs: Format of the string sort_by=timestamp.asc (id, timestamp, name, userId, release, version, public, bookmarked, sessionId)
tags:
type: optional<string>
allow-multiple: true
@@ -37,3 +40,6 @@ types:
properties:
data: list<commons.TraceWithDetails>
meta: pagination.MetaResponse
Sort:
properties:
id: string
+20
View File
@@ -884,6 +884,14 @@ paths:
schema:
type: string
nullable: true
- name: orderBy
in: query
description: >-
Format of the string sort_by=timestamp.asc (id, timestamp, name,
userId, release, version, public, bookmarked, sessionId)
required: true
schema:
type: string
- name: tags
in: query
description: Only traces that include all of these tags will be returned.
@@ -1275,6 +1283,10 @@ components:
nullable: true
- type: boolean
nullable: true
- type: array
items:
type: string
nullable: true
DatasetStatus:
title: DatasetStatus
type: string
@@ -1988,6 +2000,14 @@ components:
required:
- data
- meta
Sort:
title: Sort
type: object
properties:
id:
type: string
required:
- id
utilsMetaResponse:
title: utilsMetaResponse
type: object
+6 -1
View File
@@ -708,7 +708,7 @@
"request": {
"description": "Get list of traces",
"url": {
"raw": "{{baseUrl}}/api/public/traces?page=&limit=&userId=&name=&tags=",
"raw": "{{baseUrl}}/api/public/traces?page=&limit=&userId=&name=&orderBy=&tags=",
"host": [
"{{baseUrl}}"
],
@@ -738,6 +738,11 @@
"value": "",
"description": null
},
{
"key": "orderBy",
"value": "",
"description": "Format of the string sort_by=timestamp.asc (id, timestamp, name, userId, release, version, public, bookmarked, sessionId)"
},
{
"key": "tags",
"value": "",
+342 -309
View File
File diff suppressed because it is too large Load Diff
+11 -11
View File
@@ -1,6 +1,6 @@
{
"name": "langfuse-core",
"version": "1.33.3",
"version": "1.34.0",
"private": true,
"scripts": {
"prebuild": "cp generated/openapi-client/openapi.yml public/openapi-client.yml && cp generated/openapi-server/openapi.yml public/openapi-server.yml",
@@ -33,7 +33,7 @@
"@heroicons/react": "^2.1.1",
"@hookform/resolvers": "^3.3.4",
"@next-auth/prisma-adapter": "^1.0.7",
"@prisma/client": "^5.7.1",
"@prisma/client": "^5.8.1",
"@radix-ui/react-accordion": "^1.1.2",
"@radix-ui/react-alert-dialog": "^1.0.5",
"@radix-ui/react-avatar": "^1.0.4",
@@ -56,8 +56,8 @@
"@radix-ui/react-tooltip": "^1.0.7",
"@react-email/components": "^0.0.12",
"@react-email/render": "^0.0.10",
"@sentry/nextjs": "^7.92.0",
"@sentry/profiling-node": "^1.3.2",
"@sentry/nextjs": "^7.95.0",
"@sentry/profiling-node": "^1.3.5",
"@sentry/types": "^7.88.0",
"@t3-oss/env-nextjs": "^0.7.1",
"@tailwindcss/forms": "^0.5.7",
@@ -75,16 +75,16 @@
"cmdk": "^0.2.0",
"core-js": "^3.35.0",
"cors": "^2.8.5",
"date-fns": "^3.1.0",
"date-fns": "^3.3.1",
"decimal.js": "^10.4.3",
"exponential-backoff": "^3.1.1",
"lodash": "^4.17.21",
"lucide-react": "^0.306.0",
"next": "^14.0.4",
"next": "^14.1.0",
"next-auth": "^4.24.5",
"next-query-params": "^5.0.0",
"nodemailer": "^6.9.8",
"posthog-js": "^1.96.1",
"posthog-js": "^1.100.0",
"posthog-node": "^3.2.1",
"react": "18.2.0",
"react-day-picker": "^8.10.0",
@@ -118,18 +118,18 @@
"@types/react": "^18.2.46",
"@types/react-dom": "^18.2.18",
"@types/uuid": "^9.0.7",
"@typescript-eslint/eslint-plugin": "^6.17.0",
"@typescript-eslint/parser": "^6.16.0",
"@typescript-eslint/eslint-plugin": "^6.19.0",
"@typescript-eslint/parser": "^6.19.1",
"autoprefixer": "^10.4.16",
"dotenv-cli": "^7.3.0",
"eslint": "^8.56.0",
"eslint-config-next": "^14.0.4",
"eslint-config-next": "^14.1.0",
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
"postcss": "^8.4.33",
"prettier": "^3.1.1",
"prettier-plugin-tailwindcss": "^0.5.11",
"prisma": "^5.7.1",
"prisma": "^5.8.1",
"prisma-erd-generator": "^1.11.2",
"release-it": "^17.0.1",
"tailwindcss": "^3.4.1",
@@ -0,0 +1,2 @@
-- DropIndex
DROP INDEX "traces_project_id_external_id_key";
-1
View File
@@ -198,7 +198,6 @@ model Trace {
scores Score[]
@@unique([projectId, externalId])
@@index([projectId])
@@index([sessionId])
@@index([name])
+64
View File
@@ -300,6 +300,7 @@ describe("/api/public/ingestion API Endpoint", () => {
id: generationId,
traceId: traceId,
parentObservationId: spanId,
modelParameters: { someKey: ["user-1", "user-2"] },
},
},
{
@@ -376,6 +377,9 @@ describe("/api/public/ingestion API Endpoint", () => {
expect(dbGeneration?.traceId).toBe(traceId);
expect(dbGeneration?.name).toBe("generation-name");
expect(dbGeneration?.parentObservationId).toBe(spanId);
expect(dbGeneration?.modelParameters).toEqual({
someKey: ["user-1", "user-2"],
});
const dbEvent = await prisma.observation.findUnique({
where: {
@@ -790,6 +794,66 @@ describe("/api/public/ingestion API Endpoint", () => {
expect(dbTrace[0]?.version).toBe("2.0.0");
});
it("should not override a trace from a different project", async () => {
const traceId = v4();
const newProjectId = v4();
await prisma.project.create({
data: {
id: newProjectId,
name: "another-project",
},
});
await prisma.trace.create({
data: {
id: traceId,
project: { connect: { id: newProjectId } },
},
});
const responseOne = await makeAPICall("POST", "/api/public/ingestion", {
batch: [
{
id: v4(),
type: "trace-create",
timestamp: new Date().toISOString(),
body: {
id: traceId,
name: "trace-name",
userId: "user-1",
metadata: { key: "value" },
release: "1.0.0",
version: "2.0.0",
},
},
],
});
expect(responseOne.status).toBe(207);
console.log(responseOne.body);
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
const errors = responseOne.body.errors;
expect(errors).toBeDefined();
console.log(errors);
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
expect(errors.length).toBe(1);
const dbTrace = await prisma.trace.findMany({
where: {
id: traceId,
},
});
expect(dbTrace.length).toEqual(1);
expect(dbTrace[0]?.name).toBeNull();
expect(dbTrace[0]?.release).toBeNull();
expect(dbTrace[0]?.metadata).toBeNull();
expect(dbTrace[0]?.version).toBeNull();
});
[
{
inputs: [{ a: "a" }, { b: "b" }],
+143 -113
View File
@@ -141,7 +141,6 @@ export default function Layout(props: PropsWithChildren) {
{props.children}
</main>
);
return (
<>
<Head>
@@ -505,120 +504,151 @@ type NestedNavigationItem = Omit<Route, "children"> & {
const MainNavigation: React.FC<{
nav: NavigationItem[];
onNavitemClick?: () => void;
}> = ({ nav, onNavitemClick }) => (
<li>
<ul role="list" className="-mx-2 space-y-1">
{nav.map((item) => (
<li key={item.name}>
{(!item.children || item.children.length === 0) && item.href ? (
<Link
href={item.href}
className={clsx(
item.current
? "bg-gray-50 text-indigo-600"
: "text-gray-700 hover:bg-gray-50 hover:text-indigo-600",
"group flex gap-x-3 rounded-md p-2 text-sm font-semibold leading-6",
)}
onClick={onNavitemClick}
>
{item.icon && (
<item.icon
className={clsx(
item.current
? "text-indigo-600"
: "text-gray-400 group-hover:text-indigo-600",
"h-6 w-6 shrink-0",
)}
aria-hidden="true"
/>
)}
{item.name}
{item.label && (
<span
className={cn(
"self-center whitespace-nowrap break-keep rounded-sm border px-1 py-0.5 text-xs",
item.current
? "border-indigo-600 text-indigo-600"
: "border-gray-200 text-gray-400 group-hover:border-indigo-600 group-hover:text-indigo-600",
)}
>
{item.label}
</span>
)}
</Link>
) : item.children && item.children.length > 0 ? (
<Disclosure
as="div"
defaultOpen={item.children.some((child) => child.current)}
>
{({ open }) => (
<>
<Disclosure.Button className="group flex w-full items-center gap-x-3 rounded-md p-2 text-left text-sm font-semibold leading-6 hover:bg-gray-50 hover:text-indigo-600">
{item.icon && (
<item.icon
className="h-6 w-6 shrink-0 text-gray-400 group-hover:text-indigo-600"
aria-hidden="true"
/>
}> = ({ nav, onNavitemClick }) => {
const STORAGE_KEY = "sidebar-tracing-default-open";
const getDefaultOpen = () => {
const savedState = localStorage.getItem(STORAGE_KEY);
if (savedState !== null) {
try {
return JSON.parse(savedState) as boolean;
} catch (e) {
console.error("Error parsing saved state: ", e);
}
}
return false;
};
const handleDropDownClick = () => {
const savedState = localStorage.getItem(STORAGE_KEY);
const isOpen =
savedState !== null ? (JSON.parse(savedState) as boolean) : false;
const newState = !isOpen;
localStorage.setItem(
"sidebar-tracing-default-open",
JSON.stringify(newState),
);
};
return (
<li>
<ul role="list" className="-mx-2 space-y-1">
{nav.map((item) => (
<li key={item.name}>
{(!item.children || item.children.length === 0) && item.href ? (
<Link
href={item.href}
className={clsx(
item.current
? "bg-gray-50 text-indigo-600"
: "text-gray-700 hover:bg-gray-50 hover:text-indigo-600",
"group flex gap-x-3 rounded-md p-2 text-sm font-semibold leading-6",
)}
onClick={onNavitemClick}
>
{item.icon && (
<item.icon
className={clsx(
item.current
? "text-indigo-600"
: "text-gray-400 group-hover:text-indigo-600",
"h-6 w-6 shrink-0",
)}
{item.name}
{item.label && (
<span
className={cn(
"self-center whitespace-nowrap break-keep rounded-sm border px-1 py-0.5 text-xs",
item.current
? "border-indigo-600 text-indigo-600"
: "border-gray-200 text-gray-400 group-hover:border-indigo-600 group-hover:text-indigo-600",
)}
>
{item.label}
</span>
aria-hidden="true"
/>
)}
{item.name}
{item.label && (
<span
className={cn(
"self-center whitespace-nowrap break-keep rounded-sm border px-1 py-0.5 text-xs",
item.current
? "border-indigo-600 text-indigo-600"
: "border-gray-200 text-gray-400 group-hover:border-indigo-600 group-hover:text-indigo-600",
)}
<ChevronRightIcon
className={clsx(
open ? "rotate-90 text-gray-500" : "text-gray-400",
"ml-auto h-5 w-5 shrink-0",
>
{item.label}
</span>
)}
</Link>
) : item.children && item.children.length > 0 ? (
<Disclosure
as="div"
defaultOpen={
item.children.some((child) => child.current) ||
getDefaultOpen()
}
>
{({ open }) => (
<>
<Disclosure.Button
className="group flex w-full items-center gap-x-3 rounded-md p-2 text-left text-sm font-semibold leading-6 hover:bg-gray-50 hover:text-indigo-600"
onClick={handleDropDownClick}
>
{item.icon && (
<item.icon
className="h-6 w-6 shrink-0 text-gray-400 group-hover:text-indigo-600"
aria-hidden="true"
/>
)}
aria-hidden="true"
/>
</Disclosure.Button>
<Disclosure.Panel as="ul" className="mt-1 px-2">
{item.children?.map((subItem) => (
<li key={subItem.name}>
{/* 44px */}
<Link
href={subItem.href ?? "#"}
className={clsx(
subItem.current
? "bg-gray-50 text-indigo-600"
: "text-gray-700 hover:bg-gray-50 hover:text-indigo-600",
"flex w-full items-center gap-x-3 rounded-md py-2 pl-9 pr-2 text-sm leading-6",
{item.name}
{item.label && (
<span
className={cn(
"self-center whitespace-nowrap break-keep rounded-sm border px-1 py-0.5 text-xs",
item.current
? "border-indigo-600 text-indigo-600"
: "border-gray-200 text-gray-400 group-hover:border-indigo-600 group-hover:text-indigo-600",
)}
>
{subItem.name}
{subItem.label && (
<span className="self-center whitespace-nowrap break-keep rounded-sm border border-gray-200 px-1 py-0.5 text-xs text-gray-400 group-hover:border-indigo-600 group-hover:text-indigo-600">
{subItem.label}
</span>
)}
</Link>
</li>
))}
</Disclosure.Panel>
</>
)}
</Disclosure>
) : null}
</li>
))}
<FeedbackButtonWrapper className="w-full">
<li className="group flex cursor-pointer gap-x-3 rounded-md p-2 text-sm font-semibold leading-6 text-gray-700 hover:bg-gray-50 hover:text-indigo-600">
<MessageSquarePlus
className="h-6 w-6 shrink-0 text-gray-400 group-hover:text-indigo-600"
aria-hidden="true"
/>
Feedback
</li>
</FeedbackButtonWrapper>
</ul>
</li>
);
{item.label}
</span>
)}
<ChevronRightIcon
className={clsx(
open ? "rotate-90 text-gray-500" : "text-gray-400",
"ml-auto h-5 w-5 shrink-0",
)}
aria-hidden="true"
/>
</Disclosure.Button>
<Disclosure.Panel as="ul" className="mt-1 px-2">
{item.children?.map((subItem) => (
<li key={subItem.name}>
{/* 44px */}
<Link
href={subItem.href ?? "#"}
className={clsx(
subItem.current
? "bg-gray-50 text-indigo-600"
: "text-gray-700 hover:bg-gray-50 hover:text-indigo-600",
"flex w-full items-center gap-x-3 rounded-md py-2 pl-9 pr-2 text-sm leading-6",
)}
>
{subItem.name}
{subItem.label && (
<span className="self-center whitespace-nowrap break-keep rounded-sm border border-gray-200 px-1 py-0.5 text-xs text-gray-400 group-hover:border-indigo-600 group-hover:text-indigo-600">
{subItem.label}
</span>
)}
</Link>
</li>
))}
</Disclosure.Panel>
</>
)}
</Disclosure>
) : null}
</li>
))}
<FeedbackButtonWrapper className="w-full">
<li className="group flex cursor-pointer gap-x-3 rounded-md p-2 text-sm font-semibold leading-6 text-gray-700 hover:bg-gray-50 hover:text-indigo-600">
<MessageSquarePlus
className="h-6 w-6 shrink-0 text-gray-400 group-hover:text-indigo-600"
aria-hidden="true"
/>
Feedback
</li>
</FeedbackButtonWrapper>
</ul>
</li>
);
};
+35 -30
View File
@@ -34,6 +34,7 @@ import {
exportOptions,
type ExportFileFormats,
} from "@/src/server/api/interfaces/exportTypes";
import { useOrderByState } from "@/src/features/orderBy/hooks/useOrderByState";
export type GenerationsTableRow = {
id: string;
@@ -82,37 +83,21 @@ export default function GenerationsTable({ projectId }: GenerationsTableProps) {
},
]);
const generationsQueries = api.useQueries((t) => [
t.generations.all({
page: paginationState.pageIndex,
limit: paginationState.pageSize / 2,
projectId,
filter: filterState,
searchQuery,
}),
t.generations.all({
page: paginationState.pageIndex + paginationState.pageSize / 2,
limit: paginationState.pageSize / 2,
projectId,
filter: filterState,
searchQuery,
}),
]);
const [orderByState, setOrderByState] = useOrderByState({
column: "startTime",
order: "DESC",
});
const generations = {
isLoading:
generationsQueries[0].isLoading || generationsQueries[1].isLoading,
isError: generationsQueries[0].isError || generationsQueries[1].isError,
isSuccess:
generationsQueries[0].isSuccess || generationsQueries[1].isSuccess,
data: [
...(generationsQueries[0].data?.generations ?? []),
...(generationsQueries[1].data?.generations ?? []),
],
error: generationsQueries[0].error ?? generationsQueries[1].error,
};
const generations = api.generations.all.useQuery({
page: paginationState.pageIndex,
limit: paginationState.pageSize,
projectId,
filter: filterState,
orderBy: orderByState,
searchQuery,
});
const totalCount = generationsQueries[0].data?.totalCount ?? 0;
const totalCount = generations.data?.totalCount ?? 0;
const filterOptions = api.generations.filterOptions.useQuery({
projectId,
@@ -166,6 +151,7 @@ export default function GenerationsTable({ projectId }: GenerationsTableProps) {
const columns: LangfuseColumnDef<GenerationsTableRow>[] = [
{
accessorKey: "id",
id: "id",
header: "ID",
cell: ({ row }) => {
const observationId = row.getValue("id");
@@ -178,13 +164,17 @@ export default function GenerationsTable({ projectId }: GenerationsTableProps) {
/>
) : null;
},
enableSorting: true,
},
{
accessorKey: "name",
id: "name",
header: "name",
enableSorting: true,
},
{
accessorKey: "traceId",
id: "traceId",
header: "Trace ID",
cell: ({ row }) => {
const value = row.getValue("traceId");
@@ -195,19 +185,25 @@ export default function GenerationsTable({ projectId }: GenerationsTableProps) {
/>
) : undefined;
},
enableSorting: true,
},
{
accessorKey: "traceName",
id: "traceName",
header: "Trace Name",
enableHiding: true,
enableSorting: true,
},
{
accessorKey: "startTime",
id: "startTime",
header: "Start Time",
enableHiding: true,
enableSorting: true,
},
{
accessorKey: "latency",
id: "latency",
header: "Latency",
cell: ({ row }) => {
const value: number | undefined = row.getValue("latency");
@@ -216,6 +212,7 @@ export default function GenerationsTable({ projectId }: GenerationsTableProps) {
) : undefined;
},
enableHiding: true,
enableSorting: true,
},
{
accessorKey: "cost",
@@ -231,6 +228,7 @@ export default function GenerationsTable({ projectId }: GenerationsTableProps) {
},
{
accessorKey: "level",
id: "level",
header: "Level",
enableHiding: true,
cell({ row }) {
@@ -247,6 +245,7 @@ export default function GenerationsTable({ projectId }: GenerationsTableProps) {
</span>
) : undefined;
},
enableSorting: true,
},
{
accessorKey: "statusMessage",
@@ -256,8 +255,10 @@ export default function GenerationsTable({ projectId }: GenerationsTableProps) {
},
{
accessorKey: "model",
id: "model",
header: "Model",
enableHiding: true,
enableSorting: true,
},
{
accessorKey: "usage",
@@ -311,8 +312,10 @@ export default function GenerationsTable({ projectId }: GenerationsTableProps) {
},
{
accessorKey: "version",
id: "version",
header: "Version",
enableHiding: true,
enableSorting: true,
},
];
const [columnVisibility, setColumnVisibility] =
@@ -322,7 +325,7 @@ export default function GenerationsTable({ projectId }: GenerationsTableProps) {
);
const rows: GenerationsTableRow[] = generations.isSuccess
? generations.data.map((generation) => {
? generations.data.generations.map((generation) => {
return {
id: generation.id,
traceId: generation.traceId,
@@ -420,6 +423,8 @@ export default function GenerationsTable({ projectId }: GenerationsTableProps) {
onChange: setPaginationState,
state: paginationState,
}}
setOrderBy={setOrderByState}
orderBy={orderByState}
columnVisibility={columnVisibility}
onColumnVisibilityChange={setColumnVisibility}
/>
@@ -7,6 +7,7 @@ import useColumnVisibility from "@/src/features/column-visibility/hooks/useColum
import { useQueryFilterState } from "@/src/features/filters/hooks/useFilterState";
import { type FilterState } from "@/src/features/filters/types";
import { useDetailPageLists } from "@/src/features/navigate-detail-pages/context";
import { useOrderByState } from "@/src/features/orderBy/hooks/useOrderByState";
import { sessionsViewCols } from "@/src/server/api/definitions/sessionsView";
import { api } from "@/src/utils/api";
import { formatInterval, utcDateOffsetByDays } from "@/src/utils/dates";
@@ -63,11 +64,17 @@ export default function SessionsTable({
pageSize: withDefault(NumberParam, 50),
});
const [orderByState, setOrderByState] = useOrderByState({
column: "createdAt",
order: "DESC",
});
const sessions = api.sessions.all.useQuery({
page: paginationState.pageIndex,
limit: paginationState.pageSize,
projectId,
filter: filterState,
orderBy: orderByState,
});
const totalCount = sessions.data?.slice(1)[0]?.totalCount ?? 0;
@@ -97,6 +104,7 @@ export default function SessionsTable({
const columns: LangfuseColumnDef<SessionTableRow>[] = [
{
accessorKey: "bookmarked",
id: "bookmarked",
header: undefined,
cell: ({ row }) => {
const bookmarked = row.getValue("bookmarked");
@@ -112,9 +120,11 @@ export default function SessionsTable({
/>
) : undefined;
},
enableSorting: true,
},
{
accessorKey: "id",
id: "id",
header: "ID",
cell: ({ row }) => {
const value = row.getValue("id");
@@ -125,14 +135,18 @@ export default function SessionsTable({
/>
) : undefined;
},
enableSorting: true,
},
{
accessorKey: "createdAt",
id: "createdAt",
header: "Created At",
enableHiding: true,
enableSorting: true,
},
{
accessorKey: "sessionDuration",
id: "sessionDuration",
header: "Duration",
enableHiding: true,
cell: ({ row }) => {
@@ -141,6 +155,7 @@ export default function SessionsTable({
? formatInterval(value)
: undefined;
},
enableSorting: true,
},
{
accessorKey: "userIds",
@@ -165,8 +180,10 @@ export default function SessionsTable({
},
{
accessorKey: "countTraces",
id: "countTraces",
header: "Traces",
enableHiding: true,
enableSorting: true,
},
];
@@ -205,6 +222,8 @@ export default function SessionsTable({
onChange: setPaginationState,
state: paginationState,
}}
setOrderBy={setOrderByState}
orderBy={orderByState}
columnVisibility={columnVisibility}
onColumnVisibilityChange={setColumnVisibility}
help={{
+1 -1
View File
@@ -1 +1 @@
export const VERSION = "v1.33.3";
export const VERSION = "v1.34.0";
+19 -11
View File
@@ -57,13 +57,6 @@ export function tokenCount(p: {
}
function openAiChatTokenCount(params: TokenCalculationParams) {
let encoding: Tiktoken;
try {
encoding = encoding_for_model(params.model);
} catch (KeyError) {
console.log("Warning: model not found. Using cl100k_base encoding.");
encoding = get_encoding("cl100k_base");
}
let tokens_per_message = 0;
let tokens_per_name = 0;
@@ -100,7 +93,7 @@ function openAiChatTokenCount(params: TokenCalculationParams) {
Object.keys(message).forEach((key) => {
const value = message[key as keyof typeof message];
if (value) {
num_tokens += encoding.encode(value).length;
num_tokens += getTokensByModel(params.model, value);
}
if (key === "name") {
num_tokens += tokens_per_name;
@@ -108,9 +101,7 @@ function openAiChatTokenCount(params: TokenCalculationParams) {
});
});
num_tokens += 3; // every reply is primed with <| start |> assistant <| message |>
// https://github.com/dqbd/tiktoken/issues/72
// we need to ensure to deallocate memory from the encoder
encoding.free();
return num_tokens;
}
@@ -141,6 +132,23 @@ const getTokens = (name: TiktokenEncoding, text: string) => {
return tokens.length;
};
const getTokensByModel = (model: TiktokenModel, text: string) => {
let encoding: Tiktoken;
try {
encoding = encoding_for_model(model);
} catch (KeyError) {
console.log("Warning: model not found. Using cl100k_base encoding.");
encoding = get_encoding("cl100k_base");
}
const length = encoding.encode(text).length;
// https://github.com/dqbd/tiktoken/issues/72
// we need to ensure to deallocate memory from the encoder
encoding.free();
return length;
};
function isString(value: unknown): value is string {
return typeof value === "string";
}
+74 -83
View File
@@ -5,7 +5,6 @@ import {
} from "@/src/features/public-api/lib/apiKeys";
import { type ApiAccessScope } from "@/src/features/public-api/server/types";
import { prisma } from "@/src/server/db";
import { instrumentAsync } from "@/src/utils/instrumentation";
export type AuthHeaderVerificationResult =
| {
@@ -20,93 +19,85 @@ export type AuthHeaderVerificationResult =
export async function verifyAuthHeaderAndReturnScope(
authHeader: string | undefined,
): Promise<AuthHeaderVerificationResult> {
return instrumentAsync(
{ name: "verifyAuthHeaderAndReturnScope" },
async () => {
if (!authHeader) {
console.error("No authorization header");
return {
validKey: false,
error: "No authorization header",
};
if (!authHeader) {
console.error("No authorization header");
return {
validKey: false,
error: "No authorization header",
};
}
try {
// Basic auth, full scope, needs secret key and public key
if (authHeader.startsWith("Basic ")) {
const { username: publicKey, password: secretKey } =
extractBasicAuthCredentials(authHeader);
const salt = env.SALT;
const hashFromProvidedKey = createShaHash(secretKey, salt);
const apiKey = await prisma.apiKey.findUnique({
where: { fastHashedSecretKey: hashFromProvidedKey },
});
let projectId = apiKey?.projectId;
if (!apiKey || !apiKey.fastHashedSecretKey) {
const dbKey = await findDbKeyOrThrow(publicKey);
const isValid = await verifySecretKey(secretKey, dbKey.hashedSecretKey);
if (!isValid) {
console.log("Old key is invalid", publicKey);
throw new Error("Invalid credentials");
}
const shaKey = createShaHash(secretKey, salt);
await prisma.apiKey.update({
where: { publicKey },
data: {
fastHashedSecretKey: shaKey,
},
});
projectId = dbKey.projectId;
}
try {
// Basic auth, full scope, needs secret key and public key
if (authHeader.startsWith("Basic ")) {
const { username: publicKey, password: secretKey } =
extractBasicAuthCredentials(authHeader);
const salt = env.SALT;
const hashFromProvidedKey = createShaHash(secretKey, salt);
const apiKey = await prisma.apiKey.findUnique({
where: { fastHashedSecretKey: hashFromProvidedKey },
});
let projectId = apiKey?.projectId;
if (!apiKey || !apiKey.fastHashedSecretKey) {
const dbKey = await findDbKeyOrThrow(publicKey);
const isValid = await verifySecretKey(
secretKey,
dbKey.hashedSecretKey,
);
if (!isValid) {
console.log("Old key is invalid", publicKey);
throw new Error("Invalid credentials");
}
const shaKey = createShaHash(secretKey, salt);
await prisma.apiKey.update({
where: { publicKey },
data: {
fastHashedSecretKey: shaKey,
},
});
projectId = dbKey.projectId;
}
if (!projectId) {
console.log("No project id found for key", publicKey);
throw new Error("Invalid credentials");
}
return {
validKey: true,
scope: {
projectId: projectId,
accessLevel: "all",
},
};
}
// Bearer auth, limited scope, only needs public key
if (authHeader.startsWith("Bearer ")) {
const publicKey = authHeader.replace("Bearer ", "");
const dbKey = await findDbKeyOrThrow(publicKey);
return {
validKey: true,
scope: {
projectId: dbKey.projectId,
accessLevel: "scores",
},
};
}
} catch (error: unknown) {
console.error("Error verifying auth header: ", error);
return {
validKey: false,
error: error instanceof Error ? error.message : "Authorization error",
};
if (!projectId) {
console.log("No project id found for key", publicKey);
throw new Error("Invalid credentials");
}
return {
validKey: false,
error: "Invalid authorization header",
validKey: true,
scope: {
projectId: projectId,
accessLevel: "all",
},
};
},
);
}
// Bearer auth, limited scope, only needs public key
if (authHeader.startsWith("Bearer ")) {
const publicKey = authHeader.replace("Bearer ", "");
const dbKey = await findDbKeyOrThrow(publicKey);
return {
validKey: true,
scope: {
projectId: dbKey.projectId,
accessLevel: "scores",
},
};
}
} catch (error: unknown) {
console.error("Error verifying auth header: ", error);
return {
validKey: false,
error: error instanceof Error ? error.message : "Authorization error",
};
}
return {
validKey: false,
error: "Invalid authorization header",
};
}
function extractBasicAuthCredentials(basicAuthHeader: string): {
@@ -96,7 +96,9 @@ export const CreateGenerationBody = CreateSpanBody.extend({
modelParameters: z
.record(
z.string(),
z.union([z.string(), z.number(), z.boolean()]).nullish(),
z
.union([z.string(), z.number(), z.boolean(), z.array(z.string())])
.nullish(),
)
.nullish(),
usage: usage,
@@ -116,7 +118,9 @@ export const UpdateGenerationBody = UpdateSpanBody.extend({
modelParameters: z
.record(
z.string(),
z.union([z.string(), z.number(), z.boolean()]).nullish(),
z
.union([z.string(), z.number(), z.boolean(), z.array(z.string())])
.nullish(),
)
.nullish(),
usage: usage,
@@ -1,5 +1,4 @@
import React from "react";
import { cn } from "@/src/utils/tailwind";
import { CommandItem } from "cmdk";
type TagItemCreateProps = {
@@ -25,10 +24,9 @@ const TagItemCreate = ({
<CommandItem
key={inputValue}
value={inputValue}
className="flex cursor-pointer items-center rounded-sm px-1 py-2 text-muted-foreground hover:bg-secondary/80"
className="flex cursor-pointer items-center rounded-sm px-3 py-2 text-sm text-muted-foreground hover:bg-secondary/80"
onSelect={onSelect}
>
<div className={cn("mr-2 h-4 w-4")} />
Create new tag: &quot;{inputValue}&quot;
</CommandItem>
);
+20 -17
View File
@@ -16,31 +16,34 @@ export const TagInput = React.forwardRef<
TagInputProps
>(({ className, selectedTags, setSelectedTags, ...props }, ref) => (
<div
className="flex flex-wrap items-center overflow-auto rounded-lg border px-2 pt-2"
className="flex flex-wrap items-center overflow-auto rounded-lg border px-2"
cmdk-input-wrapper=""
>
<div className="flex flex-wrap items-center gap-x-2 gap-y-1">
{selectedTags.map((tag: string) => (
<Button
key={tag}
variant="secondary"
size="xs"
onClick={() => {
const newTags = selectedTags.filter((t) => t !== tag);
setSelectedTags(newTags);
}}
>
{tag}
<X className="ml-1 h-3 w-3" />
</Button>
))}
</div>
{selectedTags.length > 0 && (
<div className="flex flex-wrap items-center gap-x-2 gap-y-1 pt-2">
{selectedTags.map((tag: string) => (
<Button
key={tag}
variant="secondary"
size="xs"
onClick={() => {
const newTags = selectedTags.filter((t) => t !== tag);
setSelectedTags(newTags);
}}
>
{tag}
<X className="ml-1 h-3 w-3" />
</Button>
))}
</div>
)}
<CommandPrimitive.Input
ref={ref}
className={cn(
"flex h-11 w-full rounded-md border-transparent bg-transparent px-1 text-sm outline-none placeholder:text-slate-500 focus:border-0 focus:border-none focus:border-transparent focus:ring-0 disabled:cursor-not-allowed disabled:opacity-50 dark:placeholder:text-slate-400 ",
className,
)}
autoFocus
{...props}
/>
</div>
+1 -1
View File
@@ -64,7 +64,7 @@ const TagManager = ({
className={availableTags.length > 0 ? "mt-2" : undefined}
>
<CommandGroup>
{availableTags.map((value: string) => (
{availableTags.slice(0, 5).map((value: string) => (
<TagCommandItem
key={value}
value={value}
+4
View File
@@ -25,6 +25,7 @@ import { ScoreProcessor } from "../../../server/api/services/EventProcessor";
import { isNotNullOrUndefined } from "@/src/utils/types";
import { telemetry } from "@/src/features/telemetry";
import { jsonSchema } from "@/src/utils/zod";
import * as Sentry from "@sentry/nextjs";
export const config = {
api: {
@@ -306,6 +307,9 @@ export const handleBatchResult = (
error: error.error.message,
});
} else {
if (process.env.NEXT_PUBLIC_SENTRY_DSN) {
Sentry.captureException(error.error);
}
returnedErrors.push({
id: error.id,
status: 500,
+18 -1
View File
@@ -15,12 +15,24 @@ import {
} from "@/src/features/public-api/server/ingestion-api-schema";
import { v4 } from "uuid";
import { telemetry } from "@/src/features/telemetry";
import { orderByToPrismaSql } from "@/src/features/orderBy/server/orderByToPrisma";
import { tracesTableCols } from "@/src/server/api/definitions/tracesTable";
import { orderBy } from "@/src/server/api/interfaces/orderBy";
const GetTracesSchema = z.object({
...paginationZod,
userId: z.string().nullish(),
name: z.string().nullish(),
tags: z.union([z.array(z.string()), z.string()]).nullish(),
orderBy: z
.string() // orderBy=timestamp.asc
.nullish()
.transform((v) => {
if (!v) return null;
const [column, order] = v.split(".");
return { column, order: order?.toUpperCase() };
})
.pipe(orderBy.nullish()),
});
export default async function handler(
@@ -92,6 +104,11 @@ export default async function handler(
)}] <@ t."tags"`
: Prisma.empty;
const orderByCondition = orderByToPrismaSql(
obj.orderBy ?? null,
tracesTableCols,
);
const traces = await prisma.$queryRaw<
Array<Trace & { observations: string[]; scores: string[] }>
>(Prisma.sql`
@@ -116,7 +133,7 @@ export default async function handler(
${nameCondition}
${tagsCondition}
GROUP BY t.id
ORDER BY t."timestamp" DESC
${orderByCondition}
LIMIT ${obj.limit} OFFSET ${skipValue}
`);
const totalItems = await prisma.trace.count({
@@ -8,62 +8,73 @@ import { ObservationLevel } from "@prisma/client";
export const observationsTableCols: ColumnDefinition[] = [
{
name: "id",
id: "id",
type: "string",
internal: 'o."id"',
},
{
name: "name",
id: "name",
type: "stringOptions",
internal: 'o."name"',
options: [], // to be added at runtime
},
{ name: "traceId", type: "string", internal: 't."id"' },
{ name: "traceId", id: "traceId", type: "string", internal: 't."id"' },
{
name: "traceName",
id: "traceName",
type: "stringOptions",
internal: 't."name"',
options: [], // to be added at runtime
},
{ name: "userId", type: "string", internal: 't."user_id"' },
{ name: "userId", id: "userId", type: "string", internal: 't."user_id"' },
{
name: "start_time",
id: "startTime",
type: "datetime",
internal: 'o."start_time"',
},
{
name: "end_time",
id: "endTime",
type: "datetime",
internal: 'o."end_time"',
},
{
name: "latency (s)",
id: "latency",
type: "number",
internal: '"latency"',
},
{
name: "level",
id: "level",
type: "stringOptions",
internal: 'o."level"::text',
options: Object.values(ObservationLevel).map((value) => ({ value })),
},
{
name: "Status Message",
id: "statusMessage",
type: "string",
internal: 'o."status_message"',
},
{
name: "model",
id: "model",
type: "stringOptions",
internal: 'o."model"',
options: [], // to be added at runtime
},
{
name: "metadata",
id: "metadata",
type: "stringObject",
internal: 't."metadata"',
internal: 'o."metadata"',
},
{
name: "version",
id: "version",
type: "string",
internal: 'o."version"',
},
+15 -1
View File
@@ -1,7 +1,13 @@
import { type ColumnDefinition } from "@/src/server/api/interfaces/tableDefinition";
export const sessionsViewCols: ColumnDefinition[] = [
{ name: "⭐️", type: "boolean", internal: "s.bookmarked" },
{ name: "⭐️", id: "bookmarked", type: "boolean", internal: "s.bookmarked" },
{
name: "id",
id: "id",
type: "string",
internal: 's."id"',
},
{
name: "userId",
type: "string",
@@ -9,12 +15,20 @@ export const sessionsViewCols: ColumnDefinition[] = [
},
{
name: "Session duration (s)",
id: "sessionDuration",
type: "number",
internal: 'o."sessionDuration"',
},
{
name: "createdAt",
id: "createdAt",
type: "datetime",
internal: 's."created_at"',
},
{
name: "countTraces",
id: "countTraces",
type: "number",
internal: 't."countTraces"',
},
];
+24 -6
View File
@@ -6,7 +6,7 @@ import {
} from "@/src/server/api/trpc";
import { type Observation, Prisma } from "@prisma/client";
import { paginationZod } from "@/src/utils/zod";
import { jsonSchema, paginationZod } from "@/src/utils/zod";
import { singleFilter } from "@/src/server/api/interfaces/filters";
import {
datetimeFilterToPrismaSql,
@@ -30,6 +30,8 @@ import {
exportFileFormats,
exportOptions,
} from "@/src/server/api/interfaces/exportTypes";
import { orderBy } from "@/src/server/api/interfaces/orderBy";
import { orderByToPrismaSql } from "@/src/features/orderBy/server/orderByToPrisma";
const GenerationFilterOptions = z.object({
projectId: z.string(), // Required for protectedProjectProcedure
@@ -38,6 +40,7 @@ const GenerationFilterOptions = z.object({
});
const ListInputs = GenerationFilterOptions.extend({
orderBy: orderBy,
...paginationZod,
});
@@ -64,6 +67,11 @@ export const generationsRouter = createTRPCRouter({
observationsTableCols,
);
const orderByCondition = orderByToPrismaSql(
input.orderBy,
observationsTableCols,
);
// to improve query performance, add timeseries filter to observation queries as well
const startTimeFilter = input.filter.find(
(f) => f.column === "start_time" && f.type === "datetime",
@@ -122,7 +130,7 @@ export const generationsRouter = createTRPCRouter({
t.project_id = ${input.projectId}
${searchCondition}
${filterCondition}
ORDER BY o.start_time DESC
${orderByCondition}
LIMIT ${input.limit}
OFFSET ${input.page * input.limit}
`,
@@ -303,9 +311,11 @@ export const generationsRouter = createTRPCRouter({
content: z.string(),
}),
);
const outputSchema = z.object({
completion: z.string(),
});
const outputSchema = z
.object({
completion: jsonSchema,
})
.or(jsonSchema);
output = enrichedGenerations
.map((generation) => ({
parsedInput: inputSchemaOpenAI.safeParse(generation.input),
@@ -319,7 +329,14 @@ export const generationsRouter = createTRPCRouter({
? [
{
role: "assistant",
content: generation.parsedOutput.data.completion,
content:
typeof generation.parsedOutput.data ===
"object" &&
"completion" in generation.parsedOutput.data
? JSON.stringify(
generation.parsedOutput.data.completion,
)
: JSON.stringify(generation.parsedOutput.data),
},
]
: [],
@@ -329,6 +346,7 @@ export const generationsRouter = createTRPCRouter({
// to jsonl
.map((row) => JSON.stringify(row))
.join("\n");
console.log(output);
break;
default:
throw new Error("Invalid export file format");
+8 -1
View File
@@ -12,10 +12,13 @@ import { singleFilter } from "@/src/server/api/interfaces/filters";
import { paginationZod } from "@/src/utils/zod";
import { throwIfNoAccess } from "@/src/features/rbac/utils/checkAccess";
import { TRPCError } from "@trpc/server";
import { orderBy } from "@/src/server/api/interfaces/orderBy";
import { orderByToPrismaSql } from "@/src/features/orderBy/server/orderByToPrisma";
const SessionFilterOptions = z.object({
projectId: z.string(), // Required for protectedProjectProcedure
filter: z.array(singleFilter).nullable(),
orderBy: orderBy,
...paginationZod,
});
@@ -28,6 +31,10 @@ export const sessionRouter = createTRPCRouter({
input.filter ?? [],
sessionsViewCols,
);
const orderByCondition = orderByToPrismaSql(
input.orderBy,
sessionsViewCols,
);
const sessions = await ctx.prisma.$queryRaw<
Array<{
@@ -79,7 +86,7 @@ export const sessionRouter = createTRPCRouter({
WHERE
s."project_id" = ${input.projectId}
${filterCondition}
ORDER BY 2 desc
${orderByCondition}
LIMIT ${input.limit}
OFFSET ${input.page * input.limit}
`);
+13 -21
View File
@@ -23,7 +23,6 @@ import { TRPCError } from "@trpc/server";
import { orderBy } from "@/src/server/api/interfaces/orderBy";
import { orderByToPrismaSql } from "@/src/features/orderBy/server/orderByToPrisma";
import { type Sql } from "@prisma/client/runtime/library";
import { instrumentAsync } from "@/src/utils/instrumentation";
const TraceFilterOptions = z.object({
projectId: z.string(), // Required for protectedProjectProcedure
@@ -91,21 +90,17 @@ export const traceRouter = createTRPCRouter({
orderByCondition,
);
const traces = await instrumentAsync(
{ name: "get-all-traces" },
async () =>
await ctx.prisma.$queryRaw<
Array<
Trace & {
promptTokens: number;
completionTokens: number;
totalTokens: number;
totalCount: number;
latency: number | null;
}
>
>(tracesQuery),
);
const traces = await ctx.prisma.$queryRaw<
Array<
Trace & {
promptTokens: number;
completionTokens: number;
totalTokens: number;
totalCount: number;
latency: number | null;
}
>
>(tracesQuery);
const countQyery = createTracesQuery(
Prisma.sql`count(*)`,
@@ -118,11 +113,8 @@ export const traceRouter = createTRPCRouter({
Prisma.empty,
);
const totalTraces = await instrumentAsync(
{ name: "get-total-traces" },
async () =>
await ctx.prisma.$queryRaw<Array<{ count: bigint }>>(countQyery),
);
const totalTraces =
await ctx.prisma.$queryRaw<Array<{ count: bigint }>>(countQyery);
// get scores for each trace individually to increase
// performance of the query above
+70 -46
View File
@@ -57,10 +57,13 @@ export class ObservationProcessor implements EventProcessor {
this.event = event;
}
async convertToObservation(apiScope: ApiAccessScope): Promise<{
async convertToObservation(
apiScope: ApiAccessScope,
existingObservation: Observation | null,
): Promise<{
id: string;
create: Prisma.ObservationCreateInput;
update: Prisma.ObservationUpdateInput;
create: Prisma.ObservationUncheckedCreateInput;
update: Prisma.ObservationUncheckedUpdateInput;
}> {
const { body } = this.event;
@@ -85,12 +88,6 @@ export class ObservationProcessor implements EventProcessor {
const { id, traceId, name, startTime, metadata } = body;
const existingObservation = id
? await prisma.observation.findUnique({
where: { id, projectId: apiScope.projectId },
})
: null;
if (
this.event.type === eventTypes.OBSERVATION_UPDATE &&
!existingObservation
@@ -124,20 +121,26 @@ export class ObservationProcessor implements EventProcessor {
metadata ?? undefined,
);
const prompts =
const prompt =
"promptName" in this.event.body &&
typeof this.event.body.promptName === "string" &&
"promptVersion" in this.event.body &&
typeof this.event.body.promptVersion === "number"
? await prisma.prompt.findMany({
? await prisma.prompt.findUnique({
where: {
projectId: apiScope.projectId,
name: this.event.body.promptName,
version: this.event.body.promptVersion,
projectId_name_version: {
projectId: apiScope.projectId,
name: this.event.body.promptName,
version: this.event.body.promptVersion,
},
},
})
: undefined;
// Only null if promptName and promptVersion are set but prompt is not found
if (prompt === null)
console.warn("Prompt not found for observation", this.event.body);
const observationId = id ?? v4();
return {
id: observationId,
@@ -174,10 +177,8 @@ export class ObservationProcessor implements EventProcessor {
statusMessage: body.statusMessage ?? undefined,
parentObservationId: body.parentObservationId ?? undefined,
version: body.version ?? undefined,
project: { connect: { id: apiScope.projectId } },
...(prompts && prompts.length === 1
? { prompt: { connect: { id: prompts[0]?.id } } }
: undefined),
projectId: apiScope.projectId,
promptId: prompt ? prompt.id : undefined,
},
update: {
name,
@@ -209,9 +210,7 @@ export class ObservationProcessor implements EventProcessor {
statusMessage: body.statusMessage ?? undefined,
parentObservationId: body.parentObservationId ?? undefined,
version: body.version ?? undefined,
...(prompts && prompts.length === 1
? { prompt: { connect: { id: prompts[0]?.id } } }
: undefined),
promptId: prompt ? prompt.id : undefined,
},
};
}
@@ -250,14 +249,28 @@ export class ObservationProcessor implements EventProcessor {
if (apiScope.accessLevel !== "all")
throw new AuthenticationError("Access denied for observation creation");
const obs = await this.convertToObservation(apiScope);
const existingObservation = this.event.body.id
? await prisma.observation.findFirst({
where: { id: this.event.body.id },
})
: null;
if (
existingObservation &&
existingObservation.projectId !== apiScope.projectId
) {
throw new AuthenticationError(
`Access denied for observation creation ${existingObservation.projectId} `,
);
}
const obs = await this.convertToObservation(apiScope, existingObservation);
// Do not use nested upserts or multiple where conditions as this should be a single native database upsert
// https://www.prisma.io/docs/orm/reference/prisma-client-reference#database-upserts
return await prisma.observation.upsert({
where: {
id_projectId: {
id: obs.id,
projectId: apiScope.projectId,
},
id: obs.id,
},
create: obs.create,
update: obs.update,
@@ -277,7 +290,9 @@ export class TraceProcessor implements EventProcessor {
const { body } = this.event;
if (apiScope.accessLevel !== "all")
throw new AuthenticationError("Access denied for trace creation");
throw new AuthenticationError(
`Access denied for trace creation, ${apiScope.accessLevel}`,
);
const internalId = body.id ?? v4();
@@ -288,12 +303,18 @@ export class TraceProcessor implements EventProcessor {
body,
);
const existingTrace = await prisma.trace.findUnique({
const existingTrace = await prisma.trace.findFirst({
where: {
id: internalId,
},
});
if (existingTrace && existingTrace.projectId !== apiScope.projectId) {
throw new AuthenticationError(
`Access denied for trace creation ${existingTrace.projectId} `,
);
}
const mergedMetadata = mergeJson(
existingTrace?.metadata
? jsonSchema.parse(existingTrace.metadata)
@@ -301,10 +322,27 @@ export class TraceProcessor implements EventProcessor {
body.metadata ?? undefined,
);
if (body.sessionId) {
await prisma.traceSession.upsert({
where: {
id_projectId: {
id: body.sessionId,
projectId: apiScope.projectId,
},
},
create: {
id: body.sessionId,
projectId: apiScope.projectId,
},
update: {},
});
}
// Do not use nested upserts or multiple where conditions as this should be a single native database upsert
// https://www.prisma.io/docs/orm/reference/prisma-client-reference#database-upserts
const upsertedTrace = await prisma.trace.upsert({
where: {
id: internalId,
projectId: apiScope.projectId,
},
create: {
id: internalId,
@@ -315,16 +353,9 @@ export class TraceProcessor implements EventProcessor {
metadata: mergedMetadata ?? body.metadata ?? undefined,
release: body.release ?? undefined,
version: body.version ?? undefined,
session: body.sessionId
? {
connectOrCreate: {
where: { id: body.sessionId, projectId: apiScope.projectId },
create: { id: body.sessionId, projectId: apiScope.projectId },
},
}
: undefined,
sessionId: body.sessionId ?? undefined,
public: body.public ?? undefined,
project: { connect: { id: apiScope.projectId } },
projectId: apiScope.projectId,
tags: body.tags ?? undefined,
},
update: {
@@ -335,14 +366,7 @@ export class TraceProcessor implements EventProcessor {
metadata: mergedMetadata ?? body.metadata ?? undefined,
release: body.release ?? undefined,
version: body.version ?? undefined,
session: body.sessionId
? {
connectOrCreate: {
where: { id: body.sessionId, projectId: apiScope.projectId },
create: { id: body.sessionId, projectId: apiScope.projectId },
},
}
: undefined,
sessionId: body.sessionId ?? undefined,
public: body.public ?? undefined,
tags: body.tags ?? undefined,
},