Compare commits

...
29 Commits
Author SHA1 Message Date
Marc Klingen 8584f93307 chore: release v2.30.2
CI/CD / lint (push) Waiting to run
CI/CD / test-docker-build (push) Waiting to run
CI/CD / tests-web (20) (push) Waiting to run
CI/CD / tests-worker (20) (push) Waiting to run
CI/CD / e2e-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-04-19 18:05:17 +02:00
Marc KlingenandGitHub 08e576f815 feat(cloud): add settings for posthog integration (#1778) 2024-04-19 17:38:55 +02:00
Marc KlingenandGitHub f64da44b20 chore: update docs links in settings (#1786) 2024-04-19 10:49:26 +00:00
Marc KlingenandGitHub c720a4d896 perf: reduce memory usage of users.all trpc route (#1784) 2024-04-19 10:03:06 +00:00
Marc Klingen b59b61a5cf chore: release v2.30.1
CI/CD / lint (push) Waiting to run
CI/CD / test-docker-build (push) Waiting to run
CI/CD / tests-web (20) (push) Waiting to run
CI/CD / tests-worker (20) (push) Waiting to run
CI/CD / e2e-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-04-19 01:07:09 +02:00
Marc KlingenandGitHub 3f18e42fef feat(api): add countObservations to metrics endpoint, model nullable (#1779) 2024-04-18 23:04:29 +00:00
Marc KlingenandGitHub 31a9be6f1d fix(api): support special characters in dataset name and run name (#1762) 2024-04-18 22:49:10 +00:00
Marc Klingen fe6d8a38ec chore: release v2.30.0
CI/CD / lint (push) Waiting to run
CI/CD / test-docker-build (push) Waiting to run
CI/CD / tests-web (20) (push) Waiting to run
CI/CD / tests-worker (20) (push) Waiting to run
CI/CD / e2e-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-04-18 18:25:26 +02:00
Marc KlingenandGitHub d4fecbb2a6 fix: support special characters for prompt names (#1763) 2024-04-18 14:20:20 +00:00
Marc KlingenandGitHub d92ba1d26b feat(ui): concise IOTableCell for small rowHeight across tables (#1766) 2024-04-18 13:42:48 +00:00
Marc KlingenandGitHub 61c2312f12 chore: add dev env flag (#1765) 2024-04-18 15:29:41 +02:00
Marc KlingenandGitHub 862a1395d5 feat(ui): render errors on sign in page and log unknown to sentry (#1767) 2024-04-18 14:56:14 +02:00
Marc KlingenandGitHub 21e9e17e93 docs: add comment on too large page sizes to openapi reference (#1772) 2024-04-18 11:17:11 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
5b32d4b9a2 chore(deps): bump react-responsive from 9.0.2 to 10.0.0 (#1749)
Bumps [react-responsive](https://github.com/yocontra/react-responsive) from 9.0.2 to 10.0.0.
- [Changelog](https://github.com/yocontra/react-responsive/blob/master/CHANGELOG.md)
- [Commits](https://github.com/yocontra/react-responsive/compare/v9.0.2...v10.0.0)

---
updated-dependencies:
- dependency-name: react-responsive
  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-04-17 23:17:07 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
ea5617d621 chore(deps-dev): bump @typescript-eslint/parser from 7.6.0 to 7.7.0 (#1750)
Bumps [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) from 7.6.0 to 7.7.0.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/parser/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v7.7.0/packages/parser)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-17 23:10:35 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
9c72fddc20 chore(deps-dev): bump vitest from 1.4.0 to 1.5.0 (#1751)
Bumps [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest) from 1.4.0 to 1.5.0.
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Commits](https://github.com/vitest-dev/vitest/commits/v1.5.0/packages/vitest)

---
updated-dependencies:
- dependency-name: vitest
  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-04-17 18:44:22 +00:00
Marc Klingen 548669e546 chore: release v2.29.2
CI/CD / lint (push) Waiting to run
CI/CD / test-docker-build (push) Waiting to run
CI/CD / tests-web (20) (push) Waiting to run
CI/CD / tests-worker (20) (push) Waiting to run
CI/CD / e2e-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-04-17 17:08:09 +02:00
Marc Klingen a7a5870508 fix: do not load NODE_ENV for posthog in frontend 2024-04-17 16:58:55 +02:00
Marc Klingen 0385a52e2c chore: release v2.29.1
CI/CD / lint (push) Waiting to run
CI/CD / test-docker-build (push) Waiting to run
CI/CD / tests-web (20) (push) Waiting to run
CI/CD / tests-worker (20) (push) Waiting to run
CI/CD / e2e-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-04-17 15:09:29 +02:00
Marc Klingen dd3f4824e3 fix: import types from correct package to fix runtime issue 2024-04-17 15:09:15 +02:00
Marc Klingen aa6145e341 chore: release v2.29.0
CI/CD / lint (push) Waiting to run
CI/CD / test-docker-build (push) Waiting to run
CI/CD / tests-web (20) (push) Waiting to run
CI/CD / tests-worker (20) (push) Waiting to run
CI/CD / e2e-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-04-17 14:24:13 +02:00
Marc KlingenandGitHub a70f1d58a5 feat(datasets): add metadata to dataset and datasetItems (#1756) 2024-04-17 14:22:20 +02:00
Max DeichmannandGitHub 16870fb489 fix: use trash icon for eval config (#1757) 2024-04-17 11:15:19 +00:00
Max DeichmannandGitHub 24b8722ad5 fix: disable eval config after confirmation only (#1746) 2024-04-16 22:08:14 +00:00
d0cf2f56a0 feat(scores): Add a score value filter to api (#1745)
---------

Co-authored-by: DHacky <dhgacky@gmail.com>
2024-04-16 21:46:52 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
20e3c25fd6 chore(deps): bump the patches group with 6 updates (#1741)
Bumps the patches group with 6 updates:

| Package | From | To |
| --- | --- | --- |
| [@headlessui/react](https://github.com/tailwindlabs/headlessui/tree/HEAD/packages/@headlessui-react) | `1.7.18` | `1.7.19` |
| [@langchain/core](https://github.com/langchain-ai/langchainjs) | `0.1.57` | `0.1.58` |
| [ai](https://github.com/vercel/ai) | `3.0.22` | `3.0.23` |
| [react-day-picker](https://github.com/gpbl/react-day-picker) | `8.10.0` | `8.10.1` |
| [@types/react](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react) | `18.2.78` | `18.2.79` |
| [prettier-plugin-tailwindcss](https://github.com/tailwindlabs/prettier-plugin-tailwindcss) | `0.5.13` | `0.5.14` |


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

Updates `@langchain/core` from 0.1.57 to 0.1.58
- [Release notes](https://github.com/langchain-ai/langchainjs/releases)
- [Changelog](https://github.com/langchain-ai/langchainjs/blob/main/release_workspace.js)
- [Commits](https://github.com/langchain-ai/langchainjs/commits)

Updates `ai` from 3.0.22 to 3.0.23
- [Release notes](https://github.com/vercel/ai/releases)
- [Commits](https://github.com/vercel/ai/compare/ai@3.0.22...ai@3.0.23)

Updates `react-day-picker` from 8.10.0 to 8.10.1
- [Release notes](https://github.com/gpbl/react-day-picker/releases)
- [Commits](https://github.com/gpbl/react-day-picker/compare/v8.10.0...v8.10.1)

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

Updates `prettier-plugin-tailwindcss` from 0.5.13 to 0.5.14
- [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.13...v0.5.14)

---
updated-dependencies:
- dependency-name: "@headlessui/react"
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: patches
- dependency-name: "@langchain/core"
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: patches
- dependency-name: ai
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: patches
- dependency-name: react-day-picker
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: patches
- dependency-name: "@types/react"
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: patches
- dependency-name: prettier-plugin-tailwindcss
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: patches
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-16 19:58:15 +00:00
b2be82b436 feat: add posthog to chatCompletion endpoint (#1742)
---------

Co-authored-by: Marc Klingen <git@marcklingen.com>
2024-04-16 19:42:14 +00:00
Marc Klingen 779771950b chore: add crisp env to env.mjs 2024-04-16 21:36:38 +02:00
Max DeichmannandGitHub fdfc64b853 perf: add internal model index (#1743) 2024-04-16 19:54:31 +02:00
91 changed files with 2123 additions and 846 deletions
+1 -1
View File
@@ -33,7 +33,7 @@
"@repo/eslint-config": "*",
"@repo/typescript-config": "*",
"@types/node": "^20.11.29",
"@typescript-eslint/parser": "^7.6.0",
"@typescript-eslint/parser": "^7.7.0",
"eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.0",
"eslint-config-standard": "^17.1.0",
+2 -8
View File
@@ -1,15 +1,9 @@
import { z } from "zod";
import { env as sharedEnv } from "@langfuse/shared";
const EnvSchema = z.object({
NEXT_PUBLIC_LANGFUSE_CLOUD_REGION: z.string().optional(),
ENCRYPTION_KEY: z
.string()
.length(
64,
"ENCRYPTION_KEY must be 256 bits, 64 string characters in hex format, generate via: openssl rand -hex 32"
)
.optional(),
ADMIN_API_KEY: z.string().optional(),
});
export const env = EnvSchema.parse(process.env);
export const env = { ...sharedEnv, ...EnvSchema.parse(process.env) };
+1 -1
View File
@@ -6,8 +6,8 @@ import Auth0Provider from "next-auth/providers/auth0";
import AzureADProvider from "next-auth/providers/azure-ad";
import { isEeAvailable } from "..";
import { prisma } from "@langfuse/shared/src/db";
import { encrypt, decrypt } from "@langfuse/shared/encryption";
import { SsoProviderSchema } from "./types";
import { decrypt, encrypt } from "../encryption";
import { type NextApiRequest, type NextApiResponse } from "next";
import { env } from "../env";
+3
View File
@@ -127,6 +127,7 @@ types:
id: string
name: string
description: optional<string>
metadata: optional<unknown>
projectId: string
createdAt: datetime
updatedAt: datetime
@@ -151,6 +152,7 @@ types:
status: DatasetStatus
input: optional<unknown>
expectedOutput: optional<unknown>
metadata: optional<unknown>
sourceTraceId: optional<string>
sourceObservationId: optional<string>
datasetId: string
@@ -209,6 +211,7 @@ types:
enum:
- API
- REVIEW
- EVAL
errors:
Error:
@@ -26,6 +26,9 @@ types:
datasetName: string
input: optional<unknown>
expectedOutput: optional<unknown>
metadata: optional<unknown>
sourceTraceId: optional<string>
sourceObservationId: optional<string>
id:
type: optional<string>
docs: Dataset items are upserted on their id
+1
View File
@@ -47,3 +47,4 @@ types:
properties:
name: string
description: optional<string>
metadata: optional<unknown>
+2 -1
View File
@@ -47,12 +47,13 @@ types:
properties:
date: date
countTraces: integer
countObservations: integer
totalCost: double
usage: list<UsageByModel>
UsageByModel:
docs: Daily usage of a given model. Usage corresponds to the unit set for the specific model (e.g. tokens).
properties:
model: string
model: optional<string>
inputUsage: integer
outputUsage: integer
totalUsage: integer
+2 -2
View File
@@ -24,10 +24,10 @@ service:
query-parameters:
page:
type: optional<integer>
docs: page number, starts at 1
docs: Page number, starts at 1.
limit:
type: optional<integer>
docs: limit of items per page
docs: Limit of items per page. If you encounter api issues due to too large page sizes, try to reduce the limit.
name: optional<string>
userId: optional<string>
type: optional<string>
+11 -2
View File
@@ -21,15 +21,24 @@ service:
query-parameters:
page:
type: optional<integer>
docs: page number, starts at 1
docs: Page number, starts at 1.
limit:
type: optional<integer>
docs: limit of items per page
docs: Limit of items per page. If you encounter api issues due to too large page sizes, try to reduce the limit.
userId: optional<string>
name: optional<string>
fromTimestamp:
type: optional<datetime>
docs: Retrieve only scores newer than this timestamp.
source:
type: commons.ScoreSource
docs: Retrieve only scores from a specific source.
operator:
type: optional<string>
docs: Retrieve only scores with <operator> value.
value:
type: optional<double>
docs: Retrieve only scores with <operator> value.
response: Scores
get-by-id:
docs: Get a score
+3 -3
View File
@@ -16,7 +16,7 @@ service:
docs: The unique langfuse identifier of a trace
response: commons.TraceWithFullDetails
list:
docs: Get list of traces
docs: Get list of traces.
method: GET
path: /traces
request:
@@ -24,10 +24,10 @@ service:
query-parameters:
page:
type: optional<integer>
docs: page number, starts at 1
docs: Page number, starts at 1
limit:
type: optional<integer>
docs: limit of items per page
docs: Limit of items per page. If you encounter api issues due to too large page sizes, try to reduce the limit.
userId: optional<string>
name: optional<string>
fromTimestamp:
+1 -1
View File
@@ -1,4 +1,4 @@
{
"organization": "finto",
"version": "0.19.30"
"version": "0.21.0"
}
+25 -10
View File
@@ -65,7 +65,7 @@
"auth": null,
"body": {
"mode": "raw",
"raw": "{\n \"datasetName\": \"example\",\n \"input\": \"UNKNOWN\",\n \"expectedOutput\": \"UNKNOWN\",\n \"id\": \"example\"\n}",
"raw": "{\n \"datasetName\": \"example\",\n \"input\": \"UNKNOWN\",\n \"expectedOutput\": \"UNKNOWN\",\n \"metadata\": \"UNKNOWN\",\n \"sourceTraceId\": \"example\",\n \"sourceObservationId\": \"example\",\n \"id\": \"example\"\n}",
"options": {
"raw": {
"language": "json"
@@ -245,7 +245,7 @@
"auth": null,
"body": {
"mode": "raw",
"raw": "{\n \"name\": \"example\",\n \"description\": \"example\"\n}",
"raw": "{\n \"name\": \"example\",\n \"description\": \"example\",\n \"metadata\": \"UNKNOWN\"\n}",
"options": {
"raw": {
"language": "json"
@@ -493,12 +493,12 @@
{
"key": "page",
"value": "",
"description": "page number, starts at 1"
"description": "Page number, starts at 1."
},
{
"key": "limit",
"value": "",
"description": "limit of items per page"
"description": "Limit of items per page. If you encounter api issues due to too large page sizes, try to reduce the limit."
},
{
"key": "name",
@@ -694,7 +694,7 @@
"request": {
"description": "Get a list of scores",
"url": {
"raw": "{{baseUrl}}/api/public/scores?page=&limit=&userId=&name=&fromTimestamp=",
"raw": "{{baseUrl}}/api/public/scores?page=&limit=&userId=&name=&fromTimestamp=&source=&operator=&value=",
"host": [
"{{baseUrl}}"
],
@@ -707,12 +707,12 @@
{
"key": "page",
"value": "",
"description": "page number, starts at 1"
"description": "Page number, starts at 1."
},
{
"key": "limit",
"value": "",
"description": "limit of items per page"
"description": "Limit of items per page. If you encounter api issues due to too large page sizes, try to reduce the limit."
},
{
"key": "userId",
@@ -728,6 +728,21 @@
"key": "fromTimestamp",
"value": "",
"description": "Retrieve only scores newer than this timestamp."
},
{
"key": "source",
"value": "",
"description": "Retrieve only scores from a specific source."
},
{
"key": "operator",
"value": "",
"description": "Retrieve only scores with <operator> value."
},
{
"key": "value",
"value": "",
"description": "Retrieve only scores with <operator> value."
}
],
"variable": []
@@ -885,7 +900,7 @@
"_type": "endpoint",
"name": "List",
"request": {
"description": "Get list of traces",
"description": "Get list of traces.",
"url": {
"raw": "{{baseUrl}}/api/public/traces?page=&limit=&userId=&name=&fromTimestamp=&orderBy=&tags=",
"host": [
@@ -900,12 +915,12 @@
{
"key": "page",
"value": "",
"description": "page number, starts at 1"
"description": "Page number, starts at 1"
},
{
"key": "limit",
"value": "",
"description": "limit of items per page"
"description": "Limit of items per page. If you encounter api issues due to too large page sizes, try to reduce the limit."
},
{
"key": "userId",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "langfuse",
"version": "2.28.0",
"version": "2.30.2",
"author": "engineering@langfuse.com",
"license": "MIT",
"private": true,
+1 -1
View File
@@ -9,7 +9,7 @@
],
"devDependencies": {
"@typescript-eslint/eslint-plugin": "^7.1.0",
"@typescript-eslint/parser": "^7.6.0",
"@typescript-eslint/parser": "^7.7.0",
"@vercel/style-guide": "^6.0.0",
"eslint-config-next": "^14.2.1",
"eslint-config-prettier": "^9.1.0",
+8 -3
View File
@@ -20,6 +20,10 @@
"./src/server/auth": {
"import": "./dist/src/server/auth.js",
"require": "./dist/src/server/auth.js"
},
"./encryption": {
"import": "./dist/src/encryption/index.js",
"require": "./dist/src/encryption/index.js"
}
},
"scripts": {
@@ -28,6 +32,7 @@
"lint": "eslint . --ext .js,.jsx,.ts,.tsx",
"lint:fix": "eslint . --ext .js,.jsx,.ts,.tsx --fix",
"db:migrate": "DISABLE_ERD=false dotenv -e ../../.env -- npx prisma migrate dev",
"db:push": "DISABLE_ERD=false dotenv -e ../../.env -- npx prisma db push",
"db:reset": "dotenv -e ../../.env npx -- prisma migrate reset",
"db:deploy": "dotenv -e ../../.env npx -- prisma migrate deploy",
"db:seed": "dotenv -e ../../.env -- npx prisma db seed",
@@ -40,7 +45,7 @@
},
"dependencies": {
"@langchain/anthropic": "^0.1.14",
"@langchain/core": "^0.1.57",
"@langchain/core": "^0.1.58",
"@langchain/openai": "^0.0.28",
"@prisma/client": "^5.12.1",
"@types/bcryptjs": "^2.4.6",
@@ -58,7 +63,7 @@
"@types/node": "^20.11.29",
"@types/pg": "^8.11.5",
"@types/uuid": "^9.0.8",
"@typescript-eslint/parser": "^7.6.0",
"@typescript-eslint/parser": "^7.7.0",
"eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.0",
"eslint-config-standard": "^17.1.0",
@@ -72,6 +77,6 @@
"ts-node": "^10.9.2",
"tsc-watch": "^6.2.0",
"typescript": "^5.4.5",
"vitest": "^1.3.1"
"vitest": "^1.5.0"
}
}
+10
View File
@@ -113,6 +113,7 @@ export type Dataset = {
id: string;
name: string;
description: string | null;
metadata: unknown | null;
project_id: string;
created_at: Generated<Timestamp>;
updated_at: Generated<Timestamp>;
@@ -122,6 +123,7 @@ export type DatasetItem = {
status: Generated<DatasetStatus>;
input: unknown | null;
expected_output: unknown | null;
metadata: unknown | null;
source_trace_id: string | null;
source_observation_id: string | null;
dataset_id: string;
@@ -290,6 +292,13 @@ export type ObservationView = {
calculated_total_cost: string | null;
latency: number | null;
};
export type PosthogIntegration = {
project_id: string;
encrypted_posthog_api_key: string;
posthog_host_name: string;
last_sync_at: Timestamp | null;
enabled: boolean;
};
export type Pricing = {
id: string;
model_name: string;
@@ -420,6 +429,7 @@ export type DB = {
models: Model;
observations: Observation;
observations_view: ObservationView;
posthog_integrations: PosthogIntegration;
pricings: Pricing;
projects: Project;
prompts: Prompt;
@@ -0,0 +1,2 @@
-- CreateIndex
CREATE INDEX CONCURRENTLY "observations_internal_model_idx" ON "observations"("internal_model");
@@ -0,0 +1,5 @@
-- AlterTable
ALTER TABLE "dataset_items" ADD COLUMN "metadata" JSONB;
-- AlterTable
ALTER TABLE "datasets" ADD COLUMN "metadata" JSONB;
@@ -0,0 +1,16 @@
-- CreateTable
CREATE TABLE "posthog_integrations" (
"project_id" TEXT NOT NULL,
"encrypted_posthog_api_key" TEXT NOT NULL,
"posthog_host_name" TEXT NOT NULL,
"last_sync_at" TIMESTAMP(3),
"enabled" BOOLEAN NOT NULL,
CONSTRAINT "posthog_integrations_pkey" PRIMARY KEY ("project_id")
);
-- CreateIndex
CREATE INDEX "posthog_integrations_project_id_idx" ON "posthog_integrations"("project_id");
-- AddForeignKey
ALTER TABLE "posthog_integrations" ADD CONSTRAINT "posthog_integrations_project_id_fkey" FOREIGN KEY ("project_id") REFERENCES "projects"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+35 -19
View File
@@ -92,25 +92,26 @@ model VerificationToken {
}
model Project {
id String @id @default(cuid())
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
name String
cloudConfig Json? @map("cloud_config") // Langfuse Cloud, for zod schema see projectsRouter.ts
members Membership[]
traces Trace[]
observations Observation[]
apiKeys ApiKey[]
dataset Dataset[]
RawEvents Events[]
invitations MembershipInvitation[]
sessions TraceSession[]
Prompt Prompt[]
Model Model[]
AuditLog AuditLog[]
EvalTemplate EvalTemplate[]
JobConfiguration JobConfiguration[]
JobExecution JobExecution[]
id String @id @default(cuid())
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
name String
cloudConfig Json? @map("cloud_config") // Langfuse Cloud, for zod schema see projectsRouter.ts
members Membership[]
traces Trace[]
observations Observation[]
apiKeys ApiKey[]
dataset Dataset[]
RawEvents Events[]
invitations MembershipInvitation[]
sessions TraceSession[]
Prompt Prompt[]
Model Model[]
AuditLog AuditLog[]
EvalTemplate EvalTemplate[]
JobConfiguration JobConfiguration[]
JobExecution JobExecution[]
PosthogIntegration PosthogIntegration[]
@@map("projects")
}
@@ -302,6 +303,7 @@ model Observation {
@@index(projectId)
@@index(parentObservationId)
@@index(model)
@@index(internalModel)
@@index(promptId)
@@index([projectId, startTime, type])
@@map("observations")
@@ -429,6 +431,7 @@ model Dataset {
id String @id @default(cuid())
name String
description String?
metadata Json?
projectId String @map("project_id")
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now()) @map("created_at")
@@ -446,6 +449,7 @@ model DatasetItem {
status DatasetStatus @default(ACTIVE)
input Json?
expectedOutput Json? @map("expected_output")
metadata Json?
sourceTraceId String? @map("source_trace_id")
sourceTrace Trace? @relation(fields: [sourceTraceId], references: [id], onDelete: SetNull)
sourceObservationId String? @map("source_observation_id")
@@ -696,3 +700,15 @@ model SsoConfig {
@@map("sso_configs")
}
model PosthogIntegration {
projectId String @id @map("project_id")
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
encryptedPosthogApiKey String @map("encrypted_posthog_api_key")
posthogHostName String @map("posthog_host_name")
lastSyncAt DateTime? @map("last_sync_at")
enabled Boolean
@@index([projectId])
@@map("posthog_integrations")
}
+2
View File
@@ -240,6 +240,7 @@ async function main() {
description:
datasetNumber === 0 ? "Dataset test description" : undefined,
projectId: project2.id,
metadata: datasetNumber === 0 ? { key: "value" } : undefined,
},
});
@@ -268,6 +269,7 @@ async function main() {
Math.random() > 0.3
? "Creating a React component can be done in two ways: as a functional component or as a class component. Let's start with a basic example of both."
: undefined,
metadata: Math.random() > 0.5 ? { key: "value" } : undefined,
},
});
datasetItemIds.push(datasetItem.id);
+13
View File
@@ -0,0 +1,13 @@
import { z } from "zod";
const EnvSchema = z.object({
ENCRYPTION_KEY: z
.string()
.length(
64,
"ENCRYPTION_KEY must be 256 bits, 64 string characters in hex format, generate via: openssl rand -hex 32"
)
.optional(),
});
export const env = EnvSchema.parse(process.env);
+1
View File
@@ -9,6 +9,7 @@ export * from "./filterToPrisma";
export * from "./tracesTable";
export * from "./server/auth";
export * from "./observationsTable";
export { env } from "./env";
// llm api
export * from "./server/llm/types";
@@ -4,6 +4,7 @@ import {
HumanMessage,
SystemMessage,
} from "@langchain/core/messages";
import type { BaseCallbackHandler } from "@langchain/core/callbacks/base";
import {
BytesOutputParser,
StringOutputParser,
@@ -25,6 +26,7 @@ type LLMCompletionParams = {
messages: ChatMessage[];
modelParams: ModelParams;
functionCall?: LLMFunctionCall;
callbacks?: BaseCallbackHandler[];
};
type FetchLLMCompletionParams = LLMCompletionParams & {
@@ -34,14 +36,12 @@ type FetchLLMCompletionParams = LLMCompletionParams & {
export async function fetchLLMCompletion(
params: LLMCompletionParams & {
streaming: true;
functionCall: undefined;
}
): Promise<IterableReadableStream<Uint8Array>>;
export async function fetchLLMCompletion(
params: LLMCompletionParams & {
streaming: false;
functionCall: undefined;
}
): Promise<string>;
@@ -55,7 +55,7 @@ export async function fetchLLMCompletion(
export async function fetchLLMCompletion(
params: FetchLLMCompletionParams
): Promise<string | IterableReadableStream<Uint8Array> | unknown> {
const { messages, modelParams, streaming } = params;
const { messages, modelParams, streaming, callbacks } = params;
const finalMessages = messages.map((message) => {
if (message.role === ChatMessageRole.User)
return new HumanMessage(message.content);
@@ -73,6 +73,7 @@ export async function fetchLLMCompletion(
temperature: modelParams.temperature,
maxTokens: modelParams.max_tokens,
topP: modelParams.top_p,
callbacks,
});
} else {
chatModel = new ChatOpenAI({
@@ -81,6 +82,7 @@ export async function fetchLLMCompletion(
temperature: modelParams.temperature,
maxTokens: modelParams.max_tokens,
topP: modelParams.top_p,
callbacks,
});
}
+505 -524
View File
File diff suppressed because it is too large Load Diff
+53 -8
View File
@@ -590,14 +590,16 @@ paths:
parameters:
- name: page
in: query
description: page number, starts at 1
description: Page number, starts at 1.
required: false
schema:
type: integer
nullable: true
- name: limit
in: query
description: limit of items per page
description: >-
Limit of items per page. If you encounter api issues due to too
large page sizes, try to reduce the limit.
required: false
schema:
type: integer
@@ -865,14 +867,16 @@ paths:
parameters:
- name: page
in: query
description: page number, starts at 1
description: Page number, starts at 1.
required: false
schema:
type: integer
nullable: true
- name: limit
in: query
description: limit of items per page
description: >-
Limit of items per page. If you encounter api issues due to too
large page sizes, try to reduce the limit.
required: false
schema:
type: integer
@@ -897,6 +901,27 @@ paths:
type: string
format: date-time
nullable: true
- name: source
in: query
description: Retrieve only scores from a specific source.
required: true
schema:
$ref: '#/components/schemas/ScoreSource'
- name: operator
in: query
description: Retrieve only scores with <operator> value.
required: false
schema:
type: string
nullable: true
- name: value
in: query
description: Retrieve only scores with <operator> value.
required: false
schema:
type: number
format: double
nullable: true
responses:
'200':
description: ''
@@ -1111,21 +1136,23 @@ paths:
security: *ref_0
/api/public/traces:
get:
description: Get list of traces
description: Get list of traces.
operationId: trace_list
tags:
- Trace
parameters:
- name: page
in: query
description: page number, starts at 1
description: Page number, starts at 1
required: false
schema:
type: integer
nullable: true
- name: limit
in: query
description: limit of items per page
description: >-
Limit of items per page. If you encounter api issues due to too
large page sizes, try to reduce the limit.
required: false
schema:
type: integer
@@ -1508,6 +1535,8 @@ components:
description:
type: string
nullable: true
metadata:
nullable: true
projectId:
type: string
createdAt:
@@ -1571,6 +1600,8 @@ components:
nullable: true
expectedOutput:
nullable: true
metadata:
nullable: true
sourceTraceId:
type: string
nullable: true
@@ -1703,6 +1734,7 @@ components:
enum:
- API
- REVIEW
- EVAL
CreateDatasetItemRequest:
title: CreateDatasetItemRequest
type: object
@@ -1713,6 +1745,14 @@ components:
nullable: true
expectedOutput:
nullable: true
metadata:
nullable: true
sourceTraceId:
type: string
nullable: true
sourceObservationId:
type: string
nullable: true
id:
type: string
nullable: true
@@ -1768,6 +1808,8 @@ components:
description:
type: string
nullable: true
metadata:
nullable: true
required:
- name
HealthResponse:
@@ -2355,6 +2397,8 @@ components:
type: string
countTraces:
type: integer
countObservations:
type: integer
totalCost:
type: number
format: double
@@ -2365,6 +2409,7 @@ components:
required:
- date
- countTraces
- countObservations
- totalCost
- usage
UsageByModel:
@@ -2376,6 +2421,7 @@ components:
properties:
model:
type: string
nullable: true
inputUsage:
type: integer
outputUsage:
@@ -2383,7 +2429,6 @@ components:
totalUsage:
type: integer
required:
- model
- inputUsage
- outputUsage
- totalUsage
+9 -9
View File
@@ -1,6 +1,6 @@
{
"name": "web",
"version": "2.28.0",
"version": "2.30.2",
"private": true,
"license": "MIT",
"engines": {
@@ -26,11 +26,11 @@
"@aws-sdk/lib-storage": "^3.550.0",
"@aws-sdk/s3-request-presigner": "^3.554.0",
"@codemirror/lang-json": "^6.0.1",
"@headlessui/react": "^1.7.18",
"@headlessui/react": "^1.7.19",
"@heroicons/react": "^2.1.3",
"@hookform/resolvers": "^3.3.4",
"@langchain/anthropic": "^0.1.14",
"@langchain/core": "^0.1.57",
"@langchain/core": "^0.1.58",
"@langchain/openai": "^0.0.28",
"@langfuse/shared": "workspace:*",
"@langfuse/ee": "workspace:*",
@@ -75,7 +75,7 @@
"@trpc/server": "^10.45.0",
"@uiw/codemirror-theme-github": "^4.21.25",
"@uiw/react-codemirror": "^4.21.25",
"ai": "^3.0.22",
"ai": "^3.0.23",
"bcryptjs": "^2.4.3",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.0",
@@ -98,11 +98,11 @@
"posthog-node": "^3.6.3",
"prisma": "^5.12.1",
"react": "18.2.0",
"react-day-picker": "^8.10.0",
"react-day-picker": "^8.10.1",
"react-dom": "18.2.0",
"react-hook-form": "^7.51.3",
"react-icons": "^5.0.1",
"react-responsive": "^9.0.2",
"react-responsive": "^10.0.0",
"react18-json-view": "^0.2.8-canary.6",
"sonner": "^1.4.41",
"superjson": "2.2.1",
@@ -125,11 +125,11 @@
"@types/lodash": "^4.14.202",
"@types/node": "20.10.5",
"@types/nodemailer": "^6.4.14",
"@types/react": "^18.2.78",
"@types/react": "^18.2.79",
"@types/react-dom": "^18.2.25",
"@types/uuid": "^9.0.8",
"@typescript-eslint/eslint-plugin": "^6.21.0",
"@typescript-eslint/parser": "^7.6.0",
"@typescript-eslint/parser": "^7.7.0",
"autoprefixer": "^10.4.19",
"dotenv-cli": "^7.3.0",
"eslint": "^8.56.0",
@@ -139,7 +139,7 @@
"node-mocks-http": "^1.14.1",
"postcss": "^8.4.38",
"prettier": "^3.2.5",
"prettier-plugin-tailwindcss": "^0.5.13",
"prettier-plugin-tailwindcss": "^0.5.14",
"tailwindcss": "^3.4.3",
"ts-node": "^10.9.2",
"tsconfig-paths": "^4.2.0",
+82 -66
View File
@@ -5,63 +5,12 @@ import { makeAPICall, pruneDatabase } from "@/src/__tests__/test-utils";
import { v4 } from "uuid";
describe("/api/public/datasets and /api/public/dataset-items API Endpoints", () => {
beforeEach(async () => await pruneDatabase());
afterEach(async () => await pruneDatabase());
it("should create and get a dataset", async () => {
await makeAPICall("POST", "/api/public/datasets", {
name: "dataset-name",
description: "dataset-description",
});
const dbDataset = await prisma.dataset.findMany({
where: {
name: "dataset-name",
},
});
expect(dbDataset.length).toBeGreaterThan(0);
const getDataset = await makeAPICall(
"GET",
`/api/public/datasets/dataset-name`,
);
expect(getDataset.status).toBe(200);
expect(getDataset.body).toMatchObject({
name: "dataset-name",
description: "dataset-description",
});
});
it("GET datasets", async () => {
await makeAPICall("POST", "/api/public/datasets", {
name: "dataset-name-1",
});
await makeAPICall("POST", "/api/public/datasets", {
name: "dataset-name-2",
});
const datasetItemId = v4();
const createItemRes = await makeAPICall<{
datasetName: string; // field that can break if the API changes as it is not a db column
}>("POST", "/api/public/dataset-items", {
datasetName: "dataset-name-2",
input: { key: "value" },
expectedOutput: { key: "value" },
id: datasetItemId,
});
expect(createItemRes.status).toBe(200);
expect(createItemRes.body).toMatchObject({
datasetName: "dataset-name-2", // not included in db table
});
const traceId = v4();
const observationId = v4();
const traceId = v4();
const observationId = v4();
beforeEach(async () => {
await pruneDatabase();
// create sample trace and observation
const response = await makeAPICall("POST", "/api/public/ingestion", {
batch: [
{
@@ -97,6 +46,62 @@ describe("/api/public/datasets and /api/public/dataset-items API Endpoints", ()
],
});
expect(response.status).toBe(207);
});
afterEach(async () => await pruneDatabase());
it("should create and get a dataset, include special characters", async () => {
await makeAPICall("POST", "/api/public/datasets", {
name: "dataset + name",
description: "dataset-description",
metadata: { foo: "bar" },
});
const dbDataset = await prisma.dataset.findMany({
where: {
name: "dataset + name",
},
});
expect(dbDataset.length).toBeGreaterThan(0);
const getDataset = await makeAPICall(
"GET",
`/api/public/datasets/${encodeURIComponent("dataset + name")}`,
);
expect(getDataset.status).toBe(200);
expect(getDataset.body).toMatchObject({
name: "dataset + name",
description: "dataset-description",
metadata: { foo: "bar" },
});
});
it("GET datasets", async () => {
await makeAPICall("POST", "/api/public/datasets", {
name: "dataset-name-1",
});
await makeAPICall("POST", "/api/public/datasets", {
name: "dataset-name-2",
});
const datasetItemId = v4();
const createItemRes = await makeAPICall<{
datasetName: string; // field that can break if the API changes as it is not a db column
}>("POST", "/api/public/dataset-items", {
datasetName: "dataset-name-2",
input: { key: "value" },
expectedOutput: { key: "value" },
metadata: { key: "value-dataset-item" },
id: datasetItemId,
});
expect(createItemRes.status).toBe(200);
expect(createItemRes.body).toMatchObject({
datasetName: "dataset-name-2", // not included in db table
});
await makeAPICall("POST", "/api/public/dataset-run-items", {
datasetItemId: datasetItemId,
@@ -136,6 +141,9 @@ describe("/api/public/datasets and /api/public/dataset-items API Endpoints", ()
datasetName: "dataset-name",
input: { key: "value" },
expectedOutput: { key: "value" },
metadata: { key: "value-dataset-item" },
sourceTraceId: traceId,
sourceObservationId: observationId,
});
const dbDatasetItem = await prisma.datasetItem.findFirst({
where: {
@@ -159,6 +167,7 @@ describe("/api/public/datasets and /api/public/dataset-items API Endpoints", ()
id: dbDatasetItem!.id,
input: { key: "value" },
expectedOutput: { key: "value" },
metadata: { key: "value-dataset-item" },
datasetName: "dataset-name", // not included in db table
},
],
@@ -173,7 +182,10 @@ describe("/api/public/datasets and /api/public/dataset-items API Endpoints", ()
id: dbDatasetItem!.id,
input: { key: "value" },
expectedOutput: { key: "value" },
metadata: { key: "value-dataset-item" },
datasetName: "dataset-name", // not included in db table
sourceTraceId: traceId,
sourceObservationId: observationId,
});
});
@@ -186,6 +198,7 @@ describe("/api/public/datasets and /api/public/dataset-items API Endpoints", ()
id: "dataset-item-id",
datasetName: "dataset-name",
input: { key: "value" },
metadata: { key: "value-dataset-item" },
});
expect(item1.status).toBe(200);
expect(item1.body).toMatchObject({
@@ -196,6 +209,7 @@ describe("/api/public/datasets and /api/public/dataset-items API Endpoints", ()
id: "dataset-item-id",
datasetName: "dataset-name",
input: { key: "value2" },
metadata: ["hello-world"],
});
expect(item2.status).toBe(200);
expect(item2.body).toMatchObject({
@@ -208,22 +222,23 @@ describe("/api/public/datasets and /api/public/dataset-items API Endpoints", ()
});
expect(dbDatasetItem).not.toBeNull();
expect(dbDatasetItem?.input).toMatchObject({ key: "value2" });
expect(dbDatasetItem?.metadata).toMatchObject(["hello-world"]);
});
it("should create and get a dataset run", async () => {
it("should create and get a dataset run, include special characters", async () => {
const dataset = await makeAPICall<{ id: string }>(
"POST",
"/api/public/datasets",
{
name: "dataset-name",
name: "dataset name",
},
);
expect(dataset.status).toBe(200);
expect(dataset.body).toMatchObject({
name: "dataset-name",
name: "dataset name",
});
await makeAPICall("POST", "/api/public/dataset-items", {
datasetName: "dataset-name",
datasetName: "dataset name",
id: "dataset-item-id",
input: { key: "value" },
expectedOutput: { key: "value" },
@@ -272,14 +287,14 @@ describe("/api/public/datasets and /api/public/dataset-items API Endpoints", ()
{
datasetItemId: "dataset-item-id",
observationId: observationId,
runName: "run-only-observation",
runName: "run + only + observation",
runDescription: "run-description",
metadata: { key: "value" },
},
);
const dbRunObservation = await prisma.datasetRuns.findFirst({
where: {
name: "run-only-observation",
name: "run + only + observation",
},
include: {
datasetRunItems: true,
@@ -298,21 +313,22 @@ describe("/api/public/datasets and /api/public/dataset-items API Endpoints", ()
const getRunAPI = await makeAPICall(
"GET",
`/api/public/datasets/dataset-name/runs/run-only-observation`,
`/api/public/datasets/${encodeURIComponent("dataset name")}/runs/${encodeURIComponent("run + only + observation")}`,
);
expect(getRunAPI.status).toBe(200);
expect(getRunAPI.body).toMatchObject({
name: "run-only-observation",
name: "run + only + observation",
description: "run-description",
metadata: { key: "value" },
datasetId: dataset.body.id,
datasetName: "dataset-name",
datasetName: "dataset name",
datasetRunItems: expect.arrayContaining([
expect.objectContaining({
datasetItemId: "dataset-item-id",
observationId: observationId,
traceId: traceId,
datasetRunName: "run-only-observation",
datasetRunName: "run + only + observation",
}),
]),
});
+43
View File
@@ -56,6 +56,49 @@ describe("/api/public/prompts API Endpoint", () => {
expect(fetchedObservations.body.tags).toEqual([]);
});
it("should fetch a prompt with special character", async () => {
const promptId = uuidv4();
await prisma.prompt.create({
data: {
id: promptId,
name: "prompt + name",
prompt: "prompt",
isActive: true,
version: 1,
config: {
temperature: 0.1,
},
project: {
connect: { id: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a" },
},
createdBy: "user-1",
},
});
const fetchedObservations = await makeAPICall(
"GET",
`/api/public/prompts?name=${encodeURIComponent("prompt + name")}&version=1`,
undefined,
);
expect(fetchedObservations.status).toBe(200);
if (!isPrompt(fetchedObservations.body)) {
throw new Error("Expected body to be a prompt");
}
expect(fetchedObservations.body.id).toBe(promptId);
expect(fetchedObservations.body.name).toBe("prompt + name");
expect(fetchedObservations.body.prompt).toBe("prompt");
expect(fetchedObservations.body.type).toBe("text");
expect(fetchedObservations.body.version).toBe(1);
expect(fetchedObservations.body.isActive).toBe(true);
expect(fetchedObservations.body.createdBy).toBe("user-1");
expect(fetchedObservations.body.config).toEqual({ temperature: 0.1 });
expect(fetchedObservations.body.tags).toEqual([]);
});
it("should fetch active prompt only if no prompt version is given", async () => {
const promptId = uuidv4();
+277 -2
View File
@@ -5,8 +5,13 @@ import { makeAPICall, pruneDatabase } from "@/src/__tests__/test-utils";
import { v4 as uuidv4 } from "uuid";
describe("/api/public/scores API Endpoint", () => {
beforeEach(async () => await pruneDatabase());
afterEach(async () => await pruneDatabase());
let should_prune_db = true;
beforeEach(async () => {
if (should_prune_db) await pruneDatabase();
});
afterEach(async () => {
if (should_prune_db) await pruneDatabase();
});
it("should create score for a trace", async () => {
await pruneDatabase();
@@ -316,4 +321,274 @@ describe("/api/public/scores API Endpoint", () => {
observationId: generationId,
});
});
describe("should Filter scores", () => {
const userId = "user-name";
const scoreName = "score-name";
const queryUserName = `userId=${userId}&name=${scoreName}`;
const traceId = uuidv4();
const generationId = uuidv4();
const scoreId_1 = uuidv4();
const scoreId_2 = uuidv4();
const scoreId_3 = uuidv4();
interface GetScoresAPIResponse {
data: [
{
id: string;
name: string;
value: number;
},
];
meta: object;
}
beforeAll(async () => {
should_prune_db = false;
await pruneDatabase();
await makeAPICall("POST", "/api/public/traces", {
id: traceId,
userId: userId,
});
await makeAPICall("POST", "/api/public/generations", {
id: generationId,
});
await makeAPICall("POST", "/api/public/scores", {
id: scoreId_1,
observationId: generationId,
name: scoreName,
value: 10.5,
traceId: traceId,
comment: "comment",
});
await makeAPICall("POST", "/api/public/scores", {
id: scoreId_2,
observationId: generationId,
name: scoreName,
value: 50.5,
traceId: traceId,
comment: "comment",
});
await makeAPICall("POST", "/api/public/scores", {
id: scoreId_3,
observationId: generationId,
name: scoreName,
value: 100.8,
traceId: traceId,
comment: "comment",
});
});
afterAll(async () => {
await pruneDatabase();
});
it("get all scores", async () => {
const getAllScore = await makeAPICall<{
data: [
{
traceId: string;
observationId: string;
},
];
meta: object;
}>("GET", `/api/public/scores?${queryUserName}`);
expect(getAllScore.status).toBe(200);
expect(getAllScore.body.meta).toMatchObject({
page: 1,
limit: 50,
totalItems: 3,
totalPages: 1,
});
for (const val of getAllScore.body.data) {
expect(val).toMatchObject({
traceId: traceId,
observationId: generationId,
});
}
});
it("test only operator", async () => {
const getScore = await makeAPICall<GetScoresAPIResponse>(
"GET",
`/api/public/scores?${queryUserName}&operator=<`,
);
expect(getScore.status).toBe(200);
expect(getScore.body.meta).toMatchObject({
page: 1,
limit: 50,
totalItems: 3,
totalPages: 1,
});
});
it("test only value", async () => {
const getScore = await makeAPICall<GetScoresAPIResponse>(
"GET",
`/api/public/scores?${queryUserName}&value=0.8`,
);
expect(getScore.status).toBe(200);
expect(getScore.body.meta).toMatchObject({
page: 1,
limit: 50,
totalItems: 3,
totalPages: 1,
});
});
it("test operator <", async () => {
const getScore = await makeAPICall<GetScoresAPIResponse>(
"GET",
`/api/public/scores?${queryUserName}&operator=<&value=50`,
);
expect(getScore.status).toBe(200);
expect(getScore.body.meta).toMatchObject({
page: 1,
limit: 50,
totalItems: 1,
totalPages: 1,
});
expect(getScore.body.data).toMatchObject([
{
id: scoreId_1,
name: scoreName,
value: 10.5,
},
]);
});
it("test operator >", async () => {
const getScore = await makeAPICall<GetScoresAPIResponse>(
"GET",
`/api/public/scores?${queryUserName}&operator=>&value=100`,
);
expect(getScore.status).toBe(200);
expect(getScore.body.meta).toMatchObject({
page: 1,
limit: 50,
totalItems: 1,
totalPages: 1,
});
expect(getScore.body.data).toMatchObject([
{
id: scoreId_3,
name: scoreName,
value: 100.8,
},
]);
});
it("test operator <=", async () => {
const getScore = await makeAPICall<GetScoresAPIResponse>(
"GET",
`/api/public/scores?${queryUserName}&operator=<=&value=50.5`,
);
expect(getScore.status).toBe(200);
expect(getScore.body.meta).toMatchObject({
page: 1,
limit: 50,
totalItems: 2,
totalPages: 1,
});
expect(getScore.body.data).toMatchObject([
{
id: scoreId_1,
name: scoreName,
value: 10.5,
},
{
id: scoreId_2,
name: scoreName,
value: 50.5,
},
]);
});
it("test operator >=", async () => {
const getScore = await makeAPICall<GetScoresAPIResponse>(
"GET",
`/api/public/scores?${queryUserName}&operator=>=&value=50.5`,
);
expect(getScore.status).toBe(200);
expect(getScore.body.meta).toMatchObject({
page: 1,
limit: 50,
totalItems: 2,
totalPages: 1,
});
expect(getScore.body.data).toMatchObject([
{
id: scoreId_2,
name: scoreName,
value: 50.5,
},
{
id: scoreId_3,
name: scoreName,
value: 100.8,
},
]);
});
it("test operator !=", async () => {
const getScore = await makeAPICall<GetScoresAPIResponse>(
"GET",
`/api/public/scores?${queryUserName}&operator=!=&value=50.5`,
);
expect(getScore.status).toBe(200);
expect(getScore.body.meta).toMatchObject({
page: 1,
limit: 50,
totalItems: 2,
totalPages: 1,
});
expect(getScore.body.data).toMatchObject([
{
id: scoreId_1,
name: scoreName,
value: 10.5,
},
{
id: scoreId_3,
name: scoreName,
value: 100.8,
},
]);
});
it("test operator =", async () => {
const getScore = await makeAPICall<GetScoresAPIResponse>(
"GET",
`/api/public/scores?${queryUserName}&operator==&value=50.5`,
);
expect(getScore.status).toBe(200);
expect(getScore.body.meta).toMatchObject({
page: 1,
limit: 50,
totalItems: 1,
totalPages: 1,
});
expect(getScore.body.data).toMatchObject([
{
id: scoreId_2,
name: scoreName,
value: 50.5,
},
]);
});
it("test invalid operator", async () => {
const getScore = await makeAPICall(
"GET",
`/api/public/scores?${queryUserName}&operator=op&value=50.5`,
);
expect(getScore.status).toBe(400);
expect(getScore.body).toMatchObject({
message: "Invalid request data",
});
});
it("test invalid value", async () => {
const getScore = await makeAPICall(
"GET",
`/api/public/scores?${queryUserName}&operator=<&value=myvalue`,
);
expect(getScore.status).toBe(400);
expect(getScore.body).toMatchObject({
message: "Invalid request data",
});
});
});
});
+9 -3
View File
@@ -1,4 +1,4 @@
import { AlertTriangle } from "lucide-react";
import { AlertTriangle, Check } from "lucide-react";
import { VERSION } from "@/src/constants";
import { env } from "@/src/env.mjs";
@@ -42,10 +42,16 @@ export const LangfuseLogo = ({
"flex items-center gap-2 self-stretch rounded-md px-3 py-2 text-xs ring-1 lg:-mx-2",
env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION === "STAGING"
? "bg-blue-100 text-blue-500 ring-blue-500"
: "bg-red-100 text-red-500 ring-red-500",
: env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION === "DEV"
? "bg-green-100 text-green-500 ring-green-500"
: "bg-red-100 text-red-500 ring-red-500",
)}
>
<AlertTriangle size={16} />
{env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION === "DEV" ? (
<Check size={16} />
) : (
<AlertTriangle size={16} />
)}
<span className="whitespace-nowrap">
{["EU", "US"].includes(env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION)
? `PROD-${env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION}`
+21 -3
View File
@@ -5,6 +5,7 @@ import { useSession } from "next-auth/react";
import DocPopup from "@/src/components/layouts/doc-popup";
import { type Status, StatusBadge } from "./status-badge";
import { cn } from "@/src/utils/tailwind";
import { Badge } from "@/src/components/ui/badge";
export default function Header({
level = "h2",
@@ -14,8 +15,10 @@ export default function Header({
breadcrumb?: { name: string; href?: string }[];
status?: Status;
help?: { description: string; href?: string };
featureBetaURL?: string;
actionButtons?: React.ReactNode;
level?: "h2" | "h3";
className?: string;
}) {
const router = useRouter();
const session = useSession();
@@ -40,7 +43,7 @@ export default function Header({
[...props.breadcrumb.map((i) => i.href).filter(Boolean)].pop();
return (
<div className={cn(level === "h2" ? "mb-4" : "mb-2")}>
<div className={cn(level === "h2" ? "mb-4" : "mb-2", props.className)}>
<div>
{backHref ? (
<nav className="sm:hidden" aria-label="Back">
@@ -89,7 +92,7 @@ export default function Header({
</div>
<div className="mt-2 flex flex-wrap items-center justify-between gap-2">
<div className="flex items-center gap-3 md:gap-5">
<div className="flex min-w-0 flex-row">
<div className="flex min-w-0 flex-row justify-center align-middle">
{level === "h2" ? (
<h2 className="text-2xl font-bold leading-7 text-gray-900 sm:truncate sm:text-3xl sm:tracking-tight">
{props.title}
@@ -106,10 +109,25 @@ export default function Header({
size="sm"
/>
) : null}
{props.featureBetaURL ? (
<Link
href={props.featureBetaURL}
rel="noopener noreferrer"
target="_blank"
className="flex items-center"
>
<Badge
title="Feature is currently in beta. Click to learn more."
className="ml-2"
>
Beta
</Badge>
</Link>
) : null}
</div>
{props.status && <StatusBadge type={props.status} />}
</div>
<div className="flex items-center gap-3">
<div className="flex flex-wrap items-center gap-3">
{props.actionButtons ?? null}
</div>
</div>
+6 -4
View File
@@ -461,13 +461,15 @@ export default function Layout(props: PropsWithChildren) {
href={
env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION === "EU"
? "https://langfuse.com/docs/demo"
: "https://docs-staging.langfuse.com/docs/demo"
: "https://docs-staging.langfuse.com/docs/demo" // staging
}
target="_blank"
>
{env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION === "EU"
? "Use Chat ↗"
: "Use Chat (staging) ↗"}
{
env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION === "EU"
? "Use Chat ↗"
: "Use Chat (staging) ↗" // staging
}
</Link>
</Button>
</div>
+2
View File
@@ -95,6 +95,8 @@ export const ROUTES: Route[] = [
pathname: "/project/[projectId]/playground",
icon: TerminalIcon,
featureFlag: "playground",
cloudOnly: true,
label: "Beta",
},
{
name: "Datasets",
@@ -487,6 +487,7 @@ export default function GenerationsTable({ projectId }: GenerationsTableProps) {
observationId={observationId}
traceId={traceId}
io="input"
singleLine={rowHeight === "s"}
/>
);
},
@@ -505,6 +506,7 @@ export default function GenerationsTable({ projectId }: GenerationsTableProps) {
observationId={observationId}
traceId={traceId}
io="output"
singleLine={rowHeight === "s"}
/>
);
},
@@ -518,7 +520,9 @@ export default function GenerationsTable({ projectId }: GenerationsTableProps) {
const values = row.getValue(
"metadata",
) as GenerationsTableRow["metadata"];
return !!values ? <IOTableCell data={values} /> : null;
return !!values ? (
<IOTableCell data={values} singleLine={rowHeight === "s"} />
) : null;
},
enableHiding: true,
defaultHidden: true,
@@ -673,10 +677,12 @@ const GenerationsIOCell = ({
traceId,
observationId,
io,
singleLine = false,
}: {
traceId: string;
observationId: string;
io: "input" | "output";
singleLine: boolean;
}) => {
const observation = api.observations.byId.useQuery(
{
@@ -700,6 +706,7 @@ const GenerationsIOCell = ({
io === "output" ? observation.data?.output : observation.data?.input
}
className={cn(io === "output" && "bg-green-50")}
singleLine={singleLine}
/>
);
};
@@ -193,7 +193,11 @@ export default function ScoresTable({
enableHiding: true,
cell: ({ row }) => {
const value = row.getValue("comment") as ScoresTableRow["comment"];
return value !== undefined && <IOTableCell data={value} />;
return (
value !== undefined && (
<IOTableCell data={value} singleLine={rowHeight === "s"} />
)
);
},
},
];
+20 -5
View File
@@ -5,7 +5,7 @@ import { TraceTableMultiSelectAction } from "@/src/components/table/data-table-m
import { DataTableToolbar } from "@/src/components/table/data-table-toolbar";
import TableLink from "@/src/components/table/table-link";
import { type LangfuseColumnDef } from "@/src/components/table/types";
import { TagTracePopver } from "@/src/features/tag/components/TagTracePopver";
import { TagTracePopover } from "@/src/features/tag/components/TagTracePopver";
import { TokenUsageBadge } from "@/src/components/token-usage-badge";
import { Checkbox } from "@/src/components/ui/checkbox";
import useColumnVisibility from "@/src/features/column-visibility/hooks/useColumnVisibility";
@@ -469,7 +469,13 @@ export default function TracesTable({
id: "input",
cell: ({ row }) => {
const traceId: string = row.getValue("id");
return <TracesIOCell traceId={traceId} io="input" />;
return (
<TracesIOCell
traceId={traceId}
io="input"
singleLine={rowHeight === "s"}
/>
);
},
enableHiding: true,
defaultHidden: true,
@@ -480,7 +486,13 @@ export default function TracesTable({
id: "output",
cell: ({ row }) => {
const traceId: string = row.getValue("id");
return <TracesIOCell traceId={traceId} io="output" />;
return (
<TracesIOCell
traceId={traceId}
io="output"
singleLine={rowHeight === "s"}
/>
);
},
enableHiding: true,
defaultHidden: true,
@@ -490,7 +502,7 @@ export default function TracesTable({
header: "Metadata",
cell: ({ row }) => {
const values: string = row.getValue("metadata");
return <IOTableCell data={values} />;
return <IOTableCell data={values} singleLine={rowHeight === "s"} />;
},
enableHiding: true,
},
@@ -540,7 +552,7 @@ export default function TracesTable({
const filterOptionTags = traceFilterOptions.data?.tags ?? [];
const allTags = filterOptionTags.map((t) => t.value);
return (
<TagTracePopver
<TagTracePopover
tags={tags}
availableTags={allTags}
projectId={projectId}
@@ -643,9 +655,11 @@ export default function TracesTable({
const TracesIOCell = ({
traceId,
io,
singleLine = false,
}: {
traceId: string;
io: "input" | "output";
singleLine?: boolean;
}) => {
const trace = api.traces.byId.useQuery(
{ traceId: traceId },
@@ -664,6 +678,7 @@ const TracesIOCell = ({
isLoading={trace.isLoading}
data={io === "output" ? trace.data?.output : trace.data?.input}
className={cn(io === "output" && "bg-green-50")}
singleLine={singleLine}
/>
);
};
@@ -134,6 +134,7 @@ export const ObservationPreview = (props: {
projectId={props.projectId}
input={observationWithInputAndOutput.data.input}
output={observationWithInputAndOutput.data.output}
metadata={preloadedObservation.metadata}
key={preloadedObservation.id}
/>
) : null}
@@ -70,6 +70,7 @@ export const TracePreview = ({
projectId={trace.projectId}
input={trace.input}
output={trace.output}
metadata={trace.metadata}
key={trace.id}
/>
</div>
+12 -1
View File
@@ -110,18 +110,29 @@ export const IOTableCell = ({
data,
isLoading = false,
className,
singleLine = false,
}: {
data: unknown;
isLoading?: boolean;
className?: string;
singleLine?: boolean;
}) => {
return (
<>
{isLoading ? (
<JsonSkeleton className="h-full w-[400px] overflow-hidden px-2 py-1" />
) : singleLine ? (
<div
className={cn(
"h-full w-[400px] self-stretch overflow-hidden overflow-y-auto truncate rounded-sm border px-2 py-0.5",
className,
)}
>
{stringifyJsonNode(data)}
</div>
) : (
<JSONView
json={data}
json={stringifyJsonNode(data)}
className={cn(
"h-full w-[400px] self-stretch overflow-y-auto rounded-sm ",
className,
+1 -1
View File
@@ -1 +1 @@
export const VERSION = "v2.28.0";
export const VERSION = "v2.30.2";
+7
View File
@@ -97,6 +97,9 @@ export const env = createEnv({
NEXT_PUBLIC_DEMO_PROJECT_ID: z.string().optional(),
NEXT_PUBLIC_SIGN_UP_DISABLED: z.enum(["true", "false"]).optional(),
NEXT_PUBLIC_TURNSTILE_SITE_KEY: z.string().optional(),
NEXT_PUBLIC_POSTHOG_KEY: z.string().optional(),
NEXT_PUBLIC_POSTHOG_HOST: z.string().optional(),
NEXT_PUBLIC_CRISP_WEBSITE_ID: z.string().optional(),
},
/**
@@ -173,6 +176,10 @@ export const env = createEnv({
ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY,
TURNSTILE_SECRET_KEY: process.env.TURNSTILE_SECRET_KEY,
NEXT_PUBLIC_TURNSTILE_SITE_KEY: process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY,
NEXT_PUBLIC_POSTHOG_KEY: process.env.NEXT_PUBLIC_POSTHOG_KEY,
NEXT_PUBLIC_POSTHOG_HOST: process.env.NEXT_PUBLIC_POSTHOG_HOST,
// Other
NEXT_PUBLIC_CRISP_WEBSITE_ID: process.env.NEXT_PUBLIC_CRISP_WEBSITE_ID,
},
// Skip validation in Docker builds
// DOCKER_BUILD is set in Dockerfile
+2 -1
View File
@@ -15,7 +15,8 @@ export type AuditableResource =
| "session"
| "apiKey"
| "evalTemplate"
| "job";
| "job"
| "posthogIntegration";
type AuditLog = {
resourceType: AuditableResource;
@@ -12,18 +12,25 @@ const regions =
flag: "🇪🇺",
},
]
: [
{
name: "US",
hostname: "us.cloud.langfuse.com",
flag: "🇺🇸",
},
{
name: "EU",
hostname: "cloud.langfuse.com",
flag: "🇪🇺",
},
];
: env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION === "DEV"
? [
{
name: "DEV",
flag: "🚧",
},
]
: [
{
name: "US",
hostname: "us.cloud.langfuse.com",
flag: "🇺🇸",
},
{
name: "EU",
hostname: "cloud.langfuse.com",
flag: "🇪🇺",
},
];
export function CloudRegionSwitch({
isSignUpPage,
@@ -66,7 +73,8 @@ export function CloudRegionSwitch({
send_instantly: true,
},
);
window.location.hostname = region.hostname;
if ("hostname" in region)
window.location.hostname = region.hostname;
}}
>
<span className="mr-2 text-xl leading-none">{region.flag}</span>
@@ -11,6 +11,7 @@ import { useState } from "react";
import { DialogTrigger } from "@radix-ui/react-dialog";
import { DatasetForm } from "@/src/features/datasets/components/DatasetForm";
import { useHasAccess } from "@/src/features/rbac/utils/checkAccess";
import { type Prisma } from "@langfuse/shared";
interface BaseDatasetButtonProps {
mode: "create" | "update" | "delete";
@@ -33,6 +34,7 @@ interface UpdateDatasetButtonProps extends BaseDatasetButtonProps {
datasetId: string;
datasetName: string;
datasetDescription?: string;
datasetMetadata?: Prisma.JsonValue;
icon?: boolean;
}
@@ -130,6 +132,7 @@ export const DatasetActionButton = (props: DatasetActionButtonProps) => {
datasetId={props.datasetId}
datasetName={props.datasetName}
datasetDescription={props.datasetDescription}
datasetMetadata={props.datasetMetadata}
/>
)}
</DialogContent>
@@ -14,6 +14,8 @@ import { api } from "@/src/utils/api";
import { useState } from "react";
import { usePostHog } from "posthog-js/react";
import { Input } from "@/src/components/ui/input";
import { JsonEditor } from "@/src/components/json-editor";
import { type Prisma } from "@langfuse/shared";
interface BaseDatasetFormProps {
mode: "create" | "update" | "delete";
@@ -36,6 +38,7 @@ interface UpdateDatasetFormProps extends BaseDatasetFormProps {
datasetId: string;
datasetName: string;
datasetDescription?: string;
datasetMetadata?: Prisma.JsonValue;
}
type DatasetFormProps =
@@ -51,6 +54,21 @@ const formSchema = z.object({
message: "Input should not be only whitespace",
}),
description: z.string(),
metadata: z.string().refine(
(value) => {
if (value === "") return true;
try {
JSON.parse(value);
return true;
} catch (error) {
return false;
}
},
{
message:
"Invalid input. Please provide a JSON object or double-quoted string.",
},
),
});
export const DatasetForm = (props: DatasetFormProps) => {
@@ -63,10 +81,14 @@ export const DatasetForm = (props: DatasetFormProps) => {
? {
name: props.datasetName,
description: props.datasetDescription ?? "",
metadata: props.datasetMetadata
? JSON.stringify(props.datasetMetadata, null, 2)
: "",
}
: {
name: "",
description: "",
metadata: "",
},
});
@@ -173,6 +195,24 @@ export const DatasetForm = (props: DatasetFormProps) => {
</FormItem>
)}
/>
<FormField
control={form.control}
name="metadata"
render={({ field }) => (
<FormItem>
<FormLabel>Metadata (optional)</FormLabel>
<FormControl>
<JsonEditor
defaultValue={field.value}
onChange={(v) => {
field.onChange(v);
}}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
)}
<Button
@@ -33,14 +33,17 @@ type RowData = {
createdAt: string;
input: Prisma.JsonValue;
expectedOutput: Prisma.JsonValue;
metadata: Prisma.JsonValue;
};
export function DatasetItemsTable({
projectId,
datasetId,
menuItems,
}: {
projectId: string;
datasetId: string;
menuItems?: React.ReactNode;
}) {
const { setDetailPageList } = useDetailPageLists();
const utils = api.useUtils();
@@ -152,7 +155,9 @@ export function DatasetItemsTable({
enableHiding: true,
cell: ({ row }) => {
const input = row.getValue("input") as RowData["input"];
return !!input ? <IOTableCell data={input} /> : null;
return !!input ? (
<IOTableCell data={input} singleLine={rowHeight === "s"} />
) : null;
},
},
{
@@ -165,7 +170,23 @@ export function DatasetItemsTable({
"expectedOutput",
) as RowData["expectedOutput"];
return !!expectedOutput ? (
<IOTableCell data={expectedOutput} className="bg-green-50" />
<IOTableCell
data={expectedOutput}
className="bg-green-50"
singleLine={rowHeight === "s"}
/>
) : null;
},
},
{
accessorKey: "metadata",
header: "Metadata",
id: "metadata",
enableHiding: true,
cell: ({ row }) => {
const metadata = row.getValue("metadata") as RowData["metadata"];
return !!metadata ? (
<IOTableCell data={metadata} singleLine={rowHeight === "s"} />
) : null;
},
},
@@ -224,6 +245,7 @@ export function DatasetItemsTable({
createdAt: item.createdAt.toLocaleString(),
input: item.input,
expectedOutput: item.expectedOutput,
metadata: item.metadata,
};
};
@@ -240,6 +262,7 @@ export function DatasetItemsTable({
setColumnVisibility={setColumnVisibility}
rowHeight={rowHeight}
setRowHeight={setRowHeight}
actionButtons={menuItems}
/>
<DataTable
columns={columns}
@@ -161,6 +161,7 @@ export function DatasetRunItemsTable(
traceId={trace.traceId}
observationId={trace.observationId}
io="input"
singleLine={rowHeight === "s"}
/>
) : null;
},
@@ -177,6 +178,7 @@ export function DatasetRunItemsTable(
traceId={trace.traceId}
observationId={trace.observationId}
io="output"
singleLine={rowHeight === "s"}
/>
) : null;
},
@@ -194,6 +196,7 @@ export function DatasetRunItemsTable(
datasetId={props.datasetId}
datasetItemId={datasetItemId}
io="expectedOutput"
singleLine={rowHeight === "s"}
/>
) : null;
},
@@ -271,10 +274,12 @@ const TraceObservationIOCell = ({
traceId,
observationId,
io,
singleLine = false,
}: {
traceId: string;
observationId?: string;
io: "input" | "output";
singleLine?: boolean;
}) => {
// conditionally fetch the trace or observation depending on the presence of observationId
const trace = api.traces.byId.useQuery(
@@ -312,6 +317,7 @@ const TraceObservationIOCell = ({
isLoading={!!!observationId ? trace.isLoading : observation.isLoading}
data={io === "output" ? data?.output : data?.input}
className={cn(io === "output" && "bg-green-50")}
singleLine={singleLine}
/>
);
};
@@ -321,11 +327,13 @@ const DatasetItemIOCell = ({
datasetId,
datasetItemId,
io,
singleLine = false,
}: {
projectId: string;
datasetId: string;
datasetItemId: string;
io: "expectedOutput" | "input";
singleLine?: boolean;
}) => {
const datasetItem = api.datasets.itemById.useQuery(
{
@@ -351,6 +359,7 @@ const DatasetItemIOCell = ({
? datasetItem.data?.expectedOutput
: datasetItem.data?.input
}
singleLine={singleLine}
/>
);
};
@@ -33,6 +33,7 @@ type RowData = {
export function DatasetRunsTable(props: {
projectId: string;
datasetId: string;
menuItems?: React.ReactNode;
}) {
const [paginationState, setPaginationState] = useQueryParams({
pageIndex: withDefault(NumberParam, 0),
@@ -138,7 +139,9 @@ export function DatasetRunsTable(props: {
enableHiding: true,
cell: ({ row }) => {
const metadata: RowData["metadata"] = row.getValue("metadata");
return !!metadata ? <IOTableCell data={metadata} /> : null;
return !!metadata ? (
<IOTableCell data={metadata} singleLine={rowHeight === "s"} />
) : null;
},
},
];
@@ -171,6 +174,7 @@ export function DatasetRunsTable(props: {
setColumnVisibility={setColumnVisibility}
rowHeight={rowHeight}
setRowHeight={setRowHeight}
actionButtons={props.menuItems}
/>
<DataTable
columns={columns}
@@ -17,6 +17,9 @@ import { MoreVertical } from "lucide-react";
import { useEffect } from "react";
import useColumnVisibility from "@/src/features/column-visibility/hooks/useColumnVisibility";
import { DataTableToolbar } from "@/src/components/table/data-table-toolbar";
import { type Prisma } from "@langfuse/shared";
import { IOTableCell } from "@/src/components/ui/CodeJsonViewer";
import { useRowHeightLocalStorage } from "@/src/components/table/data-table-row-height-switch";
type RowData = {
key: {
@@ -28,11 +31,14 @@ type RowData = {
lastRunAt?: string;
countItems: number;
countRuns: number;
metadata: Prisma.JsonValue;
};
export function DatasetsTable(props: { projectId: string }) {
const { setDetailPageList } = useDetailPageLists();
const [rowHeight, setRowHeight] = useRowHeightLocalStorage("datasets", "s");
const [paginationState, setPaginationState] = useQueryParams({
pageIndex: withDefault(NumberParam, 0),
pageSize: withDefault(NumberParam, 50),
@@ -100,6 +106,18 @@ export function DatasetsTable(props: { projectId: string }) {
id: "lastRunAt",
enableHiding: true,
},
{
accessorKey: "metadata",
header: "Metadata",
id: "metadata",
enableHiding: true,
cell: ({ row }) => {
const metadata: RowData["metadata"] = row.getValue("metadata");
return !!metadata ? (
<IOTableCell data={metadata} singleLine={rowHeight === "s"} />
) : null;
},
},
{
id: "actions",
accessorKey: "actions",
@@ -145,6 +163,7 @@ export function DatasetsTable(props: { projectId: string }) {
lastRunAt: item.lastRunAt?.toLocaleString() ?? "",
countItems: item.countDatasetItems,
countRuns: item.countDatasetRuns,
metadata: item.metadata,
};
};
@@ -162,6 +181,8 @@ export function DatasetsTable(props: { projectId: string }) {
actionButtons={
<DatasetActionButton projectId={props.projectId} mode="create" />
}
rowHeight={rowHeight}
setRowHeight={setRowHeight}
/>
<DataTable
columns={columns}
@@ -189,6 +210,7 @@ export function DatasetsTable(props: { projectId: string }) {
}}
columnVisibility={columnVisibility}
onColumnVisibilityChange={setColumnVisibility}
rowHeight={rowHeight}
/>
</div>
);
@@ -47,6 +47,21 @@ const formSchema = z.object({
"Invalid input. Please provide a JSON object or double-quoted string.",
},
),
metadata: z.string().refine(
(value) => {
if (value === "") return true;
try {
JSON.parse(value);
return true;
} catch (error) {
return false;
}
},
{
message:
"Invalid input. Please provide a JSON object or double-quoted string.",
},
),
});
export const EditDatasetItem = ({
@@ -75,6 +90,12 @@ export const EditDatasetItem = ({
? JSON.stringify(datasetItem.expectedOutput, null, 2)
: "",
);
form.setValue(
"metadata",
datasetItem?.metadata
? JSON.stringify(datasetItem.metadata, null, 2)
: "",
);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [datasetItem]);
@@ -83,6 +104,7 @@ export const EditDatasetItem = ({
defaultValues: {
input: "",
expectedOutput: "",
metadata: "",
},
});
@@ -99,6 +121,7 @@ export const EditDatasetItem = ({
datasetItemId: datasetItem.id,
input: values.input,
expectedOutput: values.expectedOutput,
metadata: values.metadata,
});
setHasChanges(false);
}
@@ -154,6 +177,26 @@ export const EditDatasetItem = ({
)}
/>
</div>
<FormField
control={form.control}
name="metadata"
render={({ field }) => (
<FormItem>
<FormLabel>Metadata</FormLabel>
<FormControl>
<JsonEditor
defaultValue={field.value}
onChange={(v) => {
setHasChanges(true);
field.onChange(v);
}}
editable={hasAccess}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className="flex justify-end">
<Button
type="submit"
@@ -37,10 +37,8 @@ export const NewDatasetItemButton = (props: {
New item
</Button>
</DialogTrigger>
<DialogContent className="h-[calc(100vh-5rem)] max-h-none w-[calc(100vw-5rem)] max-w-none">
<DialogHeader>
<DialogTitle>Create new dataset item</DialogTitle>
</DialogHeader>
<DialogContent className="h-[calc(100vh-5rem)] max-h-none w-[calc(100vw-5rem)] max-w-none items-start">
<DialogHeader>Create new dataset item</DialogHeader>
<NewDatasetItemForm
projectId={props.projectId}
datasetId={props.datasetId}
@@ -21,7 +21,7 @@ import { api } from "@/src/utils/api";
import { useState } from "react";
import { usePostHog } from "posthog-js/react";
import { JsonEditor } from "@/src/components/json-editor";
import { type Prisma } from "@langfuse/shared/src/db";
import { type Prisma } from "@langfuse/shared";
import { cn } from "@/src/utils/tailwind";
const formSchema = z.object({
@@ -56,6 +56,21 @@ const formSchema = z.object({
"Invalid input. Please provide a JSON object or double-quoted string.",
},
),
metadata: z.string().refine(
(value) => {
if (value === "") return true;
try {
JSON.parse(value);
return true;
} catch (error) {
return false;
}
},
{
message:
"Invalid input. Please provide a JSON object or double-quoted string.",
},
),
});
export const NewDatasetItemForm = (props: {
@@ -64,6 +79,7 @@ export const NewDatasetItemForm = (props: {
observationId?: string;
input?: Prisma.JsonValue;
output?: Prisma.JsonValue;
metadata?: Prisma.JsonValue;
datasetId?: string;
className?: string;
onFormSuccess?: () => void;
@@ -76,6 +92,7 @@ export const NewDatasetItemForm = (props: {
datasetId: props.datasetId ?? "",
input: props.input ? JSON.stringify(props.input, null, 2) : "",
expectedOutput: props.output ? JSON.stringify(props.output, null, 2) : "",
metadata: props.metadata ? JSON.stringify(props.metadata, null, 2) : "",
},
});
@@ -141,7 +158,7 @@ export const NewDatasetItemForm = (props: {
</FormItem>
)}
/>
<div className="grid flex-1 content-stretch gap-4 md:grid-cols-2">
<div className="grid gap-4 md:grid-cols-2">
<FormField
control={form.control}
name="input"
@@ -175,10 +192,26 @@ export const NewDatasetItemForm = (props: {
)}
/>
</div>
<FormField
control={form.control}
name="metadata"
render={({ field }) => (
<FormItem className="flex flex-col gap-2">
<FormLabel>Metadata</FormLabel>
<FormControl>
<JsonEditor
defaultValue={field.value}
onChange={field.onChange}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button
type="submit"
loading={createDatasetItemMutation.isLoading}
className="w-full"
className="mt-auto w-full"
>
Add to dataset
</Button>
@@ -18,7 +18,7 @@ import {
} from "@/src/components/ui/dropdown-menu";
import Link from "next/link";
import { NewDatasetItemForm } from "@/src/features/datasets/components/NewDatasetItemForm";
import { type Prisma } from "@langfuse/shared/src/db";
import { type Prisma } from "@langfuse/shared";
import { useHasAccess } from "@/src/features/rbac/utils/checkAccess";
import { useSession } from "next-auth/react";
@@ -28,6 +28,7 @@ export const NewDatasetItemFromTrace = (props: {
observationId?: string;
input: Prisma.JsonValue;
output: Prisma.JsonValue;
metadata: Prisma.JsonValue;
}) => {
const [open, setOpen] = useState(false);
const session = useSession();
@@ -111,6 +112,7 @@ export const NewDatasetItemFromTrace = (props: {
projectId={props.projectId}
input={props.input}
output={props.output}
metadata={props.metadata}
onFormSuccess={() => setOpen(false)}
className="h-full overflow-y-auto"
/>
@@ -43,6 +43,7 @@ export const datasetRouter = createTRPCRouter({
"datasets.id",
"datasets.name",
"datasets.description",
"datasets.metadata",
"datasets.created_at as createdAt",
"datasets.updated_at as updatedAt",
eb.fn.count("dataset_items.id").distinct().as("countDatasetItems"),
@@ -295,6 +296,7 @@ export const datasetRouter = createTRPCRouter({
datasetItemId: z.string(),
input: z.string().optional(),
expectedOutput: z.string().optional(),
metadata: z.string().optional(),
sourceTraceId: z.string().optional(),
sourceObservationId: z.string().optional(),
status: z.enum(["ACTIVE", "ARCHIVED"]).optional(),
@@ -327,6 +329,12 @@ export const datasetRouter = createTRPCRouter({
: input.expectedOutput !== undefined
? (JSON.parse(input.expectedOutput) as Prisma.InputJsonObject)
: undefined,
metadata:
input.metadata === ""
? Prisma.DbNull
: input.metadata !== undefined
? (JSON.parse(input.metadata) as Prisma.InputJsonObject)
: undefined,
sourceTraceId: input.sourceTraceId,
sourceObservationId: input.sourceObservationId,
status: input.status,
@@ -348,6 +356,7 @@ export const datasetRouter = createTRPCRouter({
projectId: z.string(),
name: z.string(),
description: z.string().nullish(),
metadata: z.string().nullish(),
}),
)
.mutation(async ({ input, ctx }) => {
@@ -361,6 +370,12 @@ export const datasetRouter = createTRPCRouter({
name: input.name,
description: input.description ?? undefined,
projectId: input.projectId,
metadata:
input.metadata === ""
? Prisma.DbNull
: !!input.metadata
? (JSON.parse(input.metadata) as Prisma.InputJsonObject)
: undefined,
},
});
@@ -382,6 +397,7 @@ export const datasetRouter = createTRPCRouter({
datasetId: z.string(),
name: z.string().nullish(),
description: z.string().nullish(),
metadata: z.string().nullish(),
}),
)
.mutation(async ({ input, ctx }) => {
@@ -398,6 +414,12 @@ export const datasetRouter = createTRPCRouter({
data: {
name: input.name ?? undefined,
description: input.description,
metadata:
input.metadata === ""
? Prisma.DbNull
: !!input.metadata
? (JSON.parse(input.metadata) as Prisma.InputJsonObject)
: undefined,
},
});
await auditLog({
@@ -442,6 +464,7 @@ export const datasetRouter = createTRPCRouter({
datasetId: z.string(),
input: z.string().nullish(),
expectedOutput: z.string().nullish(),
metadata: z.string().nullish(),
sourceTraceId: z.string().optional(),
sourceObservationId: z.string().optional(),
}),
@@ -476,6 +499,12 @@ export const datasetRouter = createTRPCRouter({
: !!input.expectedOutput
? (JSON.parse(input.expectedOutput) as Prisma.InputJsonObject)
: undefined,
metadata:
input.metadata === ""
? Prisma.DbNull
: !!input.metadata
? (JSON.parse(input.metadata) as Prisma.InputJsonObject)
: undefined,
datasetId: input.datasetId,
sourceTraceId: input.sourceTraceId,
sourceObservationId: input.sourceObservationId,
@@ -5,13 +5,13 @@ import { useRouter } from "next/router";
import { EvalConfigForm } from "@/src/features/evals/components/eval-config-form";
import { useHasAccess } from "@/src/features/rbac/utils/checkAccess";
import { Button } from "@/src/components/ui/button";
import { Pencil } from "lucide-react";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/src/components/ui/popover";
import { useState } from "react";
import { Trash2 } from "lucide-react";
export const EvalConfigDetail = () => {
const router = useRouter();
@@ -95,11 +95,16 @@ export function DeactivateConfig({
});
const onClick = () => {
if (!projectId) {
console.error("Project ID is missing");
return;
}
mutEvalConfig.mutateAsync({
projectId,
evalConfigId: config?.id ?? "",
updatedStatus: "INACTIVE",
});
setIsOpen(false);
};
return (
@@ -108,11 +113,10 @@ export function DeactivateConfig({
<Button
variant="ghost"
size={"sm"}
onClick={() => void onClick()}
disabled={!hasAccess || config?.status !== "ACTIVE"}
loading={isLoading}
>
<Pencil className="h-5 w-5" />
<Trash2 className="h-5 w-5" />
</Button>
</PopoverTrigger>
<PopoverContent>
@@ -126,14 +130,7 @@ export function DeactivateConfig({
type="button"
variant="destructive"
loading={mutEvalConfig.isLoading}
onClick={() => {
if (!projectId) {
console.error("Project ID is missing");
return;
}
void onClick();
setIsOpen(false);
}}
onClick={onClick}
>
Deactivate Eval Job
</Button>
@@ -52,7 +52,7 @@ export const DetailPageNav = (props: {
if (ids.length > 1)
return (
<div>
<div className="flex flex-row gap-2">
<Tooltip>
<TooltipTrigger asChild>
<Button
@@ -69,7 +69,6 @@ export const DetailPageNav = (props: {
);
}
}}
className="mr-2"
>
<ChevronUp className="h-4 w-4" />
</Button>
@@ -0,0 +1,67 @@
import type { ValidatedChatCompletionBody } from "@/src/features/playground/server/validateChatCompletionBody";
import { ServerPosthog } from "@/src/server/services/posthog";
import type { LLMResult } from "@langchain/core/outputs";
import { BaseCallbackHandler } from "@langchain/core/callbacks/base";
import type { ChatMessage, ModelParams } from "@langfuse/shared";
export class PosthogCallbackHandler extends BaseCallbackHandler {
public name = "PosthogCallbackHandler";
private messages: ChatMessage[];
private modelParams: ModelParams;
private posthog: ServerPosthog;
constructor(
public eventPrefix: string,
public body: ValidatedChatCompletionBody,
private userId: string,
) {
super();
this.posthog = new ServerPosthog();
this.messages = body.messages;
this.modelParams = body.modelParams;
}
async handleLLMEnd(output: LLMResult) {
const outputString = output.generations[0][0].text;
const properties = this.getEventProperties(outputString);
this.captureEvent(properties);
await this.posthog.flushAsync();
}
private getInputLength() {
return this.messages.reduce(
(acc, message) => acc + message.content.length,
0,
);
}
private getEventProperties(output: string): ChatCompletionEventProperties {
return {
outputLength: output.length,
inputLength: this.getInputLength(),
modelProvider: this.modelParams.provider,
modelName: this.modelParams.model,
};
}
private captureEvent(properties: ChatCompletionEventProperties) {
this.posthog.capture({
event: this.eventPrefix + "_chat_completion",
distinctId: this.userId,
properties,
});
}
public async flushAsync() {
await this.posthog.flushAsync();
}
}
type ChatCompletionEventProperties = {
outputLength: number;
inputLength: number;
modelProvider: string;
modelName: string;
};
@@ -4,19 +4,23 @@ import { type NextRequest, NextResponse } from "next/server";
import { fetchLLMCompletion } from "@langfuse/shared";
import { PosthogCallbackHandler } from "./analytics/posthogCallback";
import {
validateChatCompletionBody,
type ValidatedChatCompletionBody,
} from "./validateChatCompletionBody";
import { getCookieName } from "@/src/server/utils/cookies";
import { env } from "@/src/env.mjs";
export default async function chatCompletionHandler(req: NextRequest) {
const token = await getToken({
req,
cookieName: getCookieName("next-auth.session-token"),
secret: env.NEXTAUTH_SECRET,
});
if (!token)
if (!token || !token.sub)
// sub is the user id
return NextResponse.json({ message: "Unauthenticated" }, { status: 401 });
if (req.method !== "POST")
@@ -47,7 +51,7 @@ export default async function chatCompletionHandler(req: NextRequest) {
messages,
modelParams,
streaming: true,
functionCall: undefined,
callbacks: [new PosthogCallbackHandler("playground", body, token.sub)],
});
return new StreamingTextResponse(stream);
@@ -0,0 +1,117 @@
import { z } from "zod";
import { auditLog } from "@/src/features/audit-logs/auditLog";
import { throwIfNoAccess } from "@/src/features/rbac/utils/checkAccess";
import {
createTRPCRouter,
protectedProjectProcedure,
} from "@/src/server/api/trpc";
import { decrypt, encrypt } from "@langfuse/shared/encryption";
import { posthogIntegrationFormSchema } from "@/src/features/posthog-integration/types";
import { TRPCError } from "@trpc/server";
export const posthogIntegrationRouter = createTRPCRouter({
get: protectedProjectProcedure
.input(z.object({ projectId: z.string() }))
.query(async ({ input, ctx }) => {
throwIfNoAccess({
session: ctx.session,
projectId: input.projectId,
scope: "integrations:CRUD",
});
try {
const dbConfig = await ctx.prisma.posthogIntegration.findFirst({
where: {
projectId: input.projectId,
},
});
if (!dbConfig) {
return null;
}
const { encryptedPosthogApiKey, ...config } = dbConfig;
return {
...config,
posthogApiKey: decrypt(encryptedPosthogApiKey),
};
} catch (e) {
console.error("posthog integration get", e);
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
});
}
}),
update: protectedProjectProcedure
.input(posthogIntegrationFormSchema.extend({ projectId: z.string() }))
.mutation(async ({ input, ctx }) => {
try {
throwIfNoAccess({
session: ctx.session,
projectId: input.projectId,
scope: "integrations:CRUD",
});
await auditLog({
session: ctx.session,
action: "update",
resourceType: "posthogIntegration",
resourceId: input.projectId,
});
const { posthogProjectApiKey, ...config } = input;
const encryptedPosthogApiKey = encrypt(posthogProjectApiKey);
await ctx.prisma.posthogIntegration.upsert({
where: {
projectId: input.projectId,
},
create: {
projectId: input.projectId,
posthogHostName: config.posthogHostname,
encryptedPosthogApiKey,
enabled: config.enabled,
},
update: {
encryptedPosthogApiKey,
posthogHostName: config.posthogHostname,
enabled: config.enabled,
},
});
} catch (e) {
console.log("posthog integration update", e);
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
});
}
}),
delete: protectedProjectProcedure
.input(z.object({ projectId: z.string() }))
.mutation(async ({ input, ctx }) => {
try {
throwIfNoAccess({
session: ctx.session,
projectId: input.projectId,
scope: "integrations:CRUD",
});
await auditLog({
session: ctx.session,
action: "delete",
resourceType: "posthogIntegration",
resourceId: input.projectId,
});
await ctx.prisma.posthogIntegration.delete({
where: {
projectId: input.projectId,
},
});
} catch (e) {
console.log("posthog integration delete", e);
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
});
}
}),
});
@@ -0,0 +1,9 @@
import { z } from "zod";
export const posthogIntegrationFormSchema = z.object({
posthogHostname: z.string().url(),
posthogProjectApiKey: z.string().refine((v) => v.startsWith("phc_"), {
message: "PostHog Project API Key must start with 'phc_'",
}),
enabled: z.boolean(),
});
@@ -50,7 +50,7 @@ export const CreatePromptTRPCSchema = z.union([
export type CreatePromptTRPCType = z.infer<typeof CreatePromptTRPCSchema>;
export const GetPromptSchema = z.object({
name: z.string(),
name: z.string().transform((v) => decodeURIComponent(v)),
version: z.coerce.number().int().nullish(),
});
@@ -20,6 +20,7 @@ const scopes = [
"project:delete",
"project:update",
"project:transfer",
"integrations:CRUD",
"datasets:CUD",
@@ -45,6 +46,7 @@ export const roleAccessRights: Record<MembershipRole, Scope[]> = {
"apiKeys:read",
"apiKeys:create",
"apiKeys:delete",
"integrations:CRUD",
"objects:publish",
"objects:bookmark",
"objects:tag",
@@ -70,6 +72,7 @@ export const roleAccessRights: Record<MembershipRole, Scope[]> = {
"apiKeys:read",
"apiKeys:create",
"apiKeys:delete",
"integrations:CRUD",
"objects:publish",
"objects:bookmark",
"objects:tag",
+3 -2
View File
@@ -2,11 +2,12 @@
import { useEffect } from "react";
import { Crisp } from "crisp-sdk-web";
import { env } from "@/src/env.mjs";
const CrispChat = () => {
useEffect(() => {
if (process.env.NEXT_PUBLIC_CRISP_WEBSITE_ID)
Crisp.configure(process.env.NEXT_PUBLIC_CRISP_WEBSITE_ID);
if (env.NEXT_PUBLIC_CRISP_WEBSITE_ID)
Crisp.configure(env.NEXT_PUBLIC_CRISP_WEBSITE_ID);
});
return null;
@@ -4,7 +4,7 @@ import { useHasAccess } from "@/src/features/rbac/utils/checkAccess";
import { type RouterOutput, type RouterInput } from "@/src/utils/types";
import TagManager from "@/src/features/tag/components/TagMananger";
type TagTracePopverProps = {
type TagTracePopoverProps = {
tags: string[];
availableTags: string[];
projectId: string;
@@ -12,13 +12,13 @@ type TagTracePopverProps = {
tracesFilter: RouterInput["traces"]["all"];
};
export function TagTracePopver({
export function TagTracePopover({
tags,
availableTags,
projectId,
traceId,
tracesFilter,
}: TagTracePopverProps) {
}: TagTracePopoverProps) {
const [isLoading, setIsLoading] = useState(false);
const hasAccess = useHasAccess({ projectId, scope: "objects:tag" });
+2 -9
View File
@@ -1,11 +1,8 @@
import { VERSION } from "@/src/constants";
import { ServerPosthog } from "@/src/server/services/posthog";
import { Prisma, prisma } from "@langfuse/shared/src/db";
import { PostHog } from "posthog-node";
import { v4 as uuidv4 } from "uuid";
// Safe as it is intended to be public
const POSTHOG_API_KEY = "phc_zkMwFajk8ehObUlMth0D7DtPItFnxETi3lmSvyQDrwB";
// Interval between jobs in milliseconds
const JOB_INTERVAL_MINUTES = Prisma.raw("60");
@@ -147,11 +144,7 @@ async function posthogTelemetry({
clientId: string;
}) {
try {
const posthog = new PostHog(POSTHOG_API_KEY, {
host: "https://eu.posthog.com",
});
if (process.env.NODE_ENV === "development") posthog.debug();
const posthog = new ServerPosthog();
// Count projects
const totalProjects = await prisma.project.count();
+5 -13
View File
@@ -29,6 +29,7 @@ import "core-js/features/array/to-sorted";
// Other CSS
import "react18-json-view/src/style.css";
import { DetailPageListsProvider } from "@/src/features/navigate-detail-pages/context";
import { env } from "@/src/env.mjs";
const setProjectInPosthog = () => {
// project
@@ -66,10 +67,7 @@ const MyApp: AppType<{ session: Session | null }> = ({
useEffect(() => {
// PostHog (cloud.langfuse.com)
if (
process.env.NEXT_PUBLIC_POSTHOG_KEY &&
process.env.NEXT_PUBLIC_POSTHOG_HOST
) {
if (env.NEXT_PUBLIC_POSTHOG_KEY && env.NEXT_PUBLIC_POSTHOG_HOST) {
const handleRouteChange = () => {
setProjectInPosthog();
posthog.capture("$pageview");
@@ -110,17 +108,14 @@ function UserTracking() {
useEffect(() => {
if (session.status === "authenticated") {
// PostHog
if (
process.env.NEXT_PUBLIC_POSTHOG_KEY &&
process.env.NEXT_PUBLIC_POSTHOG_HOST
)
if (env.NEXT_PUBLIC_POSTHOG_KEY && env.NEXT_PUBLIC_POSTHOG_HOST)
posthog.identify(session.data.user?.id ?? undefined, {
environment: process.env.NODE_ENV,
email: session.data.user?.email ?? undefined,
name: session.data.user?.name ?? undefined,
featureFlags: session.data.user?.featureFlags ?? undefined,
projects: session.data.user?.projects ?? undefined,
LANGFUSE_CLOUD_REGION: process.env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION,
LANGFUSE_CLOUD_REGION: env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION,
});
const emailDomain = session.data.user?.email?.split("@")[1];
if (emailDomain)
@@ -149,10 +144,7 @@ function UserTracking() {
});
} else {
// PostHog
if (
process.env.NEXT_PUBLIC_POSTHOG_KEY &&
process.env.NEXT_PUBLIC_POSTHOG_HOST
) {
if (env.NEXT_PUBLIC_POSTHOG_KEY && env.NEXT_PUBLIC_POSTHOG_HOST) {
posthog.reset();
posthog.resetGroups();
}
+2 -1
View File
@@ -84,7 +84,8 @@ export default async function handler(
if (
env.LANGFUSE_NEW_USER_SIGNUP_WEBHOOK &&
env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION &&
env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION !== "STAGING"
env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION !== "STAGING" &&
env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION !== "DEV"
) {
await fetch(env.LANGFUSE_NEW_USER_SIGNUP_WEBHOOK, {
method: "POST",
+6 -6
View File
@@ -1,7 +1,7 @@
import { env } from "@/src/env.mjs";
import { ServerPosthog } from "@/src/server/services/posthog";
import { prisma, Prisma } from "@langfuse/shared/src/db";
import { type NextApiRequest, type NextApiResponse } from "next";
import { PostHog } from "posthog-node";
export default async function handler(
req: NextApiRequest,
@@ -10,7 +10,10 @@ export default async function handler(
if (!process.env.NEXT_PUBLIC_POSTHOG_KEY)
return res.status(200).json({ message: "No PostHog key provided" });
if (env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION === undefined)
if (
env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION === undefined ||
env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION === "DEV"
)
return res.status(200).json({
message: "Only runs on Langfuse Cloud, no LANGFUSE_CLOUD_REGION provided",
});
@@ -26,10 +29,7 @@ export default async function handler(
: "langfuse-cloud-eu";
try {
const posthog = new PostHog(process.env.NEXT_PUBLIC_POSTHOG_KEY, {
host: "https://eu.posthog.com",
});
if (process.env.NODE_ENV === "development") posthog.debug();
const posthog = new ServerPosthog();
// Time frame is the last time this cron job ran until now
const startTimeframe =
@@ -11,7 +11,10 @@ const CreateDatasetItemSchema = z.object({
datasetName: z.string(),
input: jsonSchema.nullish(),
expectedOutput: jsonSchema.nullish(),
metadata: jsonSchema.nullish(),
id: z.string().nullish(),
sourceTraceId: z.string().nullish(),
sourceObservationId: z.string().nullish(),
});
export default async function handler(
@@ -72,10 +75,16 @@ export default async function handler(
input: itemBody.input ?? undefined,
expectedOutput: itemBody.expectedOutput ?? undefined,
datasetId: dataset.id,
metadata: itemBody.metadata ?? undefined,
sourceTraceId: itemBody.sourceTraceId ?? undefined,
sourceObservationId: itemBody.sourceObservationId ?? undefined,
},
update: {
input: itemBody.input ?? undefined,
expectedOutput: itemBody.expectedOutput ?? undefined,
metadata: itemBody.metadata ?? undefined,
sourceTraceId: itemBody.sourceTraceId ?? undefined,
sourceObservationId: itemBody.sourceObservationId ?? undefined,
},
});
+8 -2
View File
@@ -4,11 +4,12 @@ import { cors, runMiddleware } from "@/src/features/public-api/server/cors";
import { prisma } from "@langfuse/shared/src/db";
import { verifyAuthHeaderAndReturnScope } from "@/src/features/public-api/server/apiAuth";
import { isPrismaException } from "@/src/utils/exceptions";
import { paginationZod } from "@/src/utils/zod";
import { jsonSchema, paginationZod } from "@/src/utils/zod";
const CreateDatasetSchema = z.object({
name: z.string(),
description: z.string().nullish(),
metadata: jsonSchema.nullish(),
});
const GetDatasetsSchema = z.object({
@@ -40,7 +41,9 @@ export default async function handler(
JSON.stringify(req.body, null, 2),
);
const { name, description } = CreateDatasetSchema.parse(req.body);
const { name, description, metadata } = CreateDatasetSchema.parse(
req.body,
);
// CHECK ACCESS SCOPE
if (authCheck.scope.accessLevel !== "all") {
@@ -61,9 +64,11 @@ export default async function handler(
name,
description: description ?? undefined,
projectId: authCheck.scope.projectId,
metadata: metadata ?? undefined,
},
update: {
description: description ?? null,
metadata: metadata ?? undefined,
},
});
@@ -84,6 +89,7 @@ export default async function handler(
select: {
name: true,
description: true,
metadata: true,
projectId: true,
createdAt: true,
updatedAt: true,
@@ -6,7 +6,7 @@ import { verifyAuthHeaderAndReturnScope } from "@/src/features/public-api/server
import { isPrismaException } from "@/src/utils/exceptions";
const DatasetsGetSchema = z.object({
name: z.string(),
name: z.string().transform((val) => decodeURIComponent(val)),
});
export default async function handler(
@@ -6,8 +6,8 @@ import { verifyAuthHeaderAndReturnScope } from "@/src/features/public-api/server
import { isPrismaException } from "@/src/utils/exceptions";
const DatasetRunsGetSchema = z.object({
name: z.string(),
runName: z.string(),
name: z.string().transform((val) => decodeURIComponent(val)),
runName: z.string().transform((val) => decodeURIComponent(val)),
});
export default async function handler(
+2 -1
View File
@@ -427,7 +427,8 @@ export const sendToWorkerIfEnvironmentConfigured = async (
if (
env.LANGFUSE_WORKER_HOST &&
env.LANGFUSE_WORKER_PASSWORD &&
env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION
env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION &&
env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION !== "DEV"
) {
const traceEvents = batchResults
.filter((result) => result.type === eventTypes.TRACE_CREATE) // we only have create, no update.
@@ -108,6 +108,7 @@ export default async function handler(
SELECT
DATE_TRUNC('DAY', t.timestamp) "date",
count(distinct t.id)::integer count_traces,
count(distinct o.id)::integer count_observations,
SUM(o.calculated_total_cost)::DOUBLE PRECISION total_cost
FROM traces t
LEFT JOIN observations_view o ON o.project_id = t.project_id AND t.id = o.trace_id
@@ -122,6 +123,7 @@ export default async function handler(
SELECT
TO_CHAR(COALESCE(ds.date, daily_model_usage.date), 'YYYY-MM-DD') AS "date",
COALESCE(count_traces, 0) "countTraces",
COALESCE(count_observations, 0) "countObservations",
COALESCE(total_cost, 0) "totalCost",
COALESCE(daily_usage_json, '[]'::JSON) usage
FROM
+32 -1
View File
@@ -1,4 +1,4 @@
import { prisma } from "@langfuse/shared/src/db";
import { ScoreSource, prisma } from "@langfuse/shared/src/db";
import { Prisma, type Score } from "@langfuse/shared/src/db";
import { type NextApiRequest, type NextApiResponse } from "next";
import { z } from "zod";
@@ -18,11 +18,25 @@ import {
} from "@/src/pages/api/public/ingestion";
import { isPrismaException } from "@/src/utils/exceptions";
const operators = ["<", ">", "<=", ">=", "!=", "="] as const;
const prismaOperators: Record<(typeof operators)[number], string> = {
"<": "lt",
">": "gt",
"<=": "lte",
">=": "gte",
"!=": "not",
"=": "equals",
};
const ScoresGetSchema = z.object({
...paginationZod,
userId: z.string().nullish(),
name: z.string().nullish(),
fromTimestamp: stringDate,
source: z.nativeEnum(ScoreSource).nullish(),
value: z.coerce.number().nullish(),
operator: z.enum(operators).nullish(),
});
export default async function handler(
@@ -105,6 +119,13 @@ export default async function handler(
const fromTimestampCondition = obj.fromTimestamp
? Prisma.sql`AND t."timestamp" >= ${obj.fromTimestamp}::timestamp with time zone at time zone 'UTC'`
: Prisma.empty;
const sourceCondition = obj.source
? Prisma.sql`AND s."source" = ${obj.source}`
: Prisma.empty;
const valueCondition =
obj.operator && obj.value !== null && obj.value !== undefined
? Prisma.sql`AND s."value" ${Prisma.raw(`${obj.operator}`)} ${obj.value}`
: Prisma.empty;
const scores = await prisma.$queryRaw<
Array<Score & { trace: { userId: string } }>
@@ -114,6 +135,7 @@ export default async function handler(
s.timestamp,
s.name,
s.value,
s.source,
s.comment,
s.trace_id as "traceId",
s.observation_id as "observationId",
@@ -123,16 +145,25 @@ export default async function handler(
WHERE t.project_id = ${authCheck.scope.projectId}
${userCondition}
${nameCondition}
${sourceCondition}
${fromTimestampCondition}
${valueCondition}
ORDER BY t."timestamp" DESC
LIMIT ${obj.limit} OFFSET ${skipValue}
`);
const totalItems = await prisma.score.count({
where: {
name: obj.name ? obj.name : undefined,
source: obj.source ? obj.source : undefined,
timestamp: obj.fromTimestamp
? { gte: new Date(obj.fromTimestamp) }
: undefined,
value:
obj.operator && obj.value
? {
[prismaOperators[obj.operator]]: obj.value,
}
: undefined,
trace: {
projectId: authCheck.scope.projectId,
userId: obj.userId ? obj.userId : undefined,
+29 -2
View File
@@ -19,7 +19,7 @@ import { TbBrandAzure } from "react-icons/tb";
import { signIn } from "next-auth/react";
import Head from "next/head";
import Link from "next/link";
import { useState } from "react";
import { useState, useEffect } from "react";
import { useForm } from "react-hook-form";
import * as z from "zod";
import { usePostHog } from "posthog-js/react";
@@ -30,6 +30,8 @@ import { PasswordInput } from "@/src/components/ui/password-input";
import { Turnstile } from "@marsidev/react-turnstile";
import { isAnySsoConfigured } from "@langfuse/ee/sso";
import { Shield } from "lucide-react";
import { useRouter } from "next/router";
import { captureException } from "@sentry/nextjs";
const credentialAuthForm = z.object({
email: z.string().email(),
@@ -169,10 +171,35 @@ export function SSOButtons({
);
}
const signInErrors = [
{
code: "OAuthAccountNotLinked",
description:
"Please sign in with the same provider that you used to create this account.",
},
];
export default function SignIn({ authProviders, signUpDisabled }: PageProps) {
const router = useRouter();
// handle NextAuth error codes: https://next-auth.js.org/configuration/pages#sign-in-page
const nextAuthError =
typeof router.query.error === "string"
? decodeURIComponent(router.query.error)
: null;
const nextAuthErrorDescription = signInErrors.find(
(e) => e.code === nextAuthError,
)?.description;
useEffect(() => {
// log unexpected sign in errors to Sentry
if (nextAuthError && !nextAuthErrorDescription) {
captureException(new Error(`Sign in error: ${nextAuthError}`));
}
}, [nextAuthError, nextAuthErrorDescription]);
const [credentialsFormError, setCredentialsFormError] = useState<
string | null
>(null);
>(nextAuthErrorDescription ?? nextAuthError);
const [ssoLoading, setSsoLoading] = useState<boolean>(false);
const posthog = usePostHog();
+5 -3
View File
@@ -63,9 +63,11 @@ export default function SignIn({ authProviders }: PageProps) {
await signIn<"credentials">("credentials", {
email: values.email,
password: values.password,
callbackUrl: env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION
? "/onboarding"
: "/?getStarted=1",
callbackUrl:
env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION &&
env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION !== "DEV"
? "/onboarding"
: "/?getStarted=1",
turnstileToken,
});
} catch (err) {
@@ -7,6 +7,7 @@ import Link from "next/link";
import { DetailPageNav } from "@/src/features/navigate-detail-pages/DetailPageNav";
import { DatasetActionButton } from "@/src/features/datasets/components/DatasetActionButton";
import { DeleteButton } from "@/src/components/deleteButton";
import { JSONView } from "@/src/components/ui/CodeJsonViewer";
export default function Dataset() {
const router = useRouter();
@@ -47,6 +48,7 @@ export default function Dataset() {
datasetId={datasetId}
datasetName={dataset.data?.name ?? ""}
datasetDescription={dataset.data?.description ?? undefined}
datasetMetadata={dataset.data?.metadata}
icon
/>
<DeleteButton
@@ -61,18 +63,28 @@ export default function Dataset() {
</>
}
/>
<Tabs value="runs" className="mb-3">
<TabsList>
<TabsTrigger value="runs">Runs</TabsTrigger>
<TabsTrigger value="items" asChild>
<Link href={`/project/${projectId}/datasets/${datasetId}/items`}>
Items
</Link>
</TabsTrigger>
</TabsList>
</Tabs>
{!!dataset.data?.metadata && (
<JSONView json={dataset?.data.metadata} title="Metadata" />
)}
<DatasetRunsTable projectId={projectId} datasetId={datasetId} />
<DatasetRunsTable
projectId={projectId}
datasetId={datasetId}
menuItems={
<Tabs value="runs">
<TabsList>
<TabsTrigger value="runs">Runs</TabsTrigger>
<TabsTrigger value="items" asChild>
<Link
href={`/project/${projectId}/datasets/${datasetId}/items`}
>
Items
</Link>
</TabsTrigger>
</TabsList>
</Tabs>
}
/>
<p className="mt-3 text-xs text-gray-600">
Add new runs via Python or JS/TS SDKs. See{" "}
@@ -8,6 +8,7 @@ import { DetailPageNav } from "@/src/features/navigate-detail-pages/DetailPageNa
import { DatasetActionButton } from "@/src/features/datasets/components/DatasetActionButton";
import { DeleteButton } from "@/src/components/deleteButton";
import { NewDatasetItemButton } from "@/src/features/datasets/components/NewDatasetItemButton";
import { JSONView } from "@/src/components/ui/CodeJsonViewer";
export default function DatasetItems() {
const router = useRouter();
@@ -55,6 +56,7 @@ export default function DatasetItems() {
datasetId={datasetId}
datasetName={dataset.data?.name ?? ""}
datasetDescription={dataset.data?.description ?? undefined}
datasetMetadata={dataset.data?.metadata}
icon
/>
<DeleteButton
@@ -69,18 +71,27 @@ export default function DatasetItems() {
</>
}
/>
<Tabs value="items" className="mb-3">
<TabsList>
<TabsTrigger value="runs" asChild>
<Link href={`/project/${projectId}/datasets/${datasetId}`}>
Runs
</Link>
</TabsTrigger>
<TabsTrigger value="items">Items</TabsTrigger>
</TabsList>
</Tabs>
<DatasetItemsTable projectId={projectId} datasetId={datasetId} />
{!!dataset.data?.metadata && (
<JSONView json={dataset?.data.metadata} title="Metadata" />
)}
<DatasetItemsTable
projectId={projectId}
datasetId={datasetId}
menuItems={
<Tabs value="items">
<TabsList>
<TabsTrigger value="runs" asChild>
<Link href={`/project/${projectId}/datasets/${datasetId}`}>
Runs
</Link>
</TabsTrigger>
<TabsTrigger value="items">Items</TabsTrigger>
</TabsList>
</Tabs>
}
/>
</div>
);
}
@@ -44,19 +44,14 @@ export default function Dataset() {
/>
}
/>
{!!run.data?.description && (
<>
<Header title="Description" level="h3" />
<JSONView json={run.data.description} />
</>
)}
{!!run.data?.metadata && (
<>
<Header title="Metadata" level="h3" />
<JSONView json={run.data.metadata} />
</>
)}
<Header title="Runs" level="h3" />
<div className="flex flex-col gap-2">
{!!run.data?.description && (
<JSONView json={run.data.description} title="Description" />
)}
{!!run.data?.metadata && (
<JSONView json={run.data.metadata} title="Metadata" />
)}
</div>
<DatasetRunItemsTable
projectId={projectId}
datasetId={datasetId}
@@ -15,6 +15,7 @@ export default function PlaygroundPage() {
help={{
description: "A sandbox to test and iterate your prompts",
}}
featureBetaURL="https://github.com/orgs/langfuse/discussions/1170"
/>
<div className="flex-1 overflow-auto">
<PlaygroundProvider>
+28 -23
View File
@@ -1,6 +1,6 @@
import { ChevronRightIcon } from "@heroicons/react/20/solid";
import { CommandLineIcon, RocketLaunchIcon } from "@heroicons/react/24/outline";
import { SiPython } from "react-icons/si";
import { SiOpenai, SiPython } from "react-icons/si";
import Header from "@/src/components/layouts/header";
import { ApiKeyList } from "@/src/features/public-api/components/ApiKeyList";
import { useRouter } from "next/router";
@@ -15,10 +15,6 @@ import { env } from "@/src/env.mjs";
import { Card } from "@tremor/react";
import { Button } from "@/src/components/ui/button";
import Link from "next/link";
import {
sendUserChatMessage,
showAgentChatMessage,
} from "@/src/features/support-chat/chat";
export default function SettingsPage() {
const router = useRouter();
@@ -31,7 +27,7 @@ export default function SettingsPage() {
<ApiKeyList projectId={projectId} />
<ProjectMembersTable projectId={projectId} />
<ProjectUsageChart projectId={projectId} />
<Beta />
<Integrations projectId={projectId} />
<Instructions />
<RenameProject projectId={projectId} />
<div className="space-y-3">
@@ -58,23 +54,36 @@ const instructionItems = [
icon: RocketLaunchIcon,
},
{
name: "Langchain integration",
name: "OpenAI SDK Integration",
description: "Trace your OpenAI API calls with a single line of code",
href: "https://langfuse.com/docs/integrations/openai",
icon: SiOpenai,
},
{
name: "Langchain Integration",
description:
"Trace your Langchain llm/chain/agent/... with a single line of code",
href: "https://langfuse.com/docs/langchain",
href: "https://langfuse.com/docs/integrations/langchain",
icon: Bird,
},
{
name: "LlamaIndex Integration",
description:
"Trace your Llamaindex RAG application by adding the global callback handler",
href: "https://langfuse.com/docs/integrations/llama-index",
icon: Code,
},
{
name: "Typescript SDK",
description: "npm install langfuse",
href: "https://langfuse.com/docs/sdk/typescript",
icon: CommandLineIcon,
icon: Code,
},
{
name: "Python SDK",
name: "Python SDK (Decorator)",
description: "pip install langfuse",
href: "https://langfuse.com/docs/sdk/python",
icon: SiPython,
icon: Code,
},
{
name: "API Reference (Swagger)",
@@ -123,12 +132,12 @@ function Instructions() {
);
}
const Beta = () => {
const Integrations = (props: { projectId: string }) => {
if (env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION === undefined) return null;
return (
<div>
<Header title="Early Access" level="h3" />
<Header title="Integrations" level="h3" />
<Card className="p-4 lg:w-1/2">
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
@@ -141,16 +150,12 @@ const Beta = () => {
Langfuse Events/Metrics available in your Posthog Dashboards.
</p>
<div className="flex items-center gap-2">
<Button
variant="secondary"
onClick={() => {
sendUserChatMessage(
"I am interested to join the PostHog Integration Beta",
);
showAgentChatMessage("We'll be in touch to get you set up!");
}}
>
Get Access
<Button variant="secondary" asChild>
<Link
href={`/project/${props.projectId}/settings/posthog-integration`}
>
Configure
</Link>
</Button>
<Button asChild variant="ghost">
<Link href="https://langfuse.com/docs/analytics/posthog">
@@ -0,0 +1,222 @@
import Header from "@/src/components/layouts/header";
import { Button } from "@/src/components/ui/button";
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/src/components/ui/form";
import { Input } from "@/src/components/ui/input";
import { PasswordInput } from "@/src/components/ui/password-input";
import { Switch } from "@/src/components/ui/switch";
import { env } from "@/src/env.mjs";
import { posthogIntegrationFormSchema } from "@/src/features/posthog-integration/types";
import { api } from "@/src/utils/api";
import { type RouterOutput } from "@/src/utils/types";
import { zodResolver } from "@hookform/resolvers/zod";
import { Card } from "@tremor/react";
import Link from "next/link";
import { useRouter } from "next/router";
import posthog from "posthog-js";
import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { type z } from "zod";
export default function PosthogIntegrationSettings() {
const router = useRouter();
const projectId = router.query.projectId as string;
const state = api.posthogIntegration.get.useQuery({ projectId });
if (env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION === undefined) return null;
return (
<div className="md:container">
<Header
title="PostHog Integration"
breadcrumb={[
{ name: "Settings", href: `/project/${projectId}/settings` },
]}
actionButtons={
<Button asChild variant="secondary">
<Link href="https://langfuse.com/docs/analytics/posthog">
Integration Docs
</Link>
</Button>
}
status={state.data?.enabled ? "active" : "inactive"}
/>
<p className="mb-4 text-sm text-gray-700">
We have teamed up with{" "}
<Link href="https://posthog.com" className="underline">
PostHog
</Link>{" "}
(OSS product analytics) to make Langfuse events/metrics available in
your Posthog Dashboards. While in Beta, this integration syncs metrics
on a daily schedule to PostHog. When first activated, it will sync all
historical data from the beginning of your project.
</p>
<div className="flex flex-col gap-10"></div>
{!state.isInitialLoading && (
<>
<Header level="h3" title="Configuration" />
<Card className="p-4">
<PostHogIntegrationSettings
state={state.data}
projectId={projectId}
/>
</Card>
</>
)}
{state.data?.enabled && (
<>
<Header level="h3" title="Status" className="mt-8" />
<p className="text-sm text-gray-700">
Data synced until:{" "}
{state.data?.lastSyncAt
? new Date(state.data.lastSyncAt).toLocaleString()
: "Never (pending)"}
</p>
<p className="mt-2 text-sm text-gray-700">
While in Beta, the sync is scheduled to run once a day.
</p>
</>
)}
</div>
);
}
const PostHogIntegrationSettings = ({
state,
projectId,
}: {
state?: RouterOutput["posthogIntegration"]["get"];
projectId: string;
}) => {
const posthogForm = useForm<z.infer<typeof posthogIntegrationFormSchema>>({
resolver: zodResolver(posthogIntegrationFormSchema),
defaultValues: {
posthogHostname: state?.posthogHostName ?? "",
posthogProjectApiKey: state?.posthogApiKey ?? "",
enabled: state?.enabled ?? false,
},
});
useEffect(() => {
posthogForm.reset({
posthogHostname: state?.posthogHostName ?? "",
posthogProjectApiKey: state?.posthogApiKey ?? "",
enabled: state?.enabled ?? false,
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [state]);
const utils = api.useUtils();
const mut = api.posthogIntegration.update.useMutation({
onSuccess: () => {
utils.posthogIntegration.invalidate();
},
});
const mutDelete = api.posthogIntegration.delete.useMutation({
onSuccess: () => {
utils.posthogIntegration.invalidate();
},
});
async function onSubmit(
values: z.infer<typeof posthogIntegrationFormSchema>,
) {
posthog.capture("integrations:posthog_form_submitted");
mut.mutate({
projectId,
...values,
});
console.log(values);
}
return (
<Form {...posthogForm}>
<form
className="space-y-3"
// eslint-disable-next-line @typescript-eslint/no-misused-promises
onSubmit={posthogForm.handleSubmit(onSubmit)}
>
<FormField
control={posthogForm.control}
name="posthogHostname"
render={({ field }) => (
<FormItem>
<FormLabel>Posthog Hostname</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormDescription>
US region: https://us.posthog.com; EU region:
https://eu.posthog.com
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={posthogForm.control}
name="posthogProjectApiKey"
render={({ field }) => (
<FormItem>
<FormLabel>Posthog Project API Key</FormLabel>
<FormControl>
<PasswordInput {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={posthogForm.control}
name="enabled"
render={({ field }) => (
<FormItem>
<FormLabel>Enabled</FormLabel>
<FormControl>
<Switch
id="posthog-integration-enabled"
checked={field.value}
onCheckedChange={() => {
field.onChange(!field.value);
}}
className="ml-4 mt-1"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</form>
<div className="mt-8 flex gap-2">
<Button
loading={mut.isLoading}
onClick={posthogForm.handleSubmit(onSubmit)}
>
Save
</Button>
<Button
variant="ghost"
loading={mutDelete.isLoading}
onClick={() => {
if (
confirm(
"Are you sure you want to reset the PostHog integration for this project?",
)
)
mutDelete.mutate({ projectId });
}}
>
Reset
</Button>
</div>
</Form>
);
};
+2
View File
@@ -15,6 +15,7 @@ import { sessionRouter } from "@/src/server/api/routers/sessions";
import { promptRouter } from "@/src/features/prompts/server/prompt-router";
import { modelRouter } from "@/src/server/api/routers/models";
import { evalRouter } from "@/src/features/evals/server/router";
import { posthogIntegrationRouter } from "@/src/features/posthog-integration/posthog-integration-router";
/**
* This is the primary router for your server.
@@ -38,6 +39,7 @@ export const appRouter = createTRPCRouter({
prompts: promptRouter,
models: modelRouter,
evals: evalRouter,
posthogIntegration: posthogIntegrationRouter,
});
// export type definition of API
+14 -12
View File
@@ -19,17 +19,19 @@ export const userRouter = createTRPCRouter({
all: protectedProjectProcedure
.input(UserAllOptions)
.query(async ({ input, ctx }) => {
const uniqueUsers = await ctx.prisma.trace.findMany({
where: {
projectId: input.projectId,
},
select: {
userId: true,
},
distinct: ["userId"],
});
const totalUsers = (
await ctx.prisma.$queryRaw<
Array<{
totalCount: number;
}>
>`
SELECT COUNT(DISTINCT t.user_id)::int AS "totalCount"
FROM traces t
WHERE t.project_id = ${input.projectId}
`
)[0].totalCount;
const topUsers = await ctx.prisma.$queryRaw<
const users = await ctx.prisma.$queryRaw<
Array<{
userId: string;
totalTraces: number;
@@ -52,8 +54,8 @@ export const userRouter = createTRPCRouter({
${input.limit} OFFSET ${input.page * input.limit};
`;
return {
totalUsers: uniqueUsers.length,
users: topUsers,
totalUsers,
users,
};
}),
+2 -1
View File
@@ -324,7 +324,8 @@ export async function getAuthOptions(): Promise<NextAuthOptions> {
if (
env.LANGFUSE_NEW_USER_SIGNUP_WEBHOOK &&
env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION &&
env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION !== "STAGING"
env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION !== "STAGING" &&
env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION !== "DEV"
) {
await fetch(env.LANGFUSE_NEW_USER_SIGNUP_WEBHOOK, {
method: "POST",
+18
View File
@@ -0,0 +1,18 @@
import { env } from "@/src/env.mjs";
import { PostHog as OriginalPosthog } from "posthog-node";
// Safe as it is intended to be public
const PUBLIC_POSTHOG_API_KEY =
env.NEXT_PUBLIC_POSTHOG_KEY ||
"phc_zkMwFajk8ehObUlMth0D7DtPItFnxETi3lmSvyQDrwB";
const POSTHOG_HOST = env.NEXT_PUBLIC_POSTHOG_HOST || "https://eu.posthog.com";
export class ServerPosthog extends OriginalPosthog {
constructor() {
super(PUBLIC_POSTHOG_API_KEY, {
host: POSTHOG_HOST,
});
if (process.env.NODE_ENV === "development") this.debug();
}
}
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "worker",
"version": "2.28.0",
"version": "2.30.2",
"description": "",
"license": "MIT",
"main": "index.js",
@@ -50,7 +50,7 @@
"@types/lodash": "^4.14.202",
"@types/node": "^20.11.19",
"@types/pg": "^8.11.5",
"@typescript-eslint/parser": "^7.6.0",
"@typescript-eslint/parser": "^7.7.0",
"eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.0",
"eslint-config-standard": "^17.1.0",
@@ -62,6 +62,6 @@
"tsc-watch": "^6.2.0",
"typescript": "^5.4.5",
"vite": "^5.2.9",
"vitest": "^1.3.1"
"vitest": "^1.5.0"
}
}
+1 -1
View File
@@ -1 +1 @@
export const VERSION = "v2.28.0";
export const VERSION = "v2.30.2";