Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8995b30959 | ||
|
|
321a25cf13 | ||
|
|
2990767c4a | ||
|
|
ad10e6f76f | ||
|
|
1f9adec948 | ||
|
|
a158d6da09 | ||
|
|
e6f9e5e06d | ||
|
|
c2463cb91e | ||
|
|
38e472ec66 | ||
|
|
616e79238b | ||
|
|
e2a59a2639 | ||
|
|
07609ade4f | ||
|
|
6627c59db4 | ||
|
|
70e1426790 | ||
|
|
8550d7313f | ||
|
|
1d76ef3a18 | ||
|
|
b68d15309b | ||
|
|
c7b3e95645 | ||
|
|
2c55cd842a | ||
|
|
dc4e217a6e | ||
|
|
90f89f8b33 | ||
|
|
2cbcf1fc02 | ||
|
|
04251e2451 | ||
|
|
b3ff1e0bd1 | ||
|
|
9fc1f79b94 | ||
|
|
06bf8302d6 | ||
|
|
7acc8e1f4d | ||
|
|
b88809f7e5 | ||
|
|
7ee5c2d573 | ||
|
|
702a591a0d | ||
|
|
6021ef94c8 | ||
|
|
d2d10893e1 | ||
|
|
dd78b9be7e | ||
|
|
ec1a9c53e7 | ||
|
|
74da65f9b2 | ||
|
|
654620c336 | ||
|
|
d1b935b280 | ||
|
|
7dfa914934 | ||
|
|
a126b05bb9 | ||
|
|
b81a81d76f | ||
|
|
15014eb8ea | ||
|
|
e1fb0e4102 | ||
|
|
c7d99476eb | ||
|
|
7e3980856c | ||
|
|
54991e2246 | ||
|
|
10bbfd75c3 | ||
|
|
4476d6c1e7 | ||
|
|
9dc758693b | ||
|
|
082031ccd2 | ||
|
|
a88764db7e | ||
|
|
b6e0acb6f3 | ||
|
|
366ccde902 | ||
|
|
21c60fab76 | ||
|
|
7c5a49b4ee | ||
|
|
bfbb83e74f | ||
|
|
8cde89689f | ||
|
|
823d94e6fd | ||
|
|
2bbb88ce93 | ||
|
|
52cfba89e8 | ||
|
|
2de3d6fe8f | ||
|
|
f873a7dc97 | ||
|
|
340e8927bd | ||
|
|
cf0ea0f9fb | ||
|
|
d46715ff79 | ||
|
|
73d06086b6 | ||
|
|
e728a73f29 | ||
|
|
ec3c946965 |
@@ -80,6 +80,20 @@ LANGFUSE_CSP_ENFORCE_HTTPS="true"
|
||||
# ENABLE_EVENT_LOG=false
|
||||
|
||||
|
||||
|
||||
### START Envs to be deprecated in Langfuse v3.0
|
||||
|
||||
# Disable the expensive analytics queries and related features
|
||||
# LANGFUSE_DISABLE_EXPENSIVE_POSTGRES_QUERIES="true"
|
||||
|
||||
# Customize the default datetime offset filter used in for the UI tables
|
||||
# LANGFUSE_DEFAULT_TABLE_DATETIME_OFFSET="7" # int (days)
|
||||
|
||||
### END Envs to be deprecated in Langfuse v3.0
|
||||
|
||||
|
||||
|
||||
|
||||
### START Langfuse Cloud Config
|
||||
# Used for Langfuse Cloud deployments
|
||||
# Not recommended for self-hosted deployments as these are NOT COVERED BY SEMVER
|
||||
|
||||
@@ -12,10 +12,24 @@ body:
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: To reproduce
|
||||
description: Describe how to reproduce your bug. Steps, code snippets, reproduction repos etc.
|
||||
description: Describe how to reproduce your bug. Please provide detailed steps, code snippets, reproduction repos etc.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: SDK and container versions
|
||||
description: If you're experiencing an issue with an integration or SDK, please ensure you're using the latest version. If you're self-hosting Langfuse, check that you're running the most recent version. If updating isn't an option, please provide the specific versions you're currently using.
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Additional information
|
||||
description: Add any other information related to the bug here, screenshots if applicable.
|
||||
- type: dropdown
|
||||
id: contribute
|
||||
attributes:
|
||||
label: Are you interested to contribute a fix for this bug?
|
||||
description: If this is a confirmed bug, the maintainers are happy to support with guidance and review.
|
||||
options:
|
||||
- "No"
|
||||
- "Yes"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
@@ -290,6 +290,16 @@ When a new release is tagged on the `main` branch (excluding prereleases), it tr
|
||||
1. The Docker image is published to GitHub Packages with the version number and `latest` tag.
|
||||
2. The deployment is carried out on Langfuse Cloud. This is done by force pushing the `main` branch to the `production` branch during every release, using the [`release.yml`](.github/workflows/release.yml) GitHub Action.
|
||||
|
||||
## Using secrets stored in 1Password
|
||||
|
||||
When applying changes to non-local environments, you may need to use secrets stored in 1Password. We use the 1Password CLI for this purpose.
|
||||
|
||||
Example:
|
||||
|
||||
```bash
|
||||
op run --env-file="./.env" -- pnpm --filter=shared run db:deploy
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
Langfuse is MIT licensed, except for `ee/` folder. See [LICENSE](LICENSE) and [docs](https://langfuse.com/docs/open-source) for more details.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
Copyright (c) 2023 Finto Technologies GmbH
|
||||
Copyright (c) 2023--2024 Langfuse GmbH
|
||||
|
||||
Portions of this software are licensed as follows:
|
||||
|
||||
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
Langfuse Enterprise License (the “Enterprise License”)
|
||||
Copyright (c) 2024 Langfuse GmbH ('Langfuse')
|
||||
|
||||
With regard to the Langfuse Software:
|
||||
This software and associated documentation files (the "Software") may only be
|
||||
used in production, if you (and any entity that you represent) have agreed to,
|
||||
and are in compliance with, the Langfuse Terms of Service, available
|
||||
at https://langfuse.com/terms (the “Enterprise Terms”), or other
|
||||
agreement governing the use of the Software, as agreed by you and Langfuse,
|
||||
and otherwise have a valid Langfuse Enterprise license.
|
||||
|
||||
Subject to the foregoing sentence, you are free to
|
||||
modify this Software and publish patches to the Software. You agree that Langfuse
|
||||
and/or its licensors (as applicable) retain all right, title and interest in and
|
||||
to all such modifications and/or patches, and all such modifications and/or
|
||||
patches may only be used, copied, modified, displayed, distributed, or otherwise
|
||||
exploited with a valid Langfuse Enterprise license.
|
||||
|
||||
Notwithstanding the foregoing, you may copy and modify
|
||||
the Software for development and testing purposes, without requiring a
|
||||
subscription. You agree that Langfuse and/or its licensors (as applicable) retain
|
||||
all right, title and interest in and to all such modifications. You are not
|
||||
granted any other rights beyond what is expressly stated herein. Subject to the
|
||||
foregoing, it is forbidden to copy, merge, publish, distribute, sublicense,
|
||||
and/or sell the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
For all third party components incorporated into the Langfuse Software, those
|
||||
components are licensed under the original license provided by the owner of the
|
||||
applicable component.
|
||||
+1
-1
@@ -25,7 +25,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@langfuse/shared": "workspace:*",
|
||||
"next": "^14.2.1",
|
||||
"next": "^14.2.3",
|
||||
"next-auth": "^4.24.7",
|
||||
"zod": "^3.22.4"
|
||||
},
|
||||
|
||||
@@ -15,7 +15,7 @@ groups:
|
||||
version: 0.0.26
|
||||
output:
|
||||
location: local-file-system
|
||||
path: ../../../web/generated/openapi-client
|
||||
path: ../../../web/public/generated/api-client
|
||||
config:
|
||||
namespaceExport: Langfuse
|
||||
allowCustomFetcher: true
|
||||
|
||||
@@ -9,8 +9,8 @@ docs: |
|
||||
|
||||
## Exports
|
||||
|
||||
- OpenAPI spec: https://cloud.langfuse.com/openapi-server.yml
|
||||
- Postman collection: https://cloud.langfuse.com/postman-collection.json
|
||||
- OpenAPI spec: https://cloud.langfuse.com/generated/api/openapi.yml
|
||||
- Postman collection: https://cloud.langfuse.com/generated/postman/collection.json
|
||||
|
||||
error-discrimination:
|
||||
strategy: status-code
|
||||
|
||||
@@ -32,3 +32,11 @@ types:
|
||||
id:
|
||||
type: optional<string>
|
||||
docs: Dataset items are upserted on their id
|
||||
status:
|
||||
type: optional<DatasetItemStatus>
|
||||
docs: Defaults to ACTIVE for newly created items
|
||||
DatasetItemStatus:
|
||||
docs: Status of a single dataset item
|
||||
enum:
|
||||
- ACTIVE
|
||||
- ARCHIVED
|
||||
|
||||
@@ -31,10 +31,10 @@ service:
|
||||
docs: Optional filter for metrics where traces include all of these tags
|
||||
fromTimestamp:
|
||||
type: optional<datetime>
|
||||
docs: Optional filter to only include traces on or after a certain timestamp
|
||||
docs: Optional filter to only include traces on or after a certain datetime (ISO 8601)
|
||||
toTimestamp:
|
||||
type: optional<datetime>
|
||||
docs: Optional filter to only include traces before a certain timestamp
|
||||
docs: Optional filter to only include traces before a certain datetime (ISO 8601)
|
||||
response: DailyMetrics
|
||||
types:
|
||||
DailyMetrics:
|
||||
|
||||
@@ -35,7 +35,7 @@ service:
|
||||
parentObservationId: optional<string>
|
||||
fromStartTime:
|
||||
type: optional<datetime>
|
||||
docs: Retrieve only observations with a start_time greater than this timestamp.
|
||||
docs: Retrieve only observations with a start_time greater than this datetime (ISO 8601).
|
||||
response: ObservationsViews
|
||||
|
||||
types:
|
||||
|
||||
@@ -29,7 +29,7 @@ service:
|
||||
name: optional<string>
|
||||
fromTimestamp:
|
||||
type: optional<datetime>
|
||||
docs: Retrieve only scores newer than this timestamp.
|
||||
docs: Retrieve only scores newer than this datetime (ISO 8601).
|
||||
source:
|
||||
type: commons.ScoreSource
|
||||
docs: Retrieve only scores from a specific source.
|
||||
|
||||
@@ -32,7 +32,7 @@ service:
|
||||
name: optional<string>
|
||||
fromTimestamp:
|
||||
type: optional<datetime>
|
||||
docs: Retrieve only traces newer than this timestamp.
|
||||
docs: Retrieve only traces newer than this datetime (ISO 8601).
|
||||
orderBy:
|
||||
type: optional<string>
|
||||
docs: "Format of the string [field].[asc/desc]. Fields: id, timestamp, name, userId, release, version, public, bookmarked, sessionId. Example: timestamp.asc"
|
||||
|
||||
@@ -6,7 +6,7 @@ groups:
|
||||
version: 0.0.28
|
||||
output:
|
||||
location: local-file-system
|
||||
path: ../../../web/generated/openapi-server
|
||||
path: ../../../web/public/generated/api
|
||||
config:
|
||||
namespaceExport: Langfuse
|
||||
allowCustomFetcher: true
|
||||
@@ -33,7 +33,7 @@ groups:
|
||||
version: 0.0.45
|
||||
output:
|
||||
location: local-file-system
|
||||
path: ../../../web/generated/postman
|
||||
path: ../../../web/public/generated/postman
|
||||
# published:
|
||||
# generators:
|
||||
# - name: fernapi/fern-python-sdk
|
||||
|
||||
+3
-3
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "langfuse",
|
||||
"version": "2.37.2",
|
||||
"version": "2.41.0",
|
||||
"author": "engineering@langfuse.com",
|
||||
"license": "MIT",
|
||||
"private": true,
|
||||
@@ -30,8 +30,8 @@
|
||||
"@release-it/bumper": "^6.0.1",
|
||||
"dotenv-cli": "^7.4.1",
|
||||
"prettier": "^3.2.5",
|
||||
"release-it": "^17.2.0",
|
||||
"turbo": "^1.13.2"
|
||||
"release-it": "^17.2.1",
|
||||
"turbo": "^1.13.3"
|
||||
},
|
||||
"release-it": {
|
||||
"git": {
|
||||
|
||||
@@ -11,9 +11,9 @@
|
||||
"@typescript-eslint/eslint-plugin": "^7.1.0",
|
||||
"@typescript-eslint/parser": "^7.7.0",
|
||||
"@vercel/style-guide": "^6.0.0",
|
||||
"eslint-config-next": "^14.2.1",
|
||||
"eslint-config-next": "^14.2.3",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-config-turbo": "^1.13.2",
|
||||
"eslint-config-turbo": "^1.13.3",
|
||||
"eslint-plugin-only-warn": "^1.1.0",
|
||||
"typescript": "^5.4.5"
|
||||
}
|
||||
|
||||
@@ -44,14 +44,14 @@
|
||||
"seed": "ts-node -r tsconfig-paths/register -r dotenv/config --compiler-options {\"module\":\"CommonJS\"} prisma/seed.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@langchain/anthropic": "^0.1.14",
|
||||
"@langchain/core": "^0.1.58",
|
||||
"@langchain/anthropic": "^0.1.16",
|
||||
"@langchain/core": "^0.1.61",
|
||||
"@langchain/openai": "^0.0.28",
|
||||
"@prisma/client": "^5.12.1",
|
||||
"@prisma/client": "^5.13.0",
|
||||
"@types/bcryptjs": "^2.4.6",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"kysely": "^0.27.3",
|
||||
"langchain": "^0.1.33",
|
||||
"langchain": "^0.1.36",
|
||||
"prisma-extension-kysely": "^2.1.0",
|
||||
"zod": "^3.22.4",
|
||||
"zod-to-json-schema": "^3.22.5"
|
||||
@@ -71,12 +71,12 @@
|
||||
"kysely-codegen": "^0.11.0",
|
||||
"nodemon": "^3.0.3",
|
||||
"prettier": "^3.2.5",
|
||||
"prisma": "^5.12.1",
|
||||
"prisma": "^5.13.0",
|
||||
"prisma-erd-generator": "^1.11.2",
|
||||
"prisma-kysely": "^1.8.0",
|
||||
"ts-node": "^10.9.2",
|
||||
"tsc-watch": "^6.2.0",
|
||||
"typescript": "^5.4.5",
|
||||
"vitest": "^1.5.0"
|
||||
"vitest": "^1.5.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,13 +4,13 @@ export type Generated<T> = T extends ColumnType<infer S, infer I, infer U>
|
||||
: ColumnType<T, T | undefined, T>;
|
||||
export type Timestamp = ColumnType<Date, Date | string, Date | string>;
|
||||
|
||||
export const MembershipRole = {
|
||||
export const ProjectRole = {
|
||||
OWNER: "OWNER",
|
||||
ADMIN: "ADMIN",
|
||||
MEMBER: "MEMBER",
|
||||
VIEWER: "VIEWER"
|
||||
} as const;
|
||||
export type MembershipRole = (typeof MembershipRole)[keyof typeof MembershipRole];
|
||||
export type ProjectRole = (typeof ProjectRole)[keyof typeof ProjectRole];
|
||||
export const ObservationType = {
|
||||
SPAN: "SPAN",
|
||||
EVENT: "EVENT",
|
||||
@@ -96,7 +96,7 @@ export type AuditLog = {
|
||||
updated_at: Generated<Timestamp>;
|
||||
user_id: string;
|
||||
project_id: string;
|
||||
user_project_role: MembershipRole;
|
||||
user_project_role: ProjectRole;
|
||||
resource_type: string;
|
||||
resource_id: string;
|
||||
action: string;
|
||||
@@ -208,17 +208,10 @@ export type LlmApiKeys = {
|
||||
secret_key: string;
|
||||
project_id: string;
|
||||
};
|
||||
export type Membership = {
|
||||
project_id: string;
|
||||
user_id: string;
|
||||
role: MembershipRole;
|
||||
created_at: Generated<Timestamp>;
|
||||
updated_at: Generated<Timestamp>;
|
||||
};
|
||||
export type MembershipInvitation = {
|
||||
id: string;
|
||||
email: string;
|
||||
role: MembershipRole;
|
||||
role: ProjectRole;
|
||||
project_id: string;
|
||||
sender_id: string | null;
|
||||
created_at: Generated<Timestamp>;
|
||||
@@ -324,6 +317,13 @@ export type Project = {
|
||||
name: string;
|
||||
cloud_config: unknown | null;
|
||||
};
|
||||
export type ProjectMembership = {
|
||||
project_id: string;
|
||||
user_id: string;
|
||||
role: ProjectRole;
|
||||
created_at: Generated<Timestamp>;
|
||||
updated_at: Generated<Timestamp>;
|
||||
};
|
||||
export type Prompt = {
|
||||
id: string;
|
||||
created_at: Generated<Timestamp>;
|
||||
@@ -342,6 +342,7 @@ export type Prompt = {
|
||||
export type Score = {
|
||||
id: string;
|
||||
timestamp: Generated<Timestamp>;
|
||||
project_id: string | null;
|
||||
name: string;
|
||||
value: number;
|
||||
source: ScoreSource;
|
||||
@@ -378,6 +379,8 @@ export type Trace = {
|
||||
input: unknown | null;
|
||||
output: unknown | null;
|
||||
session_id: string | null;
|
||||
created_at: Generated<Timestamp>;
|
||||
updated_at: Generated<Timestamp>;
|
||||
};
|
||||
export type TraceSession = {
|
||||
id: string;
|
||||
@@ -437,12 +440,12 @@ export type DB = {
|
||||
job_executions: JobExecution;
|
||||
llm_api_keys: LlmApiKeys;
|
||||
membership_invitations: MembershipInvitation;
|
||||
memberships: Membership;
|
||||
models: Model;
|
||||
observations: Observation;
|
||||
observations_view: ObservationView;
|
||||
posthog_integrations: PosthogIntegration;
|
||||
pricings: Pricing;
|
||||
project_memberships: ProjectMembership;
|
||||
projects: Project;
|
||||
prompts: Prompt;
|
||||
scores: Score;
|
||||
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "traces" ADD COLUMN "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
ADD COLUMN "updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP;
|
||||
@@ -0,0 +1,3 @@
|
||||
-- CreateIndex
|
||||
|
||||
CREATE INDEX CONCURRENTLY "traces_created_at_idx" ON "traces"("created_at");
|
||||
@@ -0,0 +1,3 @@
|
||||
-- CreateIndex
|
||||
|
||||
CREATE INDEX CONCURRENTLY "traces_updated_at_idx" ON "traces"("updated_at");
|
||||
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "scores" ADD COLUMN "project_id" TEXT;
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
-- CreateIndex
|
||||
CREATE INDEX CONCURRENTLY "scores_project_id_idx" ON "scores"("project_id");
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
-- Backfill project_id on existing scores based on linked trace_id
|
||||
UPDATE scores
|
||||
SET project_id = traces.project_id
|
||||
FROM traces
|
||||
WHERE scores.trace_id = traces.id AND scores.project_id IS NULL;
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
-- Rename table, will cause a downtime as it needs to be locked and synced with TRPC API
|
||||
ALTER TABLE "memberships"
|
||||
RENAME TO "project_memberships";
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "project_memberships" RENAME CONSTRAINT "memberships_pkey" TO "project_memberships_pkey";
|
||||
|
||||
-- RenameForeignKey
|
||||
ALTER TABLE "project_memberships" RENAME CONSTRAINT "memberships_project_id_fkey" TO "project_memberships_project_id_fkey";
|
||||
|
||||
-- RenameForeignKey
|
||||
ALTER TABLE "project_memberships" RENAME CONSTRAINT "memberships_user_id_fkey" TO "project_memberships_user_id_fkey";
|
||||
|
||||
-- RenameIndex
|
||||
ALTER INDEX "memberships_user_id_idx" RENAME TO "project_memberships_user_id_idx";
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
-- Goal: Rename the enum `MembershipRole` to `ProjectRole` and update all tables that use it
|
||||
|
||||
-- Create a new Enum
|
||||
CREATE TYPE "ProjectRole" AS ENUM ('OWNER', 'ADMIN', 'MEMBER', 'VIEWER');
|
||||
|
||||
-- Add a temporary column to tables (for each table)
|
||||
ALTER TABLE "audit_logs" ADD COLUMN "user_project_role_temp" text;
|
||||
ALTER TABLE "membership_invitations" ADD COLUMN "role_temp" text;
|
||||
ALTER TABLE "project_memberships" ADD COLUMN "role_temp" text;
|
||||
|
||||
-- Copy data over to temporary columns (for each table)
|
||||
UPDATE "audit_logs" SET "user_project_role_temp" = "user_project_role"::text;
|
||||
UPDATE "membership_invitations" SET "role_temp" = "role"::text;
|
||||
UPDATE "project_memberships" SET "role_temp" = "role"::text;
|
||||
|
||||
-- Drop old columns
|
||||
ALTER TABLE "audit_logs" DROP COLUMN "user_project_role";
|
||||
ALTER TABLE "membership_invitations" DROP COLUMN "role";
|
||||
ALTER TABLE "project_memberships" DROP COLUMN "role";
|
||||
|
||||
-- Rename temporary columns to old column names
|
||||
ALTER TABLE "audit_logs" RENAME COLUMN "user_project_role_temp" TO "user_project_role";
|
||||
ALTER TABLE "membership_invitations" RENAME COLUMN "role_temp" TO "role";
|
||||
ALTER TABLE "project_memberships" RENAME COLUMN "role_temp" TO "role";
|
||||
|
||||
-- Convert the text columns to enum columns (for each table)
|
||||
ALTER TABLE "audit_logs" ALTER COLUMN "user_project_role" TYPE "ProjectRole" USING "user_project_role"::"ProjectRole";
|
||||
ALTER TABLE "membership_invitations" ALTER COLUMN "role" TYPE "ProjectRole" USING "role"::"ProjectRole";
|
||||
ALTER TABLE "project_memberships" ALTER COLUMN "role" TYPE "ProjectRole" USING "role"::"ProjectRole";
|
||||
|
||||
-- Make the columns NOT NULL (for each table)
|
||||
ALTER TABLE "audit_logs" ALTER COLUMN "user_project_role" SET NOT NULL;
|
||||
ALTER TABLE "membership_invitations" ALTER COLUMN "role" SET NOT NULL;
|
||||
ALTER TABLE "project_memberships" ALTER COLUMN "role" SET NOT NULL;
|
||||
|
||||
-- Now finally, you can drop your old enum
|
||||
DROP TYPE "MembershipRole";
|
||||
@@ -0,0 +1,20 @@
|
||||
INSERT INTO models (
|
||||
id,
|
||||
project_id,
|
||||
model_name,
|
||||
match_pattern,
|
||||
start_date,
|
||||
input_price,
|
||||
output_price,
|
||||
total_price,
|
||||
unit,
|
||||
tokenizer_id,
|
||||
tokenizer_config
|
||||
)
|
||||
VALUES
|
||||
-- gpt-4o
|
||||
('b9854a5c92dc496b997d99d20', NULL, 'gpt-4o', '(?i)^(gpt-4o)$', NULL, 0.000005, 0.000015, NULL, 'TOKENS', 'openai', '{ "tokensPerMessage": 3, "tokensPerName": 1, "tokenizerModel": "gpt-4o" }'),
|
||||
|
||||
-- gpt-4o-2024-05-13
|
||||
('b9854a5c92dc496b997d99d21', NULL, 'gpt-4o-2024-05-13', '(?i)^(gpt-4o-2024-05-13)$', NULL, 0.000005, 0.000015, NULL, 'TOKENS', 'openai', '{ "tokensPerMessage": 3, "tokensPerName": 1, "tokenizerModel": "gpt-4o-2024-05-13" }')
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
previewFeatures = ["tracing", "views"]
|
||||
previewFeatures = ["tracing", "views", "relationJoins"]
|
||||
}
|
||||
|
||||
datasource db {
|
||||
@@ -63,21 +63,21 @@ model Session {
|
||||
}
|
||||
|
||||
model User {
|
||||
id String @id @default(cuid())
|
||||
name String?
|
||||
email String? @unique
|
||||
emailVerified DateTime? @map("email_verified")
|
||||
password String?
|
||||
image String?
|
||||
admin Boolean @default(false)
|
||||
accounts Account[]
|
||||
sessions Session[]
|
||||
memberships Membership[]
|
||||
invitations MembershipInvitation[]
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
|
||||
featureFlags String[] @default([]) @map("feature_flags")
|
||||
AuditLog AuditLog[]
|
||||
id String @id @default(cuid())
|
||||
name String?
|
||||
email String? @unique
|
||||
emailVerified DateTime? @map("email_verified")
|
||||
password String?
|
||||
image String?
|
||||
admin Boolean @default(false)
|
||||
accounts Account[]
|
||||
sessions Session[]
|
||||
projectMemberships ProjectMembership[]
|
||||
invitations MembershipInvitation[]
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
|
||||
featureFlags String[] @default([]) @map("feature_flags")
|
||||
AuditLog AuditLog[]
|
||||
|
||||
@@map("users")
|
||||
}
|
||||
@@ -97,7 +97,7 @@ model Project {
|
||||
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[]
|
||||
projectMembers ProjectMembership[]
|
||||
traces Trace[]
|
||||
observations Observation[]
|
||||
apiKeys ApiKey[]
|
||||
@@ -154,37 +154,37 @@ model LlmApiKeys {
|
||||
@@map("llm_api_keys")
|
||||
}
|
||||
|
||||
model Membership {
|
||||
projectId String @map("project_id")
|
||||
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
||||
userId String @map("user_id")
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
role MembershipRole
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
|
||||
model ProjectMembership {
|
||||
projectId String @map("project_id")
|
||||
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
||||
userId String @map("user_id")
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
role ProjectRole
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
|
||||
|
||||
@@id([projectId, userId])
|
||||
@@index([userId])
|
||||
@@map("memberships")
|
||||
@@map("project_memberships")
|
||||
}
|
||||
|
||||
model MembershipInvitation {
|
||||
id String @id @unique @default(cuid())
|
||||
id String @id @unique @default(cuid())
|
||||
email String
|
||||
role MembershipRole
|
||||
projectId String @map("project_id")
|
||||
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
||||
senderId String? @map("sender_id")
|
||||
sender User? @relation(fields: [senderId], references: [id], onDelete: SetNull)
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
|
||||
role ProjectRole
|
||||
projectId String @map("project_id")
|
||||
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
||||
senderId String? @map("sender_id")
|
||||
sender User? @relation(fields: [senderId], references: [id], onDelete: SetNull)
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
|
||||
|
||||
@@index([projectId])
|
||||
@@index([email])
|
||||
@@map("membership_invitations")
|
||||
}
|
||||
|
||||
enum MembershipRole {
|
||||
enum ProjectRole {
|
||||
OWNER
|
||||
ADMIN
|
||||
MEMBER
|
||||
@@ -227,6 +227,8 @@ model Trace {
|
||||
output Json?
|
||||
sessionId String? @map("session_id")
|
||||
session TraceSession? @relation(fields: [sessionId, projectId], references: [id, projectId])
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
|
||||
|
||||
scores Score[]
|
||||
DatasetRunItems DatasetRunItems[]
|
||||
@@ -241,6 +243,8 @@ model Trace {
|
||||
@@index([externalId])
|
||||
@@index(timestamp)
|
||||
@@index(release)
|
||||
@@index(updatedAt)
|
||||
@@index(createdAt)
|
||||
@@index([tags(ops: ArrayOps)], type: Gin)
|
||||
@@map("traces")
|
||||
}
|
||||
@@ -272,6 +276,8 @@ view TraceView {
|
||||
}
|
||||
|
||||
// Update ObservationView below when making changes to this model!
|
||||
// traceId is optional only due to timing during data injestion
|
||||
// (traceId is not necessarily known at the time of observation creation)
|
||||
model Observation {
|
||||
id String @id @default(cuid())
|
||||
traceId String? @map("trace_id")
|
||||
@@ -388,6 +394,7 @@ enum ObservationLevel {
|
||||
model Score {
|
||||
id String @id @default(cuid())
|
||||
timestamp DateTime @default(now())
|
||||
projectId String? @map("project_id")
|
||||
name String
|
||||
value Float
|
||||
source ScoreSource
|
||||
@@ -401,6 +408,7 @@ model Score {
|
||||
@@unique([id, traceId]) // used for upsert
|
||||
@@index(timestamp)
|
||||
@@index([value])
|
||||
@@index([projectId])
|
||||
@@index([traceId], type: Hash)
|
||||
@@index([observationId], type: Hash)
|
||||
@@index([source])
|
||||
@@ -594,16 +602,16 @@ model Model {
|
||||
}
|
||||
|
||||
model AuditLog {
|
||||
id String @id @default(cuid())
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
|
||||
userId String @map("user_id")
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
projectId String @map("project_id")
|
||||
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
||||
userProjectRole MembershipRole @map("user_project_role")
|
||||
resourceType String @map("resource_type")
|
||||
resourceId String @map("resource_id")
|
||||
id String @id @default(cuid())
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
|
||||
userId String @map("user_id")
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
projectId String @map("project_id")
|
||||
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
||||
userProjectRole ProjectRole @map("user_project_role")
|
||||
resourceType String @map("resource_type")
|
||||
resourceId String @map("resource_id")
|
||||
action String
|
||||
before String? //stringified JSON
|
||||
after String? // stringified JSON
|
||||
|
||||
@@ -51,7 +51,7 @@ async function main() {
|
||||
create: {
|
||||
id: seedProjectId,
|
||||
name: "llm-app",
|
||||
members: {
|
||||
projectMembers: {
|
||||
create: {
|
||||
role: "OWNER",
|
||||
userId: user.id,
|
||||
@@ -72,7 +72,7 @@ async function main() {
|
||||
name: "summary-prompt",
|
||||
project: { connect: { id: seedProjectId } },
|
||||
prompt: "prompt {{variable}} {{anotherVariable}}",
|
||||
labels: ["production"],
|
||||
labels: ["production", "latest"],
|
||||
version: 1,
|
||||
createdBy: "user-1",
|
||||
},
|
||||
@@ -110,7 +110,7 @@ async function main() {
|
||||
create: {
|
||||
id: "239ad00f-562f-411d-af14-831c75ddd875",
|
||||
name: "demo-app",
|
||||
members: {
|
||||
projectMembers: {
|
||||
create: {
|
||||
role: "OWNER",
|
||||
userId: user.id,
|
||||
@@ -501,6 +501,7 @@ function createObjects(
|
||||
value: Math.floor(Math.random() * 3) - 1,
|
||||
timestamp: traceTs,
|
||||
source: ScoreSource.REVIEW,
|
||||
projectId,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
@@ -512,6 +513,7 @@ function createObjects(
|
||||
value: Math.floor(Math.random() * 10) - 5,
|
||||
timestamp: traceTs,
|
||||
source: ScoreSource.API,
|
||||
projectId,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
@@ -693,6 +695,7 @@ function createObjects(
|
||||
observationId: generation.id,
|
||||
traceId: trace.id,
|
||||
source: ScoreSource.API,
|
||||
projectId: trace.projectId,
|
||||
});
|
||||
if (Math.random() > 0.6)
|
||||
scores.push({
|
||||
@@ -701,6 +704,7 @@ function createObjects(
|
||||
observationId: generation.id,
|
||||
traceId: trace.id,
|
||||
source: ScoreSource.API,
|
||||
projectId: trace.projectId,
|
||||
});
|
||||
|
||||
for (let l = 0; l < Math.floor(Math.random() * 2); l++) {
|
||||
@@ -764,7 +768,7 @@ async function generatePrompts(project: Project) {
|
||||
prompt: "Prompt 1 content",
|
||||
name: "Prompt 1",
|
||||
version: 1,
|
||||
labels: ["production"],
|
||||
labels: ["production", "latest"],
|
||||
},
|
||||
{
|
||||
id: `prompt-${v4()}`,
|
||||
@@ -773,7 +777,7 @@ async function generatePrompts(project: Project) {
|
||||
prompt: "Prompt 2 content",
|
||||
name: "Prompt 2",
|
||||
version: 1,
|
||||
labels: ["production"],
|
||||
labels: ["production", "latest"],
|
||||
},
|
||||
{
|
||||
id: `prompt-${v4()}`,
|
||||
@@ -782,7 +786,7 @@ async function generatePrompts(project: Project) {
|
||||
prompt: "Prompt 3 content",
|
||||
name: "Prompt 3 by API",
|
||||
version: 1,
|
||||
labels: ["production"],
|
||||
labels: ["production", "latest"],
|
||||
},
|
||||
{
|
||||
id: `prompt-${v4()}`,
|
||||
@@ -791,7 +795,7 @@ async function generatePrompts(project: Project) {
|
||||
prompt: "Prompt 4 content",
|
||||
name: "Prompt 4",
|
||||
version: 1,
|
||||
labels: ["production"],
|
||||
labels: ["production", "latest"],
|
||||
tags: ["tag1", "tag2"],
|
||||
},
|
||||
];
|
||||
@@ -859,6 +863,7 @@ async function generatePrompts(project: Project) {
|
||||
frequencyPenalty: 0.5,
|
||||
},
|
||||
version: 3,
|
||||
labels: ["production", "latest"],
|
||||
},
|
||||
];
|
||||
|
||||
@@ -908,7 +913,7 @@ async function generatePrompts(project: Project) {
|
||||
prompt: `${promptName} version ${i} content`,
|
||||
name: promptName,
|
||||
version: i,
|
||||
labels: i === 20 ? ["production"] : [],
|
||||
labels: i === 20 ? ["production", "latest"] : [],
|
||||
},
|
||||
update: {
|
||||
id: promptId,
|
||||
|
||||
@@ -93,10 +93,22 @@ export const evalLLMModels = [
|
||||
maxTemperature: 2,
|
||||
max_tokens: 256,
|
||||
top_p: 1,
|
||||
} as const,
|
||||
{
|
||||
provider: ModelProvider.OpenAI,
|
||||
model: "gpt-4o",
|
||||
temperature: 1,
|
||||
maxTemperature: 2,
|
||||
max_tokens: 256,
|
||||
top_p: 1,
|
||||
},
|
||||
] as const;
|
||||
|
||||
export const EvalModelNames = z.enum(["gpt-3.5-turbo", "gpt-4-turbo-preview"]);
|
||||
export const EvalModelNames = z.enum([
|
||||
"gpt-3.5-turbo",
|
||||
"gpt-4-turbo-preview",
|
||||
"gpt-4o",
|
||||
]);
|
||||
|
||||
export const OutputSchema = z.object({
|
||||
reasoning: z.string(),
|
||||
|
||||
@@ -42,6 +42,8 @@ export type OpenAIModelParams = {
|
||||
} & ModelConfig;
|
||||
|
||||
export const openAIModels = [
|
||||
"gpt-4o",
|
||||
"gpt-4o-2024-05-13",
|
||||
"gpt-4-turbo-preview",
|
||||
"gpt-4-1106-preview",
|
||||
"gpt-4-0613",
|
||||
|
||||
Generated
+791
-660
File diff suppressed because it is too large
Load Diff
+22
-20
@@ -1,14 +1,13 @@
|
||||
{
|
||||
"name": "web",
|
||||
"version": "2.37.2",
|
||||
"version": "2.41.0",
|
||||
"private": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
},
|
||||
"scripts": {
|
||||
"copy-openapi-spec": "cp generated/openapi-client/openapi.yml public/openapi-client.yml && cp generated/openapi-server/openapi.yml public/openapi-server.yml && cp generated/postman/collection.json public/postman-collection.json",
|
||||
"build": "pnpm run copy-openapi-spec && INLINE_RUNTIME_CHUNK=false dotenv -e ../.env -- next build",
|
||||
"build": "INLINE_RUNTIME_CHUNK=false dotenv -e ../.env -- next build",
|
||||
"dev": "dotenv -e ../.env -- next dev",
|
||||
"lint": "dotenv -e ../.env -- next lint",
|
||||
"lint:fix": "dotenv -e ../.env -- next lint --fix",
|
||||
@@ -23,17 +22,17 @@
|
||||
"dependencies": {
|
||||
"@anthropic-ai/tokenizer": "^0.0.4",
|
||||
"@aws-sdk/client-s3": "^3.550.0",
|
||||
"@aws-sdk/lib-storage": "^3.550.0",
|
||||
"@aws-sdk/lib-storage": "^3.568.0",
|
||||
"@aws-sdk/s3-request-presigner": "^3.554.0",
|
||||
"@codemirror/lang-json": "^6.0.1",
|
||||
"@headlessui/react": "^1.7.19",
|
||||
"@heroicons/react": "^2.1.3",
|
||||
"@hookform/resolvers": "^3.3.4",
|
||||
"@langchain/anthropic": "^0.1.14",
|
||||
"@langchain/core": "^0.1.58",
|
||||
"@langchain/anthropic": "^0.1.16",
|
||||
"@langchain/core": "^0.1.61",
|
||||
"@langchain/openai": "^0.0.28",
|
||||
"@langfuse/shared": "workspace:*",
|
||||
"@langfuse/ee": "workspace:*",
|
||||
"@langfuse/shared": "workspace:*",
|
||||
"@marsidev/react-turnstile": "^0.5.4",
|
||||
"@next-auth/prisma-adapter": "^1.0.7",
|
||||
"@radix-ui/react-accordion": "^1.1.2",
|
||||
@@ -56,15 +55,16 @@
|
||||
"@radix-ui/react-tabs": "^1.0.4",
|
||||
"@radix-ui/react-toggle": "^1.0.3",
|
||||
"@radix-ui/react-tooltip": "^1.0.7",
|
||||
"@react-email/components": "^0.0.16",
|
||||
"@react-email/render": "^0.0.12",
|
||||
"@react-email/components": "^0.0.17",
|
||||
"@react-email/render": "^0.0.13",
|
||||
"@repo/eslint-config": "workspace:*",
|
||||
"@repo/typescript-config": "workspace:*",
|
||||
"@sentry/nextjs": "^7.110.1",
|
||||
"@sentry/node": "^7.110.1",
|
||||
"@sentry/profiling-node": "^7.110.1",
|
||||
"@sentry/types": "^7.110.1",
|
||||
"@sentry/nextjs": "^7.113.0",
|
||||
"@sentry/node": "^7.113.0",
|
||||
"@sentry/profiling-node": "^7.113.0",
|
||||
"@sentry/types": "^7.113.0",
|
||||
"@t3-oss/env-nextjs": "^0.8.0",
|
||||
"@tailwindcss/container-queries": "^0.1.1",
|
||||
"@tailwindcss/forms": "^0.5.7",
|
||||
"@tanstack/react-query": "^4.36.1",
|
||||
"@tanstack/react-table": "^8.11.8",
|
||||
@@ -78,25 +78,26 @@
|
||||
"ai": "^3.0.23",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.1.0",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.0.0",
|
||||
"core-js": "^3.36.1",
|
||||
"cors": "^2.8.5",
|
||||
"date-fns": "^3.3.1",
|
||||
"decimal.js": "^10.4.3",
|
||||
"dompurify": "^3.1.2",
|
||||
"exponential-backoff": "^3.1.1",
|
||||
"js-tiktoken": "^1.0.11",
|
||||
"js-tiktoken": "^1.0.12",
|
||||
"kysely": "^0.27.3",
|
||||
"langchain": "^0.1.33",
|
||||
"langchain": "^0.1.36",
|
||||
"lodash": "^4.17.21",
|
||||
"lucide-react": "^0.364.0",
|
||||
"next": "^14.2.1",
|
||||
"next": "^14.2.3",
|
||||
"next-auth": "^4.24.7",
|
||||
"next-query-params": "^5.0.0",
|
||||
"nodemailer": "^6.9.13",
|
||||
"posthog-js": "^1.122.0",
|
||||
"posthog-node": "^3.6.3",
|
||||
"prisma": "^5.12.1",
|
||||
"prisma": "^5.13.0",
|
||||
"react": "18.2.0",
|
||||
"react-day-picker": "^8.10.1",
|
||||
"react-dom": "18.2.0",
|
||||
@@ -117,9 +118,10 @@
|
||||
"@mermaid-js/mermaid-cli": "^10.7.0",
|
||||
"@playwright/test": "^1.43.1",
|
||||
"@testing-library/jest-dom": "^6.4.2",
|
||||
"@testing-library/react": "^15.0.2",
|
||||
"@testing-library/react": "^15.0.6",
|
||||
"@types/bcryptjs": "^2.4.6",
|
||||
"@types/cors": "^2.8.17",
|
||||
"@types/dompurify": "^3.0.5",
|
||||
"@types/eslint": "^8.56.7",
|
||||
"@types/jest": "^29.5.12",
|
||||
"@types/lodash": "^4.14.202",
|
||||
@@ -133,7 +135,7 @@
|
||||
"autoprefixer": "^10.4.19",
|
||||
"dotenv-cli": "^7.3.0",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-config-next": "^14.2.1",
|
||||
"eslint-config-next": "^14.2.3",
|
||||
"jest": "^29.7.0",
|
||||
"jest-environment-jsdom": "^29.7.0",
|
||||
"node-mocks-http": "^1.14.1",
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
openapi*.yml
|
||||
postman-collection.json
|
||||
@@ -19,9 +19,10 @@ info:
|
||||
## Exports
|
||||
|
||||
|
||||
- OpenAPI spec: https://cloud.langfuse.com/openapi-server.yml
|
||||
- OpenAPI spec: https://cloud.langfuse.com/generated/api/openapi.yml
|
||||
|
||||
- Postman collection: https://cloud.langfuse.com/postman-collection.json
|
||||
- Postman collection:
|
||||
https://cloud.langfuse.com/generated/postman/collection.json
|
||||
paths:
|
||||
/api/public/dataset-items:
|
||||
post:
|
||||
@@ -495,7 +496,7 @@ paths:
|
||||
in: query
|
||||
description: >-
|
||||
Optional filter to only include traces on or after a certain
|
||||
timestamp
|
||||
datetime (ISO 8601)
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
@@ -503,7 +504,9 @@ paths:
|
||||
nullable: true
|
||||
- name: toTimestamp
|
||||
in: query
|
||||
description: Optional filter to only include traces before a certain timestamp
|
||||
description: >-
|
||||
Optional filter to only include traces before a certain datetime
|
||||
(ISO 8601)
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
@@ -647,7 +650,7 @@ paths:
|
||||
in: query
|
||||
description: >-
|
||||
Retrieve only observations with a start_time greater than this
|
||||
timestamp.
|
||||
datetime (ISO 8601).
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
@@ -985,7 +988,7 @@ paths:
|
||||
nullable: true
|
||||
- name: fromTimestamp
|
||||
in: query
|
||||
description: Retrieve only scores newer than this timestamp.
|
||||
description: Retrieve only scores newer than this datetime (ISO 8601).
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
@@ -1261,7 +1264,7 @@ paths:
|
||||
nullable: true
|
||||
- name: fromTimestamp
|
||||
in: query
|
||||
description: Retrieve only traces newer than this timestamp.
|
||||
description: Retrieve only traces newer than this datetime (ISO 8601).
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
@@ -1847,8 +1850,19 @@ components:
|
||||
type: string
|
||||
nullable: true
|
||||
description: Dataset items are upserted on their id
|
||||
status:
|
||||
$ref: '#/components/schemas/DatasetItemStatus'
|
||||
nullable: true
|
||||
description: Defaults to ACTIVE for newly created items
|
||||
required:
|
||||
- datasetName
|
||||
DatasetItemStatus:
|
||||
title: DatasetItemStatus
|
||||
type: string
|
||||
enum:
|
||||
- ACTIVE
|
||||
- ARCHIVED
|
||||
description: Status of a single dataset item
|
||||
CreateDatasetRunItemRequest:
|
||||
title: CreateDatasetRunItemRequest
|
||||
type: object
|
||||
+7
-7
@@ -2,7 +2,7 @@
|
||||
"info": {
|
||||
"name": "Langfuse",
|
||||
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json",
|
||||
"description": "## Authentication\n\nAuthenticate with the API using [Basic Auth](https://en.wikipedia.org/wiki/Basic_access_authentication), get API keys in the project settings:\n\n- username: Langfuse Public Key\n- password: Langfuse Secret Key\n\n## Exports\n\n- OpenAPI spec: https://cloud.langfuse.com/openapi-server.yml\n- Postman collection: https://cloud.langfuse.com/postman-collection.json"
|
||||
"description": "## Authentication\n\nAuthenticate with the API using [Basic Auth](https://en.wikipedia.org/wiki/Basic_access_authentication), get API keys in the project settings:\n\n- username: Langfuse Public Key\n- password: Langfuse Secret Key\n\n## Exports\n\n- OpenAPI spec: https://cloud.langfuse.com/generated/api/openapi.yml\n- Postman collection: https://cloud.langfuse.com/generated/postman/collection.json"
|
||||
},
|
||||
"variable": [
|
||||
{
|
||||
@@ -65,7 +65,7 @@
|
||||
"auth": null,
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"datasetName\": \"example\",\n \"input\": \"UNKNOWN\",\n \"expectedOutput\": \"UNKNOWN\",\n \"metadata\": \"UNKNOWN\",\n \"sourceTraceId\": \"example\",\n \"sourceObservationId\": \"example\",\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 \"status\": \"ACTIVE\"\n}",
|
||||
"options": {
|
||||
"raw": {
|
||||
"language": "json"
|
||||
@@ -418,12 +418,12 @@
|
||||
{
|
||||
"key": "fromTimestamp",
|
||||
"value": "",
|
||||
"description": "Optional filter to only include traces on or after a certain timestamp"
|
||||
"description": "Optional filter to only include traces on or after a certain datetime (ISO 8601)"
|
||||
},
|
||||
{
|
||||
"key": "toTimestamp",
|
||||
"value": "",
|
||||
"description": "Optional filter to only include traces before a certain timestamp"
|
||||
"description": "Optional filter to only include traces before a certain datetime (ISO 8601)"
|
||||
}
|
||||
],
|
||||
"variable": []
|
||||
@@ -528,7 +528,7 @@
|
||||
{
|
||||
"key": "fromStartTime",
|
||||
"value": "",
|
||||
"description": "Retrieve only observations with a start_time greater than this timestamp."
|
||||
"description": "Retrieve only observations with a start_time greater than this datetime (ISO 8601)."
|
||||
}
|
||||
],
|
||||
"variable": []
|
||||
@@ -788,7 +788,7 @@
|
||||
{
|
||||
"key": "fromTimestamp",
|
||||
"value": "",
|
||||
"description": "Retrieve only scores newer than this timestamp."
|
||||
"description": "Retrieve only scores newer than this datetime (ISO 8601)."
|
||||
},
|
||||
{
|
||||
"key": "source",
|
||||
@@ -996,7 +996,7 @@
|
||||
{
|
||||
"key": "fromTimestamp",
|
||||
"value": "",
|
||||
"description": "Retrieve only traces newer than this timestamp."
|
||||
"description": "Retrieve only traces newer than this datetime (ISO 8601)."
|
||||
},
|
||||
{
|
||||
"key": "orderBy",
|
||||
@@ -28,6 +28,7 @@ if (process.env.NEXT_PUBLIC_SENTRY_DSN)
|
||||
// Add profiling integration to list of integrations
|
||||
new ProfilingIntegration(),
|
||||
new Sentry.Integrations.Prisma({ client: prisma }),
|
||||
Sentry.metrics.metricsAggregatorIntegration(),
|
||||
],
|
||||
|
||||
// filter out passwords from the signup request body
|
||||
|
||||
@@ -70,3 +70,38 @@ test("Signup input validation", async ({ page }) => {
|
||||
// random email address to be used in tests
|
||||
const randomEmailAddress = () =>
|
||||
Math.random().toString(36).substring(2, 11) + "@example.com";
|
||||
|
||||
test("Unauthenticated user should be redirected to target URL after login", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto("/auth/sign-in");
|
||||
await page.fill('input[name="email"]', "demo@langfuse.com");
|
||||
await page.fill('input[type="password"]', "password");
|
||||
await page.click('button[data-testid="submit-email-password-sign-in-form"]');
|
||||
|
||||
// wait 2 seconds
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
// project id and prompt from seed.ts
|
||||
const promptUrl = "/project/7a88fb47-b4e2-43b8-a06c-a5ce950dc53a/prompts/summary-prompt";
|
||||
|
||||
await page.getByRole("button", { name: /Demo User/ }).click();
|
||||
|
||||
await page.getByRole("menuitem", { name: "Sign Out" }).click();
|
||||
|
||||
await expect(page).toHaveURL("/auth/sign-in");
|
||||
|
||||
await page.goto(promptUrl);
|
||||
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
await expect(page).toHaveURL(/targetPath/);
|
||||
|
||||
await page.fill('input[name="email"]', "demo@langfuse.com");
|
||||
await page.fill('input[type="password"]', "password");
|
||||
await page.click('button[data-testid="submit-email-password-sign-in-form"]');
|
||||
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
await expect(page).toHaveURL(promptUrl);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import * as DOMPurify from "dompurify";
|
||||
|
||||
// Create test cases
|
||||
describe("should sanitize dom", () => {
|
||||
it("should sanitize urls", () => {
|
||||
const dirty =
|
||||
"http://www.some.site/page.html?default=<script>alert(document.cookie)</script>";
|
||||
const cleam = DOMPurify.sanitize(dirty);
|
||||
|
||||
expect(cleam).toBe("http://www.some.site/page.html?default=");
|
||||
});
|
||||
it("should should not sanitize valid langfuse urls", () => {
|
||||
const validLangfuseUrl =
|
||||
"https://cloud.langfuse.com/project/abjdnksadn/traces?filter=timestamp%3Bdatetime%3B%3B%3E%3B2024-04-19T00%253A00%253A00.000Z%2Cid%3Bstring%3B%3Bcontains%3Ba%2Ctimestamp%3Bdatetime%3B%3B%3C%3B2024-04-30T22%253A00%253A00.000Z%2CinputTokens%3Bnumber%3B%3B%3E%3B1";
|
||||
const cleam = DOMPurify.sanitize(validLangfuseUrl);
|
||||
|
||||
expect(cleam).toBe(validLangfuseUrl);
|
||||
});
|
||||
});
|
||||
@@ -73,6 +73,15 @@ describe("model match", () => {
|
||||
input: "I am a prompt",
|
||||
output: "I am a completion",
|
||||
},
|
||||
{
|
||||
type: "GENERATION",
|
||||
projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
|
||||
model: "gpt-4o",
|
||||
startTime: new Date("2024-01-01T00:00:00.000Z"),
|
||||
unit: ModelUsageUnit.Tokens,
|
||||
input: "I am a prompt",
|
||||
output: "I am a completion",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
@@ -86,7 +95,7 @@ describe("model match", () => {
|
||||
expect(observation1?.completionTokens).toEqual(3000);
|
||||
expect(observation1?.totalTokens).toEqual(0);
|
||||
|
||||
expect(observations.length).toEqual(3);
|
||||
expect(observations.length).toEqual(4);
|
||||
observations.forEach((observation) => {
|
||||
expect(observation.internalModel).toBeDefined();
|
||||
expect(observation.promptTokens).toBeGreaterThan(0);
|
||||
|
||||
@@ -709,7 +709,7 @@ describe("/api/public/v2/prompts API Endpoint", () => {
|
||||
create: {
|
||||
id: otherProjectId,
|
||||
name: "demo-app",
|
||||
members: {
|
||||
projectMembers: {
|
||||
create: {
|
||||
role: "OWNER",
|
||||
userId: "user-test",
|
||||
|
||||
@@ -90,7 +90,7 @@ export const setupUserAndProject = async () => {
|
||||
},
|
||||
],
|
||||
},
|
||||
members: {
|
||||
projectMembers: {
|
||||
create: {
|
||||
role: "OWNER",
|
||||
userId: user.id,
|
||||
|
||||
@@ -33,6 +33,7 @@ describe("Token Count Functions", () => {
|
||||
{ model: "claude", tokenizer: "claude", tokens: 118 },
|
||||
{ model: "claude-instant-1.2", tokenizer: "claude", tokens: 118 },
|
||||
{ model: "gpt-3.5-turbo-1106", tokenizer: "openai", tokens: 114 },
|
||||
{ model: "gpt-4o-2024-05-13", tokenizer: "openai", tokens: 112 },
|
||||
].forEach(({ model, tokens, tokenizer }) => {
|
||||
it(`should return token count ${tokens} for ${model}`, () => {
|
||||
const result = tokenCount({
|
||||
|
||||
@@ -7,10 +7,10 @@ import {
|
||||
} from "@/src/components/ui/popover";
|
||||
import { Button } from "@/src/components/ui/button";
|
||||
import { TrashIcon } from "lucide-react";
|
||||
import { usePostHog } from "posthog-js/react";
|
||||
import { useHasAccess } from "@/src/features/rbac/utils/checkAccess";
|
||||
import { type Scope } from "@/src/features/rbac/constants/roleAccessRights";
|
||||
import { api } from "@/src/utils/api";
|
||||
import { usePostHogClientCapture } from "@/src/features/posthog-analytics/usePostHogClientCapture";
|
||||
|
||||
interface DeleteButtonProps {
|
||||
itemId: string;
|
||||
@@ -33,7 +33,7 @@ export function DeleteButton({
|
||||
}: DeleteButtonProps) {
|
||||
const [isDeleted, setIsDeleted] = useState(false);
|
||||
const router = useRouter();
|
||||
const posthog = usePostHog();
|
||||
const capture = usePostHogClientCapture();
|
||||
|
||||
const hasAccess = useHasAccess({ projectId, scope: scope });
|
||||
const traceMutation = api.traces.deleteMany.useMutation({
|
||||
@@ -63,6 +63,15 @@ export function DeleteButton({
|
||||
<Button
|
||||
variant={isTableAction ? "ghost" : "outline"}
|
||||
size={isTableAction ? "xs" : "icon"}
|
||||
onClick={() =>
|
||||
type === "trace"
|
||||
? capture("trace:delete_form_open", {
|
||||
source: isTableAction ? "table-single-row" : "trace detail",
|
||||
})
|
||||
: capture("datasets:delete_form_open", {
|
||||
source: "dataset",
|
||||
})
|
||||
}
|
||||
>
|
||||
<TrashIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
@@ -84,7 +93,7 @@ export function DeleteButton({
|
||||
traceIds: [itemId],
|
||||
projectId,
|
||||
});
|
||||
posthog.capture("trace:delete", {
|
||||
capture("trace:delete", {
|
||||
source: isTableAction ? "table-single-row" : "trace",
|
||||
});
|
||||
}}
|
||||
@@ -101,7 +110,7 @@ export function DeleteButton({
|
||||
projectId,
|
||||
datasetId: itemId,
|
||||
});
|
||||
posthog.capture("dataset:delete", {
|
||||
capture("datasets:delete_dataset_button_click", {
|
||||
source: isTableAction ? "table-single-row" : "dataset",
|
||||
});
|
||||
}}
|
||||
|
||||
@@ -14,6 +14,7 @@ export const ErrorPage = ({
|
||||
}) => {
|
||||
const session = useSession();
|
||||
const router = useRouter();
|
||||
const newTargetPath = router.asPath;
|
||||
|
||||
return (
|
||||
<div className="flex h-screen flex-col items-center justify-center">
|
||||
@@ -21,7 +22,15 @@ export const ErrorPage = ({
|
||||
<h1 className="mb-4 text-xl font-bold">{title}</h1>
|
||||
<p className="mb-8 text-center">{message}</p>
|
||||
{session.status === "unauthenticated" ? (
|
||||
<Button onClick={() => void router.push("/")}>Sign In</Button>
|
||||
<Button
|
||||
onClick={() =>
|
||||
void router.push(
|
||||
`/auth/sign-in?targetPath=${encodeURIComponent(newTargetPath)}`,
|
||||
)
|
||||
}
|
||||
>
|
||||
Sign In
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import CodeMirror, { EditorView } from "@uiw/react-codemirror";
|
||||
import { githubLight } from "@uiw/codemirror-theme-github";
|
||||
import { json } from "@codemirror/lang-json";
|
||||
import { cn } from "@/src/utils/tailwind";
|
||||
|
||||
// todo: add json linting
|
||||
|
||||
@@ -9,11 +10,13 @@ export function JsonEditor({
|
||||
onChange,
|
||||
editable = true,
|
||||
lineWrapping = true,
|
||||
className,
|
||||
}: {
|
||||
defaultValue: string;
|
||||
onChange?: (value: string) => void;
|
||||
editable?: boolean;
|
||||
lineWrapping?: boolean;
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<CodeMirror
|
||||
@@ -26,7 +29,7 @@ export function JsonEditor({
|
||||
extensions={[json(), ...(lineWrapping ? [EditorView.lineWrapping] : [])]}
|
||||
defaultValue={defaultValue}
|
||||
onChange={onChange}
|
||||
className="overflow-hidden rounded-md border"
|
||||
className={cn("overflow-hidden rounded-md border", className)}
|
||||
editable={editable}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -3,9 +3,9 @@ import {
|
||||
HoverCardContent,
|
||||
HoverCardTrigger,
|
||||
} from "@/src/components/ui/hover-card";
|
||||
import { usePostHogClientCapture } from "@/src/features/posthog-analytics/usePostHogClientCapture";
|
||||
import { HelpCircle, Info } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { usePostHog } from "posthog-js/react";
|
||||
|
||||
export type DocPopupProps = {
|
||||
description: React.ReactNode;
|
||||
@@ -26,14 +26,17 @@ export default function DocPopup({
|
||||
md: "w-6 h-6",
|
||||
lg: "w-8 h-8",
|
||||
};
|
||||
const posthog = usePostHog();
|
||||
const capture = usePostHogClientCapture();
|
||||
|
||||
return (
|
||||
<HoverCard
|
||||
openDelay={200}
|
||||
onOpenChange={(open) => {
|
||||
if (open) {
|
||||
posthog.capture("help-popup:opened", { href, description });
|
||||
capture("help_popup:opened", {
|
||||
hfref: href,
|
||||
description: description,
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
@@ -44,6 +47,12 @@ export default function DocPopup({
|
||||
rel="noopener"
|
||||
target="_blank"
|
||||
className="inline-block whitespace-nowrap text-gray-500 sm:pl-0"
|
||||
onClick={() => {
|
||||
capture("help_popup:href_clicked", {
|
||||
href: href,
|
||||
description: description,
|
||||
});
|
||||
}}
|
||||
>
|
||||
{
|
||||
{
|
||||
|
||||
@@ -14,7 +14,6 @@ import {
|
||||
AvatarFallback,
|
||||
AvatarImage,
|
||||
} from "@/src/components/ui/avatar";
|
||||
import { api } from "@/src/utils/api";
|
||||
import { NewProjectButton } from "@/src/features/projects/components/NewProjectButton";
|
||||
import { FeedbackButtonWrapper } from "@/src/features/feedback/component/FeedbackButton";
|
||||
import { Button } from "@/src/components/ui/button";
|
||||
@@ -31,6 +30,7 @@ import {
|
||||
import { ChevronDownIcon } from "@heroicons/react/20/solid";
|
||||
import useLocalStorage from "@/src/components/useLocalStorage";
|
||||
import { ProjectNavigation } from "@/src/components/projectNavigation";
|
||||
import DOMPurify from "dompurify";
|
||||
|
||||
const userNavigation = [
|
||||
{
|
||||
@@ -61,7 +61,7 @@ export default function Layout(props: PropsWithChildren) {
|
||||
useCheckNotification(NOTIFICATIONS, session.status === "authenticated");
|
||||
|
||||
const enableExperimentalFeatures =
|
||||
api.environment.enableExperimentalFeatures.useQuery().data ?? false;
|
||||
session.data?.environment.enableExperimentalFeatures ?? false;
|
||||
|
||||
const projectId = router.query.projectId as string | undefined;
|
||||
|
||||
@@ -147,7 +147,14 @@ export default function Layout(props: PropsWithChildren) {
|
||||
!publishablePaths.includes(router.pathname) &&
|
||||
!router.pathname.startsWith("/public/")
|
||||
) {
|
||||
void router.replace("/auth/sign-in");
|
||||
const newTargetPath = router.asPath;
|
||||
if (newTargetPath && newTargetPath !== "/") {
|
||||
void router.replace(
|
||||
`/auth/sign-in?targetPath=${encodeURIComponent(newTargetPath)}`,
|
||||
);
|
||||
} else {
|
||||
void router.replace(`/auth/sign-in`);
|
||||
}
|
||||
return <Spinner message="Redirecting" />;
|
||||
}
|
||||
|
||||
@@ -155,7 +162,13 @@ export default function Layout(props: PropsWithChildren) {
|
||||
session.status === "authenticated" &&
|
||||
unauthenticatedPaths.includes(router.pathname)
|
||||
) {
|
||||
void router.replace("/");
|
||||
const targetPath = router.query.targetPath as string | undefined;
|
||||
|
||||
const sanitizedTargetPath = targetPath
|
||||
? DOMPurify.sanitize(targetPath)
|
||||
: undefined;
|
||||
|
||||
void router.replace(sanitizedTargetPath ?? "/");
|
||||
return <Spinner message="Redirecting" />;
|
||||
}
|
||||
|
||||
|
||||
@@ -75,13 +75,13 @@ export const ROUTES: Route[] = [
|
||||
name: "Configs",
|
||||
pathname: `/project/[projectId]/evals/configs`,
|
||||
cloudOnly: true,
|
||||
rbacScope: "job:read",
|
||||
rbacScope: "evalJob:read",
|
||||
},
|
||||
{
|
||||
name: "Log",
|
||||
pathname: `/project/[projectId]/evals/log`,
|
||||
cloudOnly: true,
|
||||
rbacScope: "jobExecution:read",
|
||||
rbacScope: "evalJobExecution:read",
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Switch } from "@/src/components/ui/switch";
|
||||
import { usePostHogClientCapture } from "@/src/features/posthog-analytics/usePostHogClientCapture";
|
||||
import { useHasAccess } from "@/src/features/rbac/utils/checkAccess";
|
||||
import { api } from "@/src/utils/api";
|
||||
import { Link, LockIcon } from "lucide-react";
|
||||
import { usePostHog } from "posthog-js/react";
|
||||
import { useState } from "react";
|
||||
|
||||
export const PublishTraceSwitch = (props: {
|
||||
@@ -10,7 +10,7 @@ export const PublishTraceSwitch = (props: {
|
||||
projectId: string;
|
||||
isPublic: boolean;
|
||||
}) => {
|
||||
const posthog = usePostHog();
|
||||
const capture = usePostHogClientCapture();
|
||||
const hasAccess = useHasAccess({
|
||||
projectId: props.projectId,
|
||||
scope: "objects:publish",
|
||||
@@ -30,7 +30,7 @@ export const PublishTraceSwitch = (props: {
|
||||
traceId: props.traceId,
|
||||
public: val,
|
||||
});
|
||||
posthog.capture("trace_detail:publish_trace_button_click");
|
||||
capture("trace_detail:publish_button_click");
|
||||
}}
|
||||
isLoading={mut.isLoading}
|
||||
disabled={!hasAccess}
|
||||
@@ -43,7 +43,7 @@ export const PublishSessionSwitch = (props: {
|
||||
projectId: string;
|
||||
isPublic: boolean;
|
||||
}) => {
|
||||
const posthog = usePostHog();
|
||||
const capture = usePostHogClientCapture();
|
||||
const hasAccess = useHasAccess({
|
||||
projectId: props.projectId,
|
||||
scope: "objects:publish",
|
||||
@@ -63,7 +63,7 @@ export const PublishSessionSwitch = (props: {
|
||||
sessionId: props.sessionId,
|
||||
public: val,
|
||||
});
|
||||
posthog.capture("session_detail:publish_session_button_click");
|
||||
capture("session_detail:publish_button_click");
|
||||
}}
|
||||
isLoading={mut.isLoading}
|
||||
disabled={!hasAccess}
|
||||
|
||||
@@ -6,6 +6,7 @@ import { useHasAccess } from "@/src/features/rbac/utils/checkAccess";
|
||||
import { cn } from "@/src/utils/tailwind";
|
||||
import { type RouterOutput, type RouterInput } from "@/src/utils/types";
|
||||
import { useState } from "react";
|
||||
import { usePostHogClientCapture } from "@/src/features/posthog-analytics/usePostHogClientCapture";
|
||||
|
||||
export function StarToggle({
|
||||
value,
|
||||
@@ -53,7 +54,7 @@ export function StarTraceToggle({
|
||||
}) {
|
||||
const utils = api.useUtils();
|
||||
const hasAccess = useHasAccess({ projectId, scope: "objects:bookmark" });
|
||||
|
||||
const capture = usePostHogClientCapture();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const mutBookmarkTrace = api.traces.bookmark.useMutation({
|
||||
@@ -109,13 +110,18 @@ export function StarTraceToggle({
|
||||
size={size}
|
||||
disabled={!hasAccess}
|
||||
isLoading={isLoading}
|
||||
onClick={(value) =>
|
||||
mutBookmarkTrace.mutateAsync({
|
||||
onClick={(value) => {
|
||||
capture("table:bookmark_button_click", {
|
||||
table: "traces",
|
||||
id: traceId,
|
||||
value: value,
|
||||
});
|
||||
return mutBookmarkTrace.mutateAsync({
|
||||
projectId,
|
||||
traceId,
|
||||
bookmarked: value,
|
||||
})
|
||||
}
|
||||
});
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -133,7 +139,7 @@ export function StarTraceDetailsToggle({
|
||||
}) {
|
||||
const utils = api.useUtils();
|
||||
const hasAccess = useHasAccess({ projectId, scope: "objects:bookmark" });
|
||||
|
||||
const capture = usePostHogClientCapture();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const mutBookmarkTrace = api.traces.bookmark.useMutation({
|
||||
@@ -180,13 +186,17 @@ export function StarTraceDetailsToggle({
|
||||
size={size}
|
||||
disabled={!hasAccess}
|
||||
isLoading={isLoading}
|
||||
onClick={(value) =>
|
||||
mutBookmarkTrace.mutateAsync({
|
||||
onClick={(value) => {
|
||||
capture("trace_detail:bookmark_button_click", {
|
||||
id: traceId,
|
||||
value: value,
|
||||
});
|
||||
return mutBookmarkTrace.mutateAsync({
|
||||
projectId,
|
||||
traceId,
|
||||
bookmarked: value,
|
||||
})
|
||||
}
|
||||
});
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -204,6 +214,7 @@ export function StarSessionToggle({
|
||||
}) {
|
||||
const utils = api.useUtils();
|
||||
const hasAccess = useHasAccess({ projectId, scope: "objects:bookmark" });
|
||||
const capture = usePostHogClientCapture();
|
||||
const mutBookmarkSession = api.sessions.bookmark.useMutation({
|
||||
onSuccess: () => {
|
||||
void utils.sessions.invalidate();
|
||||
@@ -216,13 +227,18 @@ export function StarSessionToggle({
|
||||
size={size}
|
||||
isLoading={mutBookmarkSession.isLoading}
|
||||
disabled={!hasAccess}
|
||||
onClick={(value) =>
|
||||
mutBookmarkSession.mutateAsync({
|
||||
onClick={(value) => {
|
||||
capture("table:bookmark_button_click", {
|
||||
table: "sessions",
|
||||
id: sessionId,
|
||||
value: value,
|
||||
});
|
||||
return mutBookmarkSession.mutateAsync({
|
||||
projectId,
|
||||
sessionId,
|
||||
bookmarked: value,
|
||||
})
|
||||
}
|
||||
});
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
import { type VisibilityState } from "@tanstack/react-table";
|
||||
import { ChevronDown, Columns } from "lucide-react";
|
||||
import { type LangfuseColumnDef } from "@/src/components/table/types";
|
||||
import { usePostHogClientCapture } from "@/src/features/posthog-analytics/usePostHogClientCapture";
|
||||
|
||||
interface DataTableColumnVisibilityFilterProps<TData, TValue> {
|
||||
columns: LangfuseColumnDef<TData, TValue>[];
|
||||
@@ -27,14 +28,24 @@ export function DataTableColumnVisibilityFilter<TData, TValue>({
|
||||
setColumnVisibility,
|
||||
}: DataTableColumnVisibilityFilterProps<TData, TValue>) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const capture = usePostHogClientCapture();
|
||||
const toggleColumn = useCallback(
|
||||
(columnId: string) => {
|
||||
setColumnVisibility((old) => ({
|
||||
...old,
|
||||
[columnId]: !old[columnId],
|
||||
}));
|
||||
setColumnVisibility((old) => {
|
||||
const newColumnVisibility = {
|
||||
...old,
|
||||
[columnId]: !old[columnId],
|
||||
};
|
||||
const selectedColumns = Object.keys(newColumnVisibility).filter(
|
||||
(key) => newColumnVisibility[key],
|
||||
);
|
||||
capture("table:column_visibility_changed", {
|
||||
selectedColumns: selectedColumns,
|
||||
});
|
||||
return newColumnVisibility;
|
||||
});
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[setColumnVisibility],
|
||||
);
|
||||
|
||||
|
||||
+7
-3
@@ -9,7 +9,6 @@ import {
|
||||
import { Button } from "@/src/components/ui/button";
|
||||
import { useHasAccess } from "@/src/features/rbac/utils/checkAccess";
|
||||
import { api } from "@/src/utils/api";
|
||||
import { usePostHog } from "posthog-js/react";
|
||||
|
||||
import {
|
||||
Dialog,
|
||||
@@ -20,6 +19,7 @@ import {
|
||||
DialogTitle,
|
||||
} from "@/src/components/ui/dialog";
|
||||
import { useState } from "react";
|
||||
import { usePostHogClientCapture } from "@/src/features/posthog-analytics/usePostHogClientCapture";
|
||||
|
||||
export function TraceTableMultiSelectAction({
|
||||
selectedTraceIds,
|
||||
@@ -32,7 +32,7 @@ export function TraceTableMultiSelectAction({
|
||||
}) {
|
||||
const utils = api.useUtils();
|
||||
const [open, setOpen] = useState(false);
|
||||
const posthog = usePostHog();
|
||||
const capture = usePostHogClientCapture();
|
||||
|
||||
const hasDeleteAccess = useHasAccess({ projectId, scope: "traces:delete" });
|
||||
const mutDeleteTraces = api.traces.deleteMany.useMutation({
|
||||
@@ -55,6 +55,10 @@ export function TraceTableMultiSelectAction({
|
||||
<DropdownMenuItem
|
||||
disabled={!hasDeleteAccess}
|
||||
onClick={() => {
|
||||
capture("trace:delete_form_open", {
|
||||
count: selectedTraceIds.length,
|
||||
source: "table-multi-select",
|
||||
});
|
||||
setOpen(true);
|
||||
}}
|
||||
>
|
||||
@@ -94,7 +98,7 @@ export function TraceTableMultiSelectAction({
|
||||
.then(() => {
|
||||
setOpen(false);
|
||||
});
|
||||
posthog.capture("trace:delete", {
|
||||
capture("trace:delete_form_submit", {
|
||||
count: selectedTraceIds.length,
|
||||
source: "table-multi-select",
|
||||
});
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/src/components/ui/select";
|
||||
import { usePostHogClientCapture } from "@/src/features/posthog-analytics/usePostHogClientCapture";
|
||||
|
||||
interface DataTablePaginationProps<TData> {
|
||||
table: Table<TData>;
|
||||
@@ -24,6 +25,7 @@ export function DataTablePagination<TData>({
|
||||
table,
|
||||
paginationOptions = [10, 20, 30, 40, 50],
|
||||
}: DataTablePaginationProps<TData>) {
|
||||
const capture = usePostHogClientCapture();
|
||||
return (
|
||||
<div className="mt-3 flex items-center justify-between overflow-x-auto px-2">
|
||||
<div className="flex-1 text-sm text-muted-foreground">
|
||||
@@ -36,6 +38,9 @@ export function DataTablePagination<TData>({
|
||||
<Select
|
||||
value={`${table.getState().pagination.pageSize}`}
|
||||
onValueChange={(value) => {
|
||||
capture("table:pagination_page_size_select", {
|
||||
pageSize: value,
|
||||
});
|
||||
table.setPageSize(Number(value));
|
||||
}}
|
||||
>
|
||||
@@ -59,7 +64,12 @@ export function DataTablePagination<TData>({
|
||||
<Button
|
||||
variant="outline"
|
||||
className="hidden h-8 w-8 p-0 lg:flex"
|
||||
onClick={() => table.setPageIndex(0)}
|
||||
onClick={() => {
|
||||
table.setPageIndex(0);
|
||||
capture("table:pagination_button_click", {
|
||||
type: "firstPage",
|
||||
});
|
||||
}}
|
||||
disabled={!table.getCanPreviousPage()}
|
||||
>
|
||||
<span className="sr-only">Go to first page</span>
|
||||
@@ -68,7 +78,12 @@ export function DataTablePagination<TData>({
|
||||
<Button
|
||||
variant="outline"
|
||||
className="h-8 w-8 p-0"
|
||||
onClick={() => table.previousPage()}
|
||||
onClick={() => {
|
||||
table.previousPage();
|
||||
capture("table:pagination_button_click", {
|
||||
type: "previousPage",
|
||||
});
|
||||
}}
|
||||
disabled={!table.getCanPreviousPage()}
|
||||
>
|
||||
<span className="sr-only">Go to previous page</span>
|
||||
@@ -77,7 +92,12 @@ export function DataTablePagination<TData>({
|
||||
<Button
|
||||
variant="outline"
|
||||
className="h-8 w-8 p-0"
|
||||
onClick={() => table.nextPage()}
|
||||
onClick={() => {
|
||||
table.nextPage();
|
||||
capture("table:pagination_button_click", {
|
||||
type: "nextPage",
|
||||
});
|
||||
}}
|
||||
disabled={!table.getCanNextPage()}
|
||||
>
|
||||
<span className="sr-only">Go to next page</span>
|
||||
@@ -86,7 +106,12 @@ export function DataTablePagination<TData>({
|
||||
<Button
|
||||
variant="outline"
|
||||
className="hidden h-8 w-8 p-0 lg:flex"
|
||||
onClick={() => table.setPageIndex(table.getPageCount() - 1)}
|
||||
onClick={() => {
|
||||
table.setPageIndex(table.getPageCount() - 1);
|
||||
capture("table:pagination_button_click", {
|
||||
type: "lastPage",
|
||||
});
|
||||
}}
|
||||
disabled={!table.getCanNextPage()}
|
||||
>
|
||||
<span className="sr-only">Go to last page</span>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Tabs, TabsList, TabsTrigger } from "@/src/components/ui/tabs";
|
||||
import useLocalStorage from "@/src/components/useLocalStorage";
|
||||
import { usePostHogClientCapture } from "@/src/features/posthog-analytics/usePostHogClientCapture";
|
||||
import {
|
||||
MdDensityLarge,
|
||||
MdDensityMedium,
|
||||
@@ -35,25 +36,33 @@ export const DataTableRowHeightSwitch = ({
|
||||
}: {
|
||||
rowHeight: RowHeight;
|
||||
setRowHeight: (e: RowHeight) => void;
|
||||
}) => (
|
||||
<Tabs
|
||||
//defaultValue={height}
|
||||
value={rowHeight}
|
||||
onValueChange={(e) => setRowHeight(e as any)}
|
||||
key="height"
|
||||
>
|
||||
<TabsList className="gap-1 border bg-transparent px-2">
|
||||
{heightOptions.map(({ id, label, icon }) => (
|
||||
<TabsTrigger
|
||||
key={id}
|
||||
value={id}
|
||||
className="px-2 shadow-none data-[state=active]:bg-slate-200 data-[state=active]:ring-border"
|
||||
>
|
||||
<span role="img" aria-label={`${label} size`}>
|
||||
{icon}
|
||||
</span>
|
||||
</TabsTrigger>
|
||||
))}
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
);
|
||||
}) => {
|
||||
const capture = usePostHogClientCapture();
|
||||
return (
|
||||
<Tabs
|
||||
//defaultValue={height}
|
||||
value={rowHeight}
|
||||
onValueChange={(e) => {
|
||||
capture("table:row_height_switch_select", {
|
||||
rowHeight: e,
|
||||
});
|
||||
setRowHeight(e as any);
|
||||
}}
|
||||
key="height"
|
||||
>
|
||||
<TabsList className="gap-1 border bg-transparent px-2">
|
||||
{heightOptions.map(({ id, label, icon }) => (
|
||||
<TabsTrigger
|
||||
key={id}
|
||||
value={id}
|
||||
className="px-2 shadow-none data-[state=active]:bg-slate-200 data-[state=active]:ring-border"
|
||||
>
|
||||
<span role="img" aria-label={`${label} size`}>
|
||||
{icon}
|
||||
</span>
|
||||
</TabsTrigger>
|
||||
))}
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
type RowHeight,
|
||||
} from "@/src/components/table/data-table-row-height-switch";
|
||||
import { Search } from "lucide-react";
|
||||
import { usePostHogClientCapture } from "@/src/features/posthog-analytics/usePostHogClientCapture";
|
||||
|
||||
interface SearchConfig {
|
||||
placeholder: string;
|
||||
@@ -47,9 +48,10 @@ export function DataTableToolbar<TData, TValue>({
|
||||
const [searchString, setSearchString] = useState(
|
||||
searchConfig?.currentQuery ?? "",
|
||||
);
|
||||
const capture = usePostHogClientCapture();
|
||||
|
||||
return (
|
||||
<div className="my-2 flex flex-1 flex-wrap items-center gap-2">
|
||||
<div className="my-2 flex flex-1 flex-wrap items-center gap-2 @container">
|
||||
{searchConfig && (
|
||||
<div className="flex max-w-md items-center">
|
||||
<Input
|
||||
@@ -59,14 +61,18 @@ export function DataTableToolbar<TData, TValue>({
|
||||
onChange={(event) => setSearchString(event.currentTarget.value)}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === "Enter") {
|
||||
capture("table:search_submit");
|
||||
searchConfig.updateQuery(searchString);
|
||||
}
|
||||
}}
|
||||
className="h-10 w-[200px] rounded-r-none lg:w-[250] 2xl:w-[350px]"
|
||||
className="h-10 w-[150px] rounded-r-none @6xl:w-[250px]"
|
||||
/>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => searchConfig.updateQuery(searchString)}
|
||||
onClick={() => {
|
||||
capture("table:search_submit");
|
||||
searchConfig.updateQuery(searchString);
|
||||
}}
|
||||
className="rounded-l-none border-l-0 p-3"
|
||||
>
|
||||
<Search className="h-4 w-4" />
|
||||
@@ -80,7 +86,7 @@ export function DataTableToolbar<TData, TValue>({
|
||||
onChange={setFilterState}
|
||||
/>
|
||||
)}
|
||||
<div className="flex flex-row flex-wrap gap-2 lg:ml-auto">
|
||||
<div className="flex flex-row flex-wrap gap-2 @6xl:ml-auto">
|
||||
{!!columnVisibility && !!setColumnVisibility && (
|
||||
<DataTableColumnVisibilityFilter
|
||||
columns={columns}
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
TableRow,
|
||||
} from "@/src/components/ui/table";
|
||||
import { type OrderByState } from "@/src/features/orderBy/types";
|
||||
import { usePostHogClientCapture } from "@/src/features/posthog-analytics/usePostHogClientCapture";
|
||||
import { cn } from "@/src/utils/tailwind";
|
||||
import {
|
||||
flexRender,
|
||||
@@ -72,6 +73,7 @@ export function DataTable<TData extends object, TValue>({
|
||||
}: DataTableProps<TData, TValue>) {
|
||||
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
|
||||
const rowheighttw = getRowHeightTailwindClass(rowHeight);
|
||||
const capture = usePostHogClientCapture();
|
||||
|
||||
const table = useReactTable({
|
||||
data: data.data ?? [],
|
||||
@@ -129,14 +131,26 @@ export function DataTable<TData extends object, TValue>({
|
||||
|
||||
if (orderBy?.column === columnDef.id) {
|
||||
if (orderBy.order === "DESC") {
|
||||
capture("table:column_sorting_header_click", {
|
||||
column: columnDef.id,
|
||||
order: "ASC",
|
||||
});
|
||||
setOrderBy({
|
||||
column: columnDef.id,
|
||||
order: "ASC",
|
||||
});
|
||||
} else {
|
||||
capture("table:column_sorting_header_click", {
|
||||
column: columnDef.id,
|
||||
order: "Disabled",
|
||||
});
|
||||
setOrderBy(null);
|
||||
}
|
||||
} else {
|
||||
capture("table:column_sorting_header_click", {
|
||||
column: columnDef.id,
|
||||
order: "DESC",
|
||||
});
|
||||
setOrderBy({
|
||||
column: columnDef.id,
|
||||
order: "DESC",
|
||||
|
||||
@@ -13,7 +13,6 @@ import {
|
||||
} from "@/src/components/ui/dropdown-menu";
|
||||
import { Button } from "@/src/components/ui/button";
|
||||
import { ChevronDownIcon, Loader } from "lucide-react";
|
||||
import { usePostHog } from "posthog-js/react";
|
||||
import {
|
||||
NumberParam,
|
||||
StringParam,
|
||||
@@ -29,7 +28,12 @@ import {
|
||||
} from "@/src/utils/dates";
|
||||
import useColumnVisibility from "@/src/features/column-visibility/hooks/useColumnVisibility";
|
||||
import { type LangfuseColumnDef } from "@/src/components/table/types";
|
||||
import { type Prisma, type ObservationLevel } from "@langfuse/shared";
|
||||
import {
|
||||
type Prisma,
|
||||
type ObservationLevel,
|
||||
type FilterState,
|
||||
type ObservationOptions,
|
||||
} from "@langfuse/shared";
|
||||
import { cn } from "@/src/utils/tailwind";
|
||||
import { LevelColors } from "@/src/components/level-colors";
|
||||
import { usdFormatter } from "@/src/utils/numbers";
|
||||
@@ -43,6 +47,8 @@ import type Decimal from "decimal.js";
|
||||
import { type ScoreSimplified } from "@/src/server/api/routers/generations/getAllQuery";
|
||||
import { useRowHeightLocalStorage } from "@/src/components/table/data-table-row-height-switch";
|
||||
import { IOTableCell } from "@/src/components/ui/CodeJsonViewer";
|
||||
import { usePostHogClientCapture } from "@/src/features/posthog-analytics/usePostHogClientCapture";
|
||||
import { useSession } from "next-auth/react";
|
||||
|
||||
export type GenerationsTableRow = {
|
||||
id: string;
|
||||
@@ -76,10 +82,19 @@ export type GenerationsTableRow = {
|
||||
|
||||
export type GenerationsTableProps = {
|
||||
projectId: string;
|
||||
promptName?: string;
|
||||
promptVersion?: number;
|
||||
omittedFilter?: string[];
|
||||
};
|
||||
|
||||
export default function GenerationsTable({ projectId }: GenerationsTableProps) {
|
||||
const posthog = usePostHog();
|
||||
export default function GenerationsTable({
|
||||
projectId,
|
||||
promptName,
|
||||
promptVersion,
|
||||
omittedFilter = [],
|
||||
}: GenerationsTableProps) {
|
||||
const session = useSession();
|
||||
const capture = usePostHogClientCapture();
|
||||
const [isExporting, setIsExporting] = useState(false);
|
||||
const [searchQuery, setSearchQuery] = useQueryParam(
|
||||
"search",
|
||||
@@ -96,13 +111,15 @@ export default function GenerationsTable({ projectId }: GenerationsTableProps) {
|
||||
"s",
|
||||
);
|
||||
|
||||
const [filterState, setFilterState] = useQueryFilterState(
|
||||
const [inputFilterState, setInputFilterState] = useQueryFilterState(
|
||||
[
|
||||
{
|
||||
column: "Start Time",
|
||||
type: "datetime",
|
||||
operator: ">",
|
||||
value: utcDateOffsetByDays(-14),
|
||||
value: utcDateOffsetByDays(
|
||||
session.data?.environment.defaultTableDateTimeOffset ?? -14,
|
||||
),
|
||||
},
|
||||
],
|
||||
"generations",
|
||||
@@ -113,6 +130,33 @@ export default function GenerationsTable({ projectId }: GenerationsTableProps) {
|
||||
order: "DESC",
|
||||
});
|
||||
|
||||
const promptNameFilter: FilterState = promptName
|
||||
? [
|
||||
{
|
||||
column: "Prompt Name",
|
||||
type: "string",
|
||||
operator: "=",
|
||||
value: promptName,
|
||||
},
|
||||
]
|
||||
: [];
|
||||
|
||||
const promptVersionFilter: FilterState = promptVersion
|
||||
? [
|
||||
{
|
||||
column: "Prompt Version",
|
||||
type: "number",
|
||||
operator: "=",
|
||||
value: promptVersion,
|
||||
},
|
||||
]
|
||||
: [];
|
||||
|
||||
const filterState = inputFilterState.concat([
|
||||
...promptNameFilter,
|
||||
...promptVersionFilter,
|
||||
]);
|
||||
|
||||
const generations = api.generations.all.useQuery({
|
||||
page: paginationState.pageIndex,
|
||||
limit: paginationState.pageSize,
|
||||
@@ -137,11 +181,19 @@ export default function GenerationsTable({ projectId }: GenerationsTableProps) {
|
||||
},
|
||||
);
|
||||
|
||||
const transformFilterOptions = (
|
||||
filterOptions: ObservationOptions | undefined,
|
||||
) => {
|
||||
return observationsTableColsWithOptions(filterOptions).filter(
|
||||
(col) => !omittedFilter?.includes(col.name),
|
||||
);
|
||||
};
|
||||
|
||||
const handleExport = async (fileFormat: ExportFileFormats) => {
|
||||
if (isExporting) return;
|
||||
|
||||
setIsExporting(true);
|
||||
posthog.capture("generations:export", { file_format: fileFormat });
|
||||
capture("generations:export", { file_format: fileFormat });
|
||||
if (fileFormat === "OPENAI-JSONL")
|
||||
alert(
|
||||
"When exporting in OpenAI-JSONL, only generations that exactly match the `ChatML` format will be exported. For any questions, reach out to support.",
|
||||
@@ -599,11 +651,9 @@ export default function GenerationsTable({ projectId }: GenerationsTableProps) {
|
||||
<div>
|
||||
<DataTableToolbar
|
||||
columns={columns}
|
||||
filterColumnDefinition={observationsTableColsWithOptions(
|
||||
filterOptions.data,
|
||||
)}
|
||||
filterState={filterState}
|
||||
setFilterState={setFilterState}
|
||||
filterColumnDefinition={transformFilterOptions(filterOptions.data)}
|
||||
filterState={inputFilterState}
|
||||
setFilterState={setInputFilterState}
|
||||
searchConfig={{
|
||||
placeholder: "Search by id, name, traceName, model",
|
||||
updateQuery: setSearchQuery,
|
||||
@@ -617,9 +667,12 @@ export default function GenerationsTable({ projectId }: GenerationsTableProps) {
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline" className="ml-auto whitespace-nowrap">
|
||||
{filterState.length > 0 || searchQuery
|
||||
? "Export selection"
|
||||
: "Export all"}{" "}
|
||||
<span className="hidden @6xl:inline">
|
||||
{filterState.length > 0 || searchQuery
|
||||
? "Export selection"
|
||||
: "Export all"}{" "}
|
||||
</span>
|
||||
<span className="@6xl:hidden">Export</span>
|
||||
{isExporting ? (
|
||||
<Loader className="ml-2 h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
|
||||
@@ -2,6 +2,7 @@ import { DataTable } from "@/src/components/table/data-table";
|
||||
import { type LangfuseColumnDef } from "@/src/components/table/types";
|
||||
import { Button } from "@/src/components/ui/button";
|
||||
import useColumnVisibility from "@/src/features/column-visibility/hooks/useColumnVisibility";
|
||||
import { usePostHogClientCapture } from "@/src/features/posthog-analytics/usePostHogClientCapture";
|
||||
import { useHasAccess } from "@/src/features/rbac/utils/checkAccess";
|
||||
import { api } from "@/src/utils/api";
|
||||
import { usdFormatter } from "@/src/utils/numbers";
|
||||
@@ -47,7 +48,6 @@ export default function ModelTable({ projectId }: { projectId: string }) {
|
||||
pageIndex: withDefault(NumberParam, 0),
|
||||
pageSize: withDefault(NumberParam, 50),
|
||||
});
|
||||
|
||||
const models = api.models.all.useQuery({
|
||||
page: paginationState.pageIndex,
|
||||
limit: paginationState.pageSize,
|
||||
@@ -294,6 +294,7 @@ const DeleteModelButton = ({
|
||||
projectId: string;
|
||||
}) => {
|
||||
const utils = api.useUtils();
|
||||
const capture = usePostHogClientCapture();
|
||||
const mut = api.models.delete.useMutation({
|
||||
onSuccess: () => {
|
||||
void utils.models.invalidate();
|
||||
@@ -314,14 +315,20 @@ const DeleteModelButton = ({
|
||||
size="xs"
|
||||
variant="ghost"
|
||||
onClick={() => {
|
||||
mut
|
||||
.mutateAsync({
|
||||
projectId,
|
||||
modelId,
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
});
|
||||
const confirmDelete = window.confirm(
|
||||
"Are you sure you want to delete this model?",
|
||||
);
|
||||
if (confirmDelete) {
|
||||
capture("models:delete_button_click");
|
||||
mut
|
||||
.mutateAsync({
|
||||
projectId,
|
||||
modelId,
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Trash size={14} />
|
||||
|
||||
@@ -17,6 +17,7 @@ import { type RouterOutput } from "@/src/utils/types";
|
||||
import type Decimal from "decimal.js";
|
||||
import { useEffect } from "react";
|
||||
import { NumberParam, useQueryParams, withDefault } from "use-query-params";
|
||||
import { useSession } from "next-auth/react";
|
||||
|
||||
export type SessionTableRow = {
|
||||
id: string;
|
||||
@@ -45,6 +46,7 @@ export default function SessionsTable({
|
||||
omittedFilter = [],
|
||||
}: SessionTableProps) {
|
||||
const { setDetailPageList } = useDetailPageLists();
|
||||
const session = useSession();
|
||||
|
||||
const [userFilterState, setUserFilterState] = useQueryFilterState(
|
||||
[
|
||||
@@ -52,7 +54,9 @@ export default function SessionsTable({
|
||||
column: "Created At",
|
||||
type: "datetime",
|
||||
operator: ">",
|
||||
value: utcDateOffsetByDays(-14),
|
||||
value: utcDateOffsetByDays(
|
||||
session.data?.environment.defaultTableDateTimeOffset ?? -14,
|
||||
),
|
||||
},
|
||||
],
|
||||
"sessions",
|
||||
|
||||
@@ -38,6 +38,7 @@ import {
|
||||
} from "@langfuse/shared";
|
||||
import { useRowHeightLocalStorage } from "@/src/components/table/data-table-row-height-switch";
|
||||
import { IOTableCell } from "@/src/components/ui/CodeJsonViewer";
|
||||
import { useSession } from "next-auth/react";
|
||||
|
||||
export type TracesTableRow = {
|
||||
bookmarked: boolean;
|
||||
@@ -47,6 +48,7 @@ export type TracesTableRow = {
|
||||
userId: string;
|
||||
metadata?: string;
|
||||
level: ObservationLevel;
|
||||
observationCount: number;
|
||||
latency?: number;
|
||||
release?: string;
|
||||
version?: string;
|
||||
@@ -80,6 +82,7 @@ export default function TracesTable({
|
||||
omittedFilter = [],
|
||||
}: TracesTableProps) {
|
||||
const utils = api.useUtils();
|
||||
const session = useSession();
|
||||
const [selectedRows, setSelectedRows] = useState<RowSelectionState>({});
|
||||
const { setDetailPageList } = useDetailPageLists();
|
||||
const [searchQuery, setSearchQuery] = useQueryParam(
|
||||
@@ -92,7 +95,9 @@ export default function TracesTable({
|
||||
column: "Timestamp",
|
||||
type: "datetime",
|
||||
operator: ">",
|
||||
value: utcDateOffsetByDays(-14),
|
||||
value: utcDateOffsetByDays(
|
||||
session.data?.environment.defaultTableDateTimeOffset ?? -14,
|
||||
),
|
||||
},
|
||||
],
|
||||
"traces",
|
||||
@@ -173,6 +178,7 @@ export default function TracesTable({
|
||||
timestamp: trace.timestamp.toLocaleString(),
|
||||
name: trace.name ?? "",
|
||||
level: trace.level,
|
||||
observationCount: trace.observationCount,
|
||||
metadata: JSON.stringify(trace.metadata),
|
||||
release: trace.release ?? undefined,
|
||||
version: trace.version ?? undefined,
|
||||
@@ -528,6 +534,13 @@ export default function TracesTable({
|
||||
defaultHidden: true,
|
||||
enableSorting: true,
|
||||
},
|
||||
{
|
||||
accessorKey: "observationCount",
|
||||
id: "observationCount",
|
||||
header: "Observation Count",
|
||||
enableHiding: true,
|
||||
defaultHidden: true,
|
||||
},
|
||||
{
|
||||
accessorKey: "version",
|
||||
id: "version",
|
||||
|
||||
@@ -6,6 +6,7 @@ import { useState } from "react";
|
||||
import { Button } from "@/src/components/ui/button";
|
||||
import { Tabs, TabsList, TabsTrigger } from "@/src/components/ui/tabs";
|
||||
import { Fragment } from "react";
|
||||
import { usePostHogClientCapture } from "@/src/features/posthog-analytics/usePostHogClientCapture";
|
||||
|
||||
export const IOPreview: React.FC<{
|
||||
input?: unknown;
|
||||
@@ -14,7 +15,7 @@ export const IOPreview: React.FC<{
|
||||
hideIfNull?: boolean;
|
||||
}> = ({ isLoading = false, hideIfNull = false, ...props }) => {
|
||||
const [currentView, setCurrentView] = useState<"pretty" | "json">("pretty");
|
||||
|
||||
const capture = usePostHogClientCapture();
|
||||
const input = deepParseJson(props.input);
|
||||
const output = deepParseJson(props.output);
|
||||
|
||||
@@ -54,7 +55,9 @@ export const IOPreview: React.FC<{
|
||||
}
|
||||
}
|
||||
}
|
||||
const outChatMlMessage = ChatMlMessageSchema.safeParse(output);
|
||||
const outChatMlArray = ChatMlArraySchema.safeParse(
|
||||
Array.isArray(output) ? output : [output],
|
||||
);
|
||||
|
||||
// Pretty view available
|
||||
const isPrettyViewAvailable = inChatMlArray.success;
|
||||
@@ -65,7 +68,10 @@ export const IOPreview: React.FC<{
|
||||
{isPrettyViewAvailable ? (
|
||||
<Tabs
|
||||
value={currentView}
|
||||
onValueChange={(v) => setCurrentView(v as "pretty" | "json")}
|
||||
onValueChange={(v) => {
|
||||
setCurrentView(v as "pretty" | "json"),
|
||||
capture("trace_detail:io_mode_switch", { view: v });
|
||||
}}
|
||||
>
|
||||
<TabsList>
|
||||
<TabsTrigger value="pretty">Pretty ✨</TabsTrigger>
|
||||
@@ -75,17 +81,20 @@ export const IOPreview: React.FC<{
|
||||
) : null}
|
||||
{isPrettyViewAvailable && currentView === "pretty" ? (
|
||||
<OpenAiMessageView
|
||||
messages={inChatMlArray.data.concat(
|
||||
outChatMlMessage.success
|
||||
? {
|
||||
...outChatMlMessage.data,
|
||||
role: outChatMlMessage.data.role ?? "assistant",
|
||||
}
|
||||
: ChatMlMessageSchema.parse({
|
||||
role: "assistant",
|
||||
content: outputClean ? JSON.stringify(outputClean) : null,
|
||||
}),
|
||||
)}
|
||||
messages={[
|
||||
...inChatMlArray.data,
|
||||
...(outChatMlArray.success
|
||||
? outChatMlArray.data.map((m) => ({
|
||||
...m,
|
||||
role: m.role ?? "assistant",
|
||||
}))
|
||||
: [
|
||||
ChatMlMessageSchema.parse({
|
||||
role: "assistant",
|
||||
content: outputClean ? JSON.stringify(outputClean) : null,
|
||||
}),
|
||||
]),
|
||||
]}
|
||||
/>
|
||||
) : null}
|
||||
{currentView === "json" || !isPrettyViewAvailable ? (
|
||||
|
||||
@@ -155,11 +155,11 @@ export const ObservationPreview = (props: {
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{preloadedObservation.metadata ? (
|
||||
{observationWithInputAndOutput.data?.metadata ? (
|
||||
<JSONView
|
||||
key={preloadedObservation.id + "-metadata"}
|
||||
key={observationWithInputAndOutput.data.id + "-metadata"}
|
||||
title="Metadata"
|
||||
json={preloadedObservation.metadata}
|
||||
json={observationWithInputAndOutput.data.metadata}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import { formatIntervalSeconds } from "@/src/utils/dates";
|
||||
import { MinusCircle, MinusIcon, PlusCircleIcon, PlusIcon } from "lucide-react";
|
||||
import { Toggle } from "@/src/components/ui/toggle";
|
||||
import { Button } from "@/src/components/ui/button";
|
||||
import { usePostHogClientCapture } from "@/src/features/posthog-analytics/usePostHogClientCapture";
|
||||
|
||||
export const ObservationTree = (props: {
|
||||
observations: ObservationReturnType[];
|
||||
@@ -120,126 +121,138 @@ const ObservationTreeNode = (props: {
|
||||
setCurrentObservationId: (id: string | undefined) => void;
|
||||
showMetrics?: boolean;
|
||||
showScores?: boolean;
|
||||
}) => (
|
||||
<>
|
||||
{props.observations
|
||||
.sort((a, b) => a.startTime.getTime() - b.startTime.getTime())
|
||||
.map((observation) => {
|
||||
const collapsed = props.collapsedObservations.includes(observation.id);
|
||||
|
||||
return (
|
||||
<Fragment key={observation.id}>
|
||||
<div className="flex">
|
||||
{Array.from({ length: props.indentationLevel }, (_, i) => (
|
||||
<div className="mx-2 border-r" key={i} />
|
||||
))}
|
||||
<div
|
||||
className={cn(
|
||||
"group my-0.5 flex flex-1 cursor-pointer flex-col gap-1 rounded-sm p-1",
|
||||
props.currentObservationId === observation.id
|
||||
? "bg-gray-100"
|
||||
: "hover:bg-gray-50",
|
||||
)}
|
||||
onClick={() => props.setCurrentObservationId(observation.id)}
|
||||
>
|
||||
<div className="flex gap-2">
|
||||
<ColorCodedObservationType
|
||||
observationType={observation.type}
|
||||
/>
|
||||
<span className="flex-1 break-all text-sm">
|
||||
{observation.name}
|
||||
</span>
|
||||
{observation.children.length === 0 ? null : (
|
||||
<Toggle
|
||||
onClick={(ev) => (
|
||||
ev.stopPropagation(),
|
||||
props.toggleCollapsedObservation(observation.id)
|
||||
)}
|
||||
variant="default"
|
||||
pressed={collapsed}
|
||||
size="xs"
|
||||
className="w-7"
|
||||
title={
|
||||
collapsed ? "Expand children" : "Collapse children"
|
||||
}
|
||||
>
|
||||
{collapsed ? (
|
||||
<PlusIcon className="h-4 w-4" />
|
||||
) : (
|
||||
<MinusIcon className="h-4 w-4" />
|
||||
)}
|
||||
</Toggle>
|
||||
}) => {
|
||||
const capture = usePostHogClientCapture();
|
||||
return (
|
||||
<>
|
||||
{props.observations
|
||||
.sort((a, b) => a.startTime.getTime() - b.startTime.getTime())
|
||||
.map((observation) => {
|
||||
const collapsed = props.collapsedObservations.includes(
|
||||
observation.id,
|
||||
);
|
||||
return (
|
||||
<Fragment key={observation.id}>
|
||||
<div className="flex">
|
||||
{Array.from({ length: props.indentationLevel }, (_, i) => (
|
||||
<div className="mx-2 border-r" key={i} />
|
||||
))}
|
||||
<div
|
||||
className={cn(
|
||||
"group my-0.5 flex flex-1 cursor-pointer flex-col gap-1 rounded-sm p-1",
|
||||
props.currentObservationId === observation.id
|
||||
? "bg-gray-100"
|
||||
: "hover:bg-gray-50",
|
||||
)}
|
||||
</div>
|
||||
{props.showMetrics &&
|
||||
(observation.promptTokens ||
|
||||
observation.completionTokens ||
|
||||
observation.totalTokens ||
|
||||
observation.endTime) && (
|
||||
<div className="flex gap-2">
|
||||
{observation.endTime ? (
|
||||
<span className="text-xs text-gray-500">
|
||||
{formatIntervalSeconds(
|
||||
(observation.endTime.getTime() -
|
||||
observation.startTime.getTime()) /
|
||||
1000,
|
||||
)}
|
||||
</span>
|
||||
) : null}
|
||||
{observation.promptTokens ||
|
||||
observation.completionTokens ||
|
||||
observation.totalTokens ? (
|
||||
<span className="text-xs text-gray-500">
|
||||
{observation.promptTokens} →{" "}
|
||||
{observation.completionTokens} (∑{" "}
|
||||
{observation.totalTokens})
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
{observation.level !== "DEFAULT" ? (
|
||||
<div className="flex">
|
||||
<span
|
||||
className={cn(
|
||||
"rounded-sm p-0.5 text-xs",
|
||||
LevelColors[observation.level].bg,
|
||||
LevelColors[observation.level].text,
|
||||
)}
|
||||
>
|
||||
{observation.level}
|
||||
</span>
|
||||
</div>
|
||||
) : null}
|
||||
{props.showScores &&
|
||||
props.scores.find((s) => s.observationId === observation.id) ? (
|
||||
<div className="flex flex-wrap gap-1">
|
||||
<GroupedScoreBadges
|
||||
scores={props.scores.filter(
|
||||
(s) => s.observationId === observation.id,
|
||||
)}
|
||||
onClick={() => props.setCurrentObservationId(observation.id)}
|
||||
>
|
||||
<div className="flex gap-2">
|
||||
<ColorCodedObservationType
|
||||
observationType={observation.type}
|
||||
/>
|
||||
<span className="flex-1 break-all text-sm">
|
||||
{observation.name}
|
||||
</span>
|
||||
{observation.children.length === 0 ? null : (
|
||||
<Toggle
|
||||
onClick={(ev) => (
|
||||
ev.stopPropagation(),
|
||||
props.toggleCollapsedObservation(observation.id),
|
||||
capture(
|
||||
collapsed
|
||||
? "trace_detail:observation_tree_expand"
|
||||
: "trace_detail:observation_tree_collapse",
|
||||
{ type: "single" },
|
||||
)
|
||||
)}
|
||||
variant="default"
|
||||
pressed={collapsed}
|
||||
size="xs"
|
||||
className="w-7"
|
||||
title={
|
||||
collapsed ? "Expand children" : "Collapse children"
|
||||
}
|
||||
>
|
||||
{collapsed ? (
|
||||
<PlusIcon className="h-4 w-4" />
|
||||
) : (
|
||||
<MinusIcon className="h-4 w-4" />
|
||||
)}
|
||||
</Toggle>
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
{props.showMetrics &&
|
||||
(observation.promptTokens ||
|
||||
observation.completionTokens ||
|
||||
observation.totalTokens ||
|
||||
observation.endTime) && (
|
||||
<div className="flex gap-2">
|
||||
{observation.endTime ? (
|
||||
<span className="text-xs text-gray-500">
|
||||
{formatIntervalSeconds(
|
||||
(observation.endTime.getTime() -
|
||||
observation.startTime.getTime()) /
|
||||
1000,
|
||||
)}
|
||||
</span>
|
||||
) : null}
|
||||
{observation.promptTokens ||
|
||||
observation.completionTokens ||
|
||||
observation.totalTokens ? (
|
||||
<span className="text-xs text-gray-500">
|
||||
{observation.promptTokens} →{" "}
|
||||
{observation.completionTokens} (∑{" "}
|
||||
{observation.totalTokens})
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
{observation.level !== "DEFAULT" ? (
|
||||
<div className="flex">
|
||||
<span
|
||||
className={cn(
|
||||
"rounded-sm p-0.5 text-xs",
|
||||
LevelColors[observation.level].bg,
|
||||
LevelColors[observation.level].text,
|
||||
)}
|
||||
>
|
||||
{observation.level}
|
||||
</span>
|
||||
</div>
|
||||
) : null}
|
||||
{props.showScores &&
|
||||
props.scores.find(
|
||||
(s) => s.observationId === observation.id,
|
||||
) ? (
|
||||
<div className="flex flex-wrap gap-1">
|
||||
<GroupedScoreBadges
|
||||
scores={props.scores.filter(
|
||||
(s) => s.observationId === observation.id,
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{!collapsed && (
|
||||
<ObservationTreeNode
|
||||
observations={observation.children}
|
||||
collapsedObservations={props.collapsedObservations}
|
||||
toggleCollapsedObservation={props.toggleCollapsedObservation}
|
||||
scores={props.scores}
|
||||
indentationLevel={props.indentationLevel + 1}
|
||||
currentObservationId={props.currentObservationId}
|
||||
setCurrentObservationId={props.setCurrentObservationId}
|
||||
showMetrics={props.showMetrics}
|
||||
showScores={props.showScores}
|
||||
/>
|
||||
)}
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
{!collapsed && (
|
||||
<ObservationTreeNode
|
||||
observations={observation.children}
|
||||
collapsedObservations={props.collapsedObservations}
|
||||
toggleCollapsedObservation={props.toggleCollapsedObservation}
|
||||
scores={props.scores}
|
||||
indentationLevel={props.indentationLevel + 1}
|
||||
currentObservationId={props.currentObservationId}
|
||||
setCurrentObservationId={props.setCurrentObservationId}
|
||||
showMetrics={props.showMetrics}
|
||||
showScores={props.showScores}
|
||||
/>
|
||||
)}
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const ColorCodedObservationType = (props: {
|
||||
observationType: $Enums.ObservationType;
|
||||
|
||||
@@ -23,6 +23,7 @@ import { usdFormatter } from "@/src/utils/numbers";
|
||||
import Decimal from "decimal.js";
|
||||
import { useCallback, useState } from "react";
|
||||
import { DeleteButton } from "@/src/components/deleteButton";
|
||||
import { usePostHogClientCapture } from "@/src/features/posthog-analytics/usePostHogClientCapture";
|
||||
|
||||
export function Trace(props: {
|
||||
observations: Array<ObservationReturnType>;
|
||||
@@ -30,6 +31,7 @@ export function Trace(props: {
|
||||
scores: Score[];
|
||||
projectId: string;
|
||||
}) {
|
||||
const capture = usePostHogClientCapture();
|
||||
const [currentObservationId, setCurrentObservationId] = useQueryParam(
|
||||
"observation",
|
||||
StringParam,
|
||||
@@ -77,17 +79,19 @@ export function Trace(props: {
|
||||
.filter((id) => !excludeParentObservations.has(id)),
|
||||
);
|
||||
} while (newExcludeParentObservations.size > 0);
|
||||
|
||||
capture("trace_detail:observation_tree_collapse", { type: "all" });
|
||||
setCollapsedObservations(
|
||||
props.observations
|
||||
.map((o) => o.id)
|
||||
.filter((id) => !excludeParentObservations.has(id)),
|
||||
);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [props.observations, currentObservationId]);
|
||||
|
||||
const expandAll = useCallback(() => {
|
||||
capture("trace_detail:observation_tree_expand", { type: "all" });
|
||||
setCollapsedObservations([]);
|
||||
}, [setCollapsedObservations]);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="grid gap-4 md:h-full md:grid-cols-5 lg:grid-cols-6 xl:grid-cols-7">
|
||||
@@ -115,6 +119,9 @@ export function Trace(props: {
|
||||
<Toggle
|
||||
pressed={scoresOnObservationTree}
|
||||
onPressedChange={(e) => {
|
||||
capture("trace_detail:observation_tree_toggle_scores", {
|
||||
show: e,
|
||||
});
|
||||
setScoresOnObservationTree(e);
|
||||
}}
|
||||
size="sm"
|
||||
@@ -125,6 +132,9 @@ export function Trace(props: {
|
||||
<Toggle
|
||||
pressed={metricsOnObservationTree}
|
||||
onPressedChange={(e) => {
|
||||
capture("trace_detail:observation_tree_toggle_metrics", {
|
||||
show: e,
|
||||
});
|
||||
setMetricsOnObservationTree(e);
|
||||
}}
|
||||
size="sm"
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as AccordionPrimitive from "@radix-ui/react-accordion"
|
||||
import { ChevronDown } from "lucide-react"
|
||||
|
||||
import { cn } from "@/src/utils/tailwind"
|
||||
|
||||
const Accordion = AccordionPrimitive.Root
|
||||
|
||||
const AccordionItem = React.forwardRef<
|
||||
React.ElementRef<typeof AccordionPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AccordionPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn("border-b", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
AccordionItem.displayName = "AccordionItem"
|
||||
|
||||
const AccordionTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof AccordionPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<AccordionPrimitive.Header className="flex">
|
||||
<AccordionPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex flex-1 items-center justify-between py-4 font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronDown className="h-4 w-4 shrink-0 transition-transform duration-200" />
|
||||
</AccordionPrimitive.Trigger>
|
||||
</AccordionPrimitive.Header>
|
||||
))
|
||||
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName
|
||||
|
||||
const AccordionContent = React.forwardRef<
|
||||
React.ElementRef<typeof AccordionPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<AccordionPrimitive.Content
|
||||
ref={ref}
|
||||
className="overflow-hidden text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
|
||||
{...props}
|
||||
>
|
||||
<div className={cn("pb-4 pt-0", className)}>{children}</div>
|
||||
</AccordionPrimitive.Content>
|
||||
))
|
||||
|
||||
AccordionContent.displayName = AccordionPrimitive.Content.displayName
|
||||
|
||||
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
|
||||
@@ -1 +1 @@
|
||||
export const VERSION = "v2.37.2";
|
||||
export const VERSION = "v2.41.0";
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
import { Label } from "@/src/components/ui/label";
|
||||
import TableLink from "@/src/components/table/table-link";
|
||||
import EvalLogTable from "@/src/ee/features/evals/components/eval-log";
|
||||
import { usePostHogClientCapture } from "@/src/features/posthog-analytics/usePostHogClientCapture";
|
||||
|
||||
export const EvalConfigDetail = () => {
|
||||
const router = useRouter();
|
||||
@@ -122,8 +123,9 @@ export function DeactivateConfig({
|
||||
isLoading: boolean;
|
||||
}) {
|
||||
const utils = api.useUtils();
|
||||
const hasAccess = useHasAccess({ projectId, scope: "job:CUD" });
|
||||
const hasAccess = useHasAccess({ projectId, scope: "evalJob:CUD" });
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const capture = usePostHogClientCapture();
|
||||
|
||||
const mutEvalConfig = api.evals.updateEvalJob.useMutation({
|
||||
onSuccess: () => {
|
||||
@@ -141,6 +143,7 @@ export function DeactivateConfig({
|
||||
evalConfigId: config?.id ?? "",
|
||||
updatedStatus: "INACTIVE",
|
||||
});
|
||||
capture("eval_config:delete");
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { usePostHog } from "posthog-js/react";
|
||||
import { useFieldArray, useForm } from "react-hook-form";
|
||||
import { Input } from "@/src/components/ui/input";
|
||||
import { Button } from "@/src/components/ui/button";
|
||||
@@ -42,6 +41,7 @@ import { Card } from "@/src/components/ui/card";
|
||||
import { JSONView } from "@/src/components/ui/CodeJsonViewer";
|
||||
import { Label } from "@/src/components/ui/label";
|
||||
import DocPopup from "@/src/components/layouts/doc-popup";
|
||||
import { usePostHogClientCapture } from "@/src/features/posthog-analytics/usePostHogClientCapture";
|
||||
|
||||
const formSchema = z.object({
|
||||
scoreName: z.string(),
|
||||
@@ -113,7 +113,7 @@ export const InnerEvalConfigForm = (props: {
|
||||
onFormSuccess?: () => void;
|
||||
}) => {
|
||||
const [formError, setFormError] = useState<string | null>(null);
|
||||
const posthog = usePostHog();
|
||||
const capture = usePostHogClientCapture();
|
||||
|
||||
const form = useForm<z.infer<typeof formSchema>>({
|
||||
resolver: zodResolver(formSchema),
|
||||
@@ -182,7 +182,7 @@ export const InnerEvalConfigForm = (props: {
|
||||
});
|
||||
|
||||
function onSubmit(values: z.infer<typeof formSchema>) {
|
||||
posthog.capture("evals:new_config_form");
|
||||
capture("eval_config:new_form_submit");
|
||||
|
||||
const validatedFilter = z.array(singleFilter).safeParse(values.filter);
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ import { useHasAccess } from "@/src/features/rbac/utils/checkAccess";
|
||||
import { Button } from "@/src/components/ui/button";
|
||||
import { Pencil } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { usePostHogClientCapture } from "@/src/features/posthog-analytics/usePostHogClientCapture";
|
||||
|
||||
export const EvalTemplateDetail = () => {
|
||||
const router = useRouter();
|
||||
@@ -92,12 +93,14 @@ export function EvalVersionDropdown(props: {
|
||||
defaultOption?: EvalTemplate;
|
||||
onSelect?: (template: EvalTemplate) => void;
|
||||
}) {
|
||||
const capture = usePostHogClientCapture();
|
||||
const handleSelect = (value: string) => {
|
||||
const selectedTemplate = props.options?.find(
|
||||
(template) => template.id === value,
|
||||
);
|
||||
if (selectedTemplate && props.onSelect) {
|
||||
props.onSelect(selectedTemplate);
|
||||
capture("eval_templates:view_version");
|
||||
}
|
||||
};
|
||||
|
||||
@@ -133,9 +136,11 @@ export function UpdateTemplate({
|
||||
setIsEditing: (isEditing: boolean) => void;
|
||||
}) {
|
||||
const hasAccess = useHasAccess({ projectId, scope: "evalTemplate:create" });
|
||||
const capture = usePostHogClientCapture();
|
||||
|
||||
const handlePromptEdit = () => {
|
||||
setIsEditing(true);
|
||||
capture("eval_templates:update_form_open");
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { usePostHog } from "posthog-js/react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import * as z from "zod";
|
||||
@@ -45,6 +44,7 @@ import {
|
||||
import { TEMPLATES } from "@/src/ee/features/evals/components/templates";
|
||||
import { Label } from "@/src/components/ui/label";
|
||||
import { useHasAccess } from "@/src/features/rbac/utils/checkAccess";
|
||||
import { usePostHogClientCapture } from "@/src/features/posthog-analytics/usePostHogClientCapture";
|
||||
|
||||
export const EvalTemplateForm = (props: {
|
||||
projectId: string;
|
||||
@@ -186,7 +186,7 @@ export const InnerEvalTemplateForm = (props: {
|
||||
isEditing?: boolean;
|
||||
setIsEditing?: (isEditing: boolean) => void;
|
||||
}) => {
|
||||
const posthog = usePostHog();
|
||||
const capture = usePostHogClientCapture();
|
||||
const [formError, setFormError] = useState<string | null>(null);
|
||||
|
||||
// updates the model params based on the pre-filled data
|
||||
@@ -281,7 +281,11 @@ export const InnerEvalTemplateForm = (props: {
|
||||
});
|
||||
|
||||
function onSubmit(values: z.infer<typeof formSchema>) {
|
||||
posthog.capture("evals:new_template_form");
|
||||
capture(
|
||||
props.isEditing
|
||||
? "eval_templates:update_form_submit"
|
||||
: "eval_templates:new_form_submit",
|
||||
);
|
||||
|
||||
const model = EvalModelNames.safeParse(modelParams.model);
|
||||
|
||||
|
||||
@@ -5,14 +5,15 @@ import Link from "next/link";
|
||||
import { useHasAccess } from "@/src/features/rbac/utils/checkAccess";
|
||||
import { Lock } from "lucide-react";
|
||||
import EvalConfigTable from "@/src/ee/features/evals/components/eval-config-table";
|
||||
import { usePostHogClientCapture } from "@/src/features/posthog-analytics/usePostHogClientCapture";
|
||||
|
||||
export default function ConfigsPage() {
|
||||
const router = useRouter();
|
||||
const projectId = router.query.projectId as string;
|
||||
|
||||
const capture = usePostHogClientCapture();
|
||||
const hasWriteAccess = useHasAccess({
|
||||
projectId,
|
||||
scope: "job:CUD",
|
||||
scope: "evalJob:CUD",
|
||||
});
|
||||
|
||||
return (
|
||||
@@ -20,11 +21,16 @@ export default function ConfigsPage() {
|
||||
<Header
|
||||
title="Eval configs"
|
||||
help={{
|
||||
description: "XXX",
|
||||
href: "https://langfuse.com/docs/evals",
|
||||
description:
|
||||
"Eval configs let you define how your evaluations templates are applied to incoming traces in Langfuse.",
|
||||
href: "https://langfuse.com/docs/scores/model-based-evals",
|
||||
}}
|
||||
actionButtons={
|
||||
<Button disabled={!hasWriteAccess} asChild>
|
||||
<Button
|
||||
disabled={!hasWriteAccess}
|
||||
onClick={() => capture("eval_config:new_form_open")}
|
||||
asChild
|
||||
>
|
||||
<Link
|
||||
href={
|
||||
hasWriteAccess ? `/project/${projectId}/evals/configs/new` : "#"
|
||||
|
||||
@@ -16,7 +16,14 @@ export default function NewConfigsPage() {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Header title="Create eval config" />
|
||||
<Header
|
||||
title="Create eval config"
|
||||
help={{
|
||||
description:
|
||||
"Eval configs let you define how your evaluations templates are applied to incoming traces in Langfuse.",
|
||||
href: "https://langfuse.com/docs/scores/model-based-evals",
|
||||
}}
|
||||
/>
|
||||
<EvalConfigForm
|
||||
projectId={projectId}
|
||||
evalTemplates={evalTemplates.data?.templates ?? []}
|
||||
|
||||
@@ -16,7 +16,14 @@ export default function NewTemplatesPage() {
|
||||
|
||||
return (
|
||||
<div className="md:container">
|
||||
<Header title="Create eval template" />
|
||||
<Header
|
||||
title="Create eval template"
|
||||
help={{
|
||||
description:
|
||||
"Create an evaluation template. Choose from one of the pre-defined templates or create your own.",
|
||||
href: "https://langfuse.com/docs/scores/model-based-evals",
|
||||
}}
|
||||
/>
|
||||
<EvalTemplateForm projectId={projectId} isEditing={true} />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -5,11 +5,12 @@ import Link from "next/link";
|
||||
import { useHasAccess } from "@/src/features/rbac/utils/checkAccess";
|
||||
import { Lock } from "lucide-react";
|
||||
import EvalsTemplateTable from "@/src/ee/features/evals/components/eval-templates-table";
|
||||
import { usePostHogClientCapture } from "@/src/features/posthog-analytics/usePostHogClientCapture";
|
||||
|
||||
export default function TemplatesPage() {
|
||||
const router = useRouter();
|
||||
const projectId = router.query.projectId as string;
|
||||
|
||||
const capture = usePostHogClientCapture();
|
||||
const hasWriteAccess = useHasAccess({
|
||||
projectId,
|
||||
scope: "evalTemplate:create",
|
||||
@@ -20,11 +21,16 @@ export default function TemplatesPage() {
|
||||
<Header
|
||||
title="Eval Templates"
|
||||
help={{
|
||||
description: "XXX",
|
||||
href: "https://langfuse.com/docs/evals",
|
||||
description:
|
||||
"Create an evaluation template. Choose from one of the pre-defined templates or create your own.",
|
||||
href: "https://langfuse.com/docs/scores/model-based-evals",
|
||||
}}
|
||||
actionButtons={
|
||||
<Button disabled={!hasWriteAccess} asChild>
|
||||
<Button
|
||||
disabled={!hasWriteAccess}
|
||||
onClick={() => capture("eval_templates:new_form_open")}
|
||||
asChild
|
||||
>
|
||||
<Link
|
||||
href={
|
||||
hasWriteAccess
|
||||
|
||||
@@ -45,7 +45,7 @@ export const evalRouter = createTRPCRouter({
|
||||
throwIfNoAccess({
|
||||
session: ctx.session,
|
||||
projectId: input.projectId,
|
||||
scope: "job:read",
|
||||
scope: "evalJob:read",
|
||||
});
|
||||
|
||||
const configs = await ctx.prisma.jobConfiguration.findMany({
|
||||
@@ -86,7 +86,7 @@ export const evalRouter = createTRPCRouter({
|
||||
throwIfNoAccess({
|
||||
session: ctx.session,
|
||||
projectId: input.projectId,
|
||||
scope: "job:read",
|
||||
scope: "evalJob:read",
|
||||
});
|
||||
|
||||
const config = await ctx.prisma.jobConfiguration.findUnique({
|
||||
@@ -249,7 +249,7 @@ export const evalRouter = createTRPCRouter({
|
||||
filter: z.array(singleFilter).nullable(), // re-using the filter type from the tables
|
||||
mapping: z.array(variableMapping),
|
||||
sampling: z.number().gte(0).lte(1),
|
||||
delay: z.number().gte(0).default(10_000),
|
||||
delay: z.number().gte(0).default(DEFAULT_TRACE_JOB_DELAY), // 10 seconds default
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
@@ -260,7 +260,7 @@ export const evalRouter = createTRPCRouter({
|
||||
throwIfNoAccess({
|
||||
session: ctx.session,
|
||||
projectId: input.projectId,
|
||||
scope: "job:CUD",
|
||||
scope: "evalJob:CUD",
|
||||
});
|
||||
|
||||
const evalTemplate = await ctx.prisma.evalTemplate.findUnique({
|
||||
@@ -287,7 +287,7 @@ export const evalRouter = createTRPCRouter({
|
||||
filter: input.filter ?? [],
|
||||
variableMapping: input.mapping,
|
||||
sampling: input.sampling,
|
||||
delay: DEFAULT_TRACE_JOB_DELAY, // 10 seconds default
|
||||
delay: input.delay,
|
||||
status: "ACTIVE",
|
||||
},
|
||||
});
|
||||
@@ -359,7 +359,7 @@ export const evalRouter = createTRPCRouter({
|
||||
throwIfNoAccess({
|
||||
session: ctx.session,
|
||||
projectId: input.projectId,
|
||||
scope: "job:CUD",
|
||||
scope: "evalJob:CUD",
|
||||
});
|
||||
|
||||
await ctx.prisma.jobConfiguration.update({
|
||||
@@ -396,7 +396,7 @@ export const evalRouter = createTRPCRouter({
|
||||
throwIfNoAccess({
|
||||
session: ctx.session,
|
||||
projectId: input.projectId,
|
||||
scope: "job:read",
|
||||
scope: "evalJob:read",
|
||||
});
|
||||
|
||||
const jobExecutions = await ctx.prisma.jobExecution.findMany({
|
||||
|
||||
@@ -27,6 +27,7 @@ import {
|
||||
|
||||
import type { MessagesContext } from "@/src/components/ChatMessages/types";
|
||||
import type { ModelParamsContext } from "@/src/components/ModelParameters";
|
||||
import { usePostHogClientCapture } from "@/src/features/posthog-analytics/usePostHogClientCapture";
|
||||
|
||||
type PlaygroundContextType = {
|
||||
promptVariables: PromptVariable[];
|
||||
@@ -60,6 +61,7 @@ export const PlaygroundProvider: React.FC<PropsWithChildren> = ({
|
||||
children,
|
||||
}) => {
|
||||
const projectId = useProjectIdFromURL();
|
||||
const capture = usePostHogClientCapture();
|
||||
const [initialPromptId] = useQueryParam("promptId", StringParam);
|
||||
const [promptVariables, setPromptVariables] = useState<PromptVariable[]>([]);
|
||||
const [output, setOutput] = useState("");
|
||||
@@ -188,6 +190,12 @@ export const PlaygroundProvider: React.FC<PropsWithChildren> = ({
|
||||
setOutput(response);
|
||||
}
|
||||
setOutputJson(getOutputJson(response, finalMessages, modelParams));
|
||||
capture("playground:execute_button_click", {
|
||||
inputLength: finalMessages.length,
|
||||
modelName: modelParams.model,
|
||||
modelProvider: modelParams.provider,
|
||||
outputLength: response.length,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
|
||||
@@ -196,6 +204,7 @@ export const PlaygroundProvider: React.FC<PropsWithChildren> = ({
|
||||
} finally {
|
||||
setIsStreaming(false);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [messages, modelParams, promptVariables]);
|
||||
|
||||
useCommandEnter(!isStreaming, handleSubmit);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { ValidatedChatCompletionBody } from "@/src/ee/features/playground/server/validateChatCompletionBody";
|
||||
import { ServerPosthog } from "@/src/server/services/posthog";
|
||||
import { ServerPosthog } from "@/src/features/posthog-analytics/ServerPosthog";
|
||||
import type { LLMResult } from "@langchain/core/outputs";
|
||||
import { BaseCallbackHandler } from "@langchain/core/callbacks/base";
|
||||
|
||||
|
||||
@@ -30,6 +30,14 @@ export const env = createEnv({
|
||||
LANGFUSE_NEW_USER_SIGNUP_WEBHOOK: z.string().url().optional(),
|
||||
// Add `.min(1) on ID and SECRET if you want to make sure they're not empty
|
||||
LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES: z.enum(["true", "false"]).optional(),
|
||||
LANGFUSE_DEFAULT_TABLE_DATETIME_OFFSET: z.string().optional().refine((v) =>
|
||||
v === undefined || !isNaN(Number.parseInt(v))
|
||||
, {
|
||||
message: "LANGFUSE_DEFAULT_TABLE_DATETIME_OFFSET must be a number when set"
|
||||
}).transform((v) =>
|
||||
v === undefined ? undefined : -Number.parseInt(v) // negative offset
|
||||
),
|
||||
LANGFUSE_DISABLE_EXPENSIVE_POSTGRES_QUERIES: z.enum(["true", "false"]).optional().default("false"),
|
||||
SALT: z.string({
|
||||
required_error:
|
||||
"A strong Salt is required to encrypt API keys securely. See: https://langfuse.com/docs/deployment/self-host#deploy-the-container",
|
||||
@@ -121,6 +129,10 @@ export const env = createEnv({
|
||||
NEXT_PUBLIC_SIGN_UP_DISABLED: process.env.NEXT_PUBLIC_SIGN_UP_DISABLED,
|
||||
LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES:
|
||||
process.env.LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES,
|
||||
LANGFUSE_DEFAULT_TABLE_DATETIME_OFFSET:
|
||||
process.env.LANGFUSE_DEFAULT_TABLE_DATETIME_OFFSET,
|
||||
LANGFUSE_DISABLE_EXPENSIVE_POSTGRES_QUERIES:
|
||||
process.env.LANGFUSE_DISABLE_EXPENSIVE_POSTGRES_QUERIES,
|
||||
LANGFUSE_TEAM_SLACK_WEBHOOK: process.env.LANGFUSE_TEAM_SLACK_WEBHOOK,
|
||||
LANGFUSE_TEAM_BETTERSTACK_TOKEN:
|
||||
process.env.LANGFUSE_TEAM_BETTERSTACK_TOKEN,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { prisma as _prisma } from "@langfuse/shared/src/db";
|
||||
import { type MembershipRole } from "@langfuse/shared";
|
||||
import { type ProjectRole } from "@langfuse/shared";
|
||||
|
||||
export type AuditableResource =
|
||||
| "membership"
|
||||
@@ -29,14 +29,14 @@ type AuditLog = {
|
||||
| {
|
||||
projectId: string;
|
||||
userId: string;
|
||||
userProjectRole: MembershipRole;
|
||||
userProjectRole: ProjectRole;
|
||||
}
|
||||
| {
|
||||
session: {
|
||||
user: {
|
||||
id: string;
|
||||
};
|
||||
projectRole: MembershipRole;
|
||||
projectRole: ProjectRole;
|
||||
projectId: string;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -5,12 +5,12 @@ export const CloudPrivacyNotice = ({ action }: { action: string }) =>
|
||||
<div className="mt-5 text-center text-xs text-gray-500">
|
||||
By {action} you are agreeing to our{" "}
|
||||
<a
|
||||
href="https://langfuse.com/tos"
|
||||
href="https://langfuse.com/terms"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="italic"
|
||||
>
|
||||
Terms of Service
|
||||
Terms and Conditions
|
||||
</a>
|
||||
,{" "}
|
||||
<a
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { env } from "@/src/env.mjs";
|
||||
import { Tabs, TabsList, TabsTrigger } from "@/src/components/ui/tabs";
|
||||
import { Divider } from "@tremor/react";
|
||||
import { usePostHog } from "posthog-js/react";
|
||||
import { usePostHogClientCapture } from "@/src/features/posthog-analytics/usePostHogClientCapture";
|
||||
|
||||
const regions =
|
||||
env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION === "STAGING"
|
||||
@@ -37,7 +37,7 @@ export function CloudRegionSwitch({
|
||||
}: {
|
||||
isSignUpPage?: boolean;
|
||||
}) {
|
||||
const posthog = usePostHog();
|
||||
const capture = usePostHogClientCapture();
|
||||
|
||||
if (env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION === undefined) return null;
|
||||
|
||||
@@ -64,8 +64,8 @@ export function CloudRegionSwitch({
|
||||
key={region.name}
|
||||
value={region.name}
|
||||
onClick={() => {
|
||||
posthog.capture(
|
||||
"cloud_region_switch",
|
||||
capture(
|
||||
"sign_in:cloud_region_switch",
|
||||
{
|
||||
region: region.name,
|
||||
},
|
||||
|
||||
@@ -17,7 +17,7 @@ export async function createProjectMembershipsOnSignup(user: {
|
||||
)?.id
|
||||
: undefined;
|
||||
if (demoProjectId !== undefined) {
|
||||
await prisma.membership.create({
|
||||
await prisma.projectMembership.create({
|
||||
data: {
|
||||
projectId: demoProjectId,
|
||||
userId: user.id,
|
||||
@@ -37,7 +37,7 @@ export async function createProjectMembershipsOnSignup(user: {
|
||||
)?.id
|
||||
: undefined;
|
||||
if (defaultProjectID !== undefined) {
|
||||
await prisma.membership.create({
|
||||
await prisma.projectMembership.create({
|
||||
data: {
|
||||
projectId: defaultProjectID,
|
||||
userId: user.id,
|
||||
@@ -69,7 +69,7 @@ async function processMembershipInvitations(email: string, userId: string) {
|
||||
});
|
||||
|
||||
await prisma.$transaction([
|
||||
prisma.membership.createMany({
|
||||
prisma.projectMembership.createMany({
|
||||
data: membershipsData,
|
||||
}),
|
||||
prisma.membershipInvitation.deleteMany({
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { usePostHogClientCapture } from "@/src/features/posthog-analytics/usePostHogClientCapture";
|
||||
import { cn } from "@/src/utils/tailwind";
|
||||
import { type ReactNode, useState } from "react";
|
||||
|
||||
@@ -10,6 +11,7 @@ export type TabComponentProps = {
|
||||
|
||||
export const TabComponent = ({ tabs }: TabComponentProps) => {
|
||||
const [selectedIndex, setSelectedIndex] = useState(0);
|
||||
const capture = usePostHogClientCapture();
|
||||
return (
|
||||
<div>
|
||||
<div className="sm:hidden">
|
||||
@@ -44,7 +46,12 @@ export const TabComponent = ({ tabs }: TabComponentProps) => {
|
||||
"cursor-pointer whitespace-nowrap border-b-2 px-1 py-3 text-sm font-medium",
|
||||
)}
|
||||
aria-current={index === selectedIndex ? "page" : undefined}
|
||||
onClick={() => setSelectedIndex(index)}
|
||||
onClick={() => {
|
||||
setSelectedIndex(index);
|
||||
capture("dashboard:chart_tab_switch", {
|
||||
tabLabel: tab.tabTitle,
|
||||
});
|
||||
}}
|
||||
>
|
||||
{tab.tabTitle}
|
||||
</a>
|
||||
|
||||
@@ -12,6 +12,7 @@ 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";
|
||||
import { usePostHogClientCapture } from "@/src/features/posthog-analytics/usePostHogClientCapture";
|
||||
|
||||
interface BaseDatasetButtonProps {
|
||||
mode: "create" | "update" | "delete";
|
||||
@@ -44,6 +45,7 @@ type DatasetActionButtonProps =
|
||||
| DeleteDatasetButtonProps;
|
||||
|
||||
export const DatasetActionButton = (props: DatasetActionButtonProps) => {
|
||||
const capture = usePostHogClientCapture();
|
||||
const [open, setOpen] = useState(false);
|
||||
const hasAccess = useHasAccess({
|
||||
projectId: props.projectId,
|
||||
@@ -60,13 +62,23 @@ export const DatasetActionButton = (props: DatasetActionButtonProps) => {
|
||||
size={"icon"}
|
||||
className={props.className}
|
||||
disabled={!hasAccess}
|
||||
onClick={() =>
|
||||
capture("datasets:update_form_open", {
|
||||
source: "dataset",
|
||||
})
|
||||
}
|
||||
>
|
||||
<Edit className="h-4 w-4" />
|
||||
</Button>
|
||||
) : (
|
||||
<div
|
||||
className="relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors hover:bg-accent hover:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50"
|
||||
onClick={() => setOpen(true)}
|
||||
onClick={() => {
|
||||
setOpen(true);
|
||||
capture("datasets:update_form_open", {
|
||||
source: "table-single-row",
|
||||
});
|
||||
}}
|
||||
>
|
||||
{hasAccess ? (
|
||||
<Edit className="mr-2 h-4 w-4" />
|
||||
@@ -79,13 +91,22 @@ export const DatasetActionButton = (props: DatasetActionButtonProps) => {
|
||||
) : props.mode === "delete" ? (
|
||||
<div
|
||||
className="relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors hover:bg-accent hover:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50"
|
||||
onClick={() => setOpen(true)}
|
||||
onClick={() => {
|
||||
setOpen(true);
|
||||
capture("datasets:delete_form_open", {
|
||||
source: "table-single-row",
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Trash className="mr-2 h-4 w-4" />
|
||||
Delete
|
||||
</div>
|
||||
) : (
|
||||
<Button className={props.className} disabled={!hasAccess}>
|
||||
<Button
|
||||
className={props.className}
|
||||
disabled={!hasAccess}
|
||||
onClick={() => capture("datasets:new_form_open")}
|
||||
>
|
||||
{hasAccess ? (
|
||||
<PlusIcon className="-ml-0.5 mr-1.5" aria-hidden="true" />
|
||||
) : (
|
||||
|
||||
@@ -12,10 +12,10 @@ import {
|
||||
} from "@/src/components/ui/form";
|
||||
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";
|
||||
import { usePostHogClientCapture } from "@/src/features/posthog-analytics/usePostHogClientCapture";
|
||||
|
||||
interface BaseDatasetFormProps {
|
||||
mode: "create" | "update" | "delete";
|
||||
@@ -73,7 +73,7 @@ const formSchema = z.object({
|
||||
|
||||
export const DatasetForm = (props: DatasetFormProps) => {
|
||||
const [formError, setFormError] = useState<string | null>(null);
|
||||
const posthog = usePostHog();
|
||||
const capture = usePostHogClientCapture();
|
||||
const form = useForm<z.infer<typeof formSchema>>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues:
|
||||
@@ -104,7 +104,7 @@ export const DatasetForm = (props: DatasetFormProps) => {
|
||||
description: values.description !== "" ? values.description.trim() : null,
|
||||
};
|
||||
if (props.mode === "create") {
|
||||
posthog.capture("datasets:new_dataset_form_submit");
|
||||
capture("datasets:new_form_submit");
|
||||
createMutation
|
||||
.mutateAsync({
|
||||
...trimmedValues,
|
||||
@@ -120,7 +120,7 @@ export const DatasetForm = (props: DatasetFormProps) => {
|
||||
console.error(error);
|
||||
});
|
||||
} else if (props.mode === "update") {
|
||||
posthog.capture("datasets:update_dataset_form_submit");
|
||||
capture("datasets:update_form_submit");
|
||||
renameMutation
|
||||
.mutateAsync({
|
||||
...trimmedValues,
|
||||
@@ -141,7 +141,7 @@ export const DatasetForm = (props: DatasetFormProps) => {
|
||||
|
||||
const handleDelete = () => {
|
||||
if (props.mode !== "delete") return;
|
||||
posthog.capture("datasets:delete_dataset_form_submit");
|
||||
capture("datasets:delete_form_submit");
|
||||
deleteMutation
|
||||
.mutateAsync({
|
||||
projectId: props.projectId,
|
||||
|
||||
@@ -22,6 +22,7 @@ import { DataTableToolbar } from "@/src/components/table/data-table-toolbar";
|
||||
import useColumnVisibility from "@/src/features/column-visibility/hooks/useColumnVisibility";
|
||||
import { useRowHeightLocalStorage } from "@/src/components/table/data-table-row-height-switch";
|
||||
import { IOTableCell } from "@/src/components/ui/CodeJsonViewer";
|
||||
import { usePostHogClientCapture } from "@/src/features/posthog-analytics/usePostHogClientCapture";
|
||||
|
||||
type RowData = {
|
||||
id: string;
|
||||
@@ -47,7 +48,7 @@ export function DatasetItemsTable({
|
||||
}) {
|
||||
const { setDetailPageList } = useDetailPageLists();
|
||||
const utils = api.useUtils();
|
||||
|
||||
const capture = usePostHogClientCapture();
|
||||
const [paginationState, setPaginationState] = useQueryParams({
|
||||
pageIndex: withDefault(NumberParam, 0),
|
||||
pageSize: withDefault(NumberParam, 50),
|
||||
@@ -208,7 +209,13 @@ export function DatasetItemsTable({
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
||||
<DropdownMenuItem
|
||||
onClick={() =>
|
||||
onClick={() => {
|
||||
capture("dataset_item:archive_toggle", {
|
||||
status:
|
||||
status === DatasetStatus.ARCHIVED
|
||||
? "unarchived"
|
||||
: "archived",
|
||||
});
|
||||
mutUpdate.mutate({
|
||||
projectId: projectId,
|
||||
datasetId: datasetId,
|
||||
@@ -217,8 +224,8 @@ export function DatasetItemsTable({
|
||||
status === DatasetStatus.ARCHIVED
|
||||
? DatasetStatus.ACTIVE
|
||||
: DatasetStatus.ARCHIVED,
|
||||
})
|
||||
}
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Archive className="mr-2 h-4 w-4" />
|
||||
{status === DatasetStatus.ARCHIVED ? "Unarchive" : "Archive"}
|
||||
|
||||
@@ -150,6 +150,7 @@ export const EditDatasetItem = ({
|
||||
field.onChange(v);
|
||||
}}
|
||||
editable={hasAccess}
|
||||
className="max-h-[600px] overflow-y-auto"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
@@ -170,6 +171,7 @@ export const EditDatasetItem = ({
|
||||
field.onChange(v);
|
||||
}}
|
||||
editable={hasAccess}
|
||||
className="max-h-[600px] overflow-y-auto"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
@@ -191,6 +193,7 @@ export const EditDatasetItem = ({
|
||||
field.onChange(v);
|
||||
}}
|
||||
editable={hasAccess}
|
||||
className="max-h-[300px] overflow-y-auto"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
|
||||
@@ -9,6 +9,7 @@ import { useState } from "react";
|
||||
import { NewDatasetItemForm } from "@/src/features/datasets/components/NewDatasetItemForm";
|
||||
import { DialogTrigger } from "@radix-ui/react-dialog";
|
||||
import { useHasAccess } from "@/src/features/rbac/utils/checkAccess";
|
||||
import { usePostHogClientCapture } from "@/src/features/posthog-analytics/usePostHogClientCapture";
|
||||
|
||||
export const NewDatasetItemButton = (props: {
|
||||
projectId: string;
|
||||
@@ -20,6 +21,7 @@ export const NewDatasetItemButton = (props: {
|
||||
projectId: props.projectId,
|
||||
scope: "datasets:CUD",
|
||||
});
|
||||
const capture = usePostHogClientCapture();
|
||||
return (
|
||||
<Dialog open={hasAccess && open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>
|
||||
@@ -27,6 +29,7 @@ export const NewDatasetItemButton = (props: {
|
||||
variant="secondary"
|
||||
className={props.className}
|
||||
disabled={!hasAccess}
|
||||
onClick={() => capture("dataset_item:new_form_open")}
|
||||
>
|
||||
{hasAccess ? (
|
||||
<PlusIcon className="-ml-0.5 mr-1.5" aria-hidden="true" />
|
||||
|
||||
@@ -19,10 +19,10 @@ import {
|
||||
} from "@/src/components/ui/select";
|
||||
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";
|
||||
import { cn } from "@/src/utils/tailwind";
|
||||
import { usePostHogClientCapture } from "@/src/features/posthog-analytics/usePostHogClientCapture";
|
||||
|
||||
const formSchema = z.object({
|
||||
datasetId: z.string().min(1, "Select a dataset"),
|
||||
@@ -85,7 +85,7 @@ export const NewDatasetItemForm = (props: {
|
||||
onFormSuccess?: () => void;
|
||||
}) => {
|
||||
const [formError, setFormError] = useState<string | null>(null);
|
||||
const posthog = usePostHog();
|
||||
const capture = usePostHogClientCapture();
|
||||
const form = useForm<z.infer<typeof formSchema>>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
@@ -107,10 +107,13 @@ export const NewDatasetItemForm = (props: {
|
||||
});
|
||||
|
||||
function onSubmit(values: z.infer<typeof formSchema>) {
|
||||
posthog.capture("datasets:new_dataset_item_form_submit", {
|
||||
hasSourceTrace: !!props.traceId,
|
||||
hasSourceObservation: !!props.observationId,
|
||||
});
|
||||
if (props.traceId) {
|
||||
capture("dataset_item:new_from_trace_form_submit", {
|
||||
object: props.observationId ? "observation" : "trace",
|
||||
});
|
||||
} else {
|
||||
capture("dataset_item:new_form_submit");
|
||||
}
|
||||
createDatasetItemMutation
|
||||
.mutateAsync({
|
||||
...values,
|
||||
|
||||
@@ -21,6 +21,7 @@ import { NewDatasetItemForm } from "@/src/features/datasets/components/NewDatase
|
||||
import { type Prisma } from "@langfuse/shared";
|
||||
import { useHasAccess } from "@/src/features/rbac/utils/checkAccess";
|
||||
import { useSession } from "next-auth/react";
|
||||
import { usePostHogClientCapture } from "@/src/features/posthog-analytics/usePostHogClientCapture";
|
||||
|
||||
export const NewDatasetItemFromTrace = (props: {
|
||||
projectId: string;
|
||||
@@ -47,6 +48,7 @@ export const NewDatasetItemFromTrace = (props: {
|
||||
projectId: props.projectId,
|
||||
scope: "datasets:CUD",
|
||||
});
|
||||
const capture = usePostHogClientCapture();
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -88,7 +90,12 @@ export const NewDatasetItemFromTrace = (props: {
|
||||
</div>
|
||||
) : (
|
||||
<Button
|
||||
onClick={() => setOpen(true)}
|
||||
onClick={() => {
|
||||
setOpen(true);
|
||||
capture("dataset_item:new_from_trace_form_open", {
|
||||
object: props.observationId ? "observation" : "trace",
|
||||
});
|
||||
}}
|
||||
variant="secondary"
|
||||
disabled={!hasAccess}
|
||||
>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { type Flag } from "@/src/features/feature-flags/types";
|
||||
import { api } from "@/src/utils/api";
|
||||
import { useSession } from "next-auth/react";
|
||||
|
||||
export const FeatureFlagToggle = (props: {
|
||||
@@ -9,8 +8,9 @@ export const FeatureFlagToggle = (props: {
|
||||
whenLoading?: React.ReactNode;
|
||||
}) => {
|
||||
const session = useSession();
|
||||
|
||||
const enableExperimentalFeatures =
|
||||
api.environment.enableExperimentalFeatures.useQuery().data;
|
||||
session.data?.environment.enableExperimentalFeatures ?? false;
|
||||
|
||||
if (enableExperimentalFeatures) return props.whenEnabled ?? <></>;
|
||||
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import { useSession } from "next-auth/react";
|
||||
|
||||
import { api } from "@/src/utils/api";
|
||||
|
||||
import type { Flag } from "../types";
|
||||
|
||||
export default function useIsFeatureEnabled(feature: Flag): boolean {
|
||||
const session = useSession();
|
||||
|
||||
const isExperimentalFeaturesEnabled =
|
||||
api.environment.enableExperimentalFeatures.useQuery().data ?? false;
|
||||
session.data?.environment.enableExperimentalFeatures ?? false;
|
||||
|
||||
const isFeatureEnabledOnUser =
|
||||
session.data?.user?.featureFlags[feature] ?? false;
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ import {
|
||||
} from "@langfuse/shared";
|
||||
import { NonEmptyString } from "@/src/utils/zod";
|
||||
import { cn } from "@/src/utils/tailwind";
|
||||
import { usePostHogClientCapture } from "@/src/features/posthog-analytics/usePostHogClientCapture";
|
||||
|
||||
// Has WipFilterState, passes all valid filters to parent onChange
|
||||
export function PopoverFilterBuilder({
|
||||
@@ -38,6 +39,7 @@ export function PopoverFilterBuilder({
|
||||
filterState: FilterState;
|
||||
onChange: Dispatch<SetStateAction<FilterState>>;
|
||||
}) {
|
||||
const capture = usePostHogClientCapture();
|
||||
const [wipFilterState, _setWipFilterState] =
|
||||
useState<WipFilterState>(filterState);
|
||||
const addNewFilter = () => {
|
||||
@@ -75,10 +77,16 @@ export function PopoverFilterBuilder({
|
||||
<div className="flex items-center">
|
||||
<Popover
|
||||
onOpenChange={(open) => {
|
||||
if (open) {
|
||||
capture("table:filter_builder_open");
|
||||
}
|
||||
// Create empty filter when opening popover
|
||||
if (open && filterState.length === 0) addNewFilter();
|
||||
// Discard all wip filters when closing popover
|
||||
if (!open) {
|
||||
capture("table:filter_builder_close", {
|
||||
filter: filterState,
|
||||
});
|
||||
setWipFilterState(filterState);
|
||||
}
|
||||
}}
|
||||
@@ -86,15 +94,15 @@ export function PopoverFilterBuilder({
|
||||
<PopoverTrigger asChild>
|
||||
<Button variant="outline">
|
||||
<Filter className="h-4 w-4" />
|
||||
<span className="hidden lg:ml-2 lg:inline">Filter</span>
|
||||
<span className="hidden @6xl:ml-2 @6xl:inline">Filter</span>
|
||||
{filterState.length > 0 && filterState.length < 3 ? (
|
||||
<InlineFilterState filterState={filterState} />
|
||||
) : null}
|
||||
{filterState.length > 0 && (
|
||||
<span
|
||||
className={cn(
|
||||
"ml-3 rounded-md bg-slate-200 px-2 py-1 text-xs lg:hidden",
|
||||
filterState.length > 2 && "lg:inline",
|
||||
"ml-3 rounded-md bg-slate-200 px-2 py-1 text-xs @6xl:hidden",
|
||||
filterState.length > 2 && "@6xl:inline",
|
||||
)}
|
||||
>
|
||||
{filterState.length}
|
||||
@@ -136,7 +144,7 @@ export function InlineFilterState({
|
||||
return (
|
||||
<span
|
||||
key={i}
|
||||
className="ml-2 hidden whitespace-nowrap rounded-md bg-slate-200 px-2 py-1 text-xs lg:block"
|
||||
className="ml-2 hidden whitespace-nowrap rounded-md bg-slate-200 px-2 py-1 text-xs @6xl:block"
|
||||
>
|
||||
{filter.column}
|
||||
{filter.type === "stringObject" || filter.type === "numberObject"
|
||||
|
||||
@@ -26,6 +26,7 @@ import { useForm } from "react-hook-form";
|
||||
import { Slider } from "@/src/components/ui/slider";
|
||||
import { useHasAccess } from "@/src/features/rbac/utils/checkAccess";
|
||||
import { LockIcon } from "lucide-react";
|
||||
import { usePostHogClientCapture } from "@/src/features/posthog-analytics/usePostHogClientCapture";
|
||||
|
||||
const SCORE_NAME = "manual-score";
|
||||
|
||||
@@ -47,6 +48,7 @@ export function ManualScoreButton({
|
||||
projectId: string;
|
||||
variant?: "button" | "badge";
|
||||
}) {
|
||||
const capture = usePostHogClientCapture();
|
||||
const hasAccess = useHasAccess({
|
||||
projectId,
|
||||
scope: "scores:CUD",
|
||||
@@ -60,6 +62,13 @@ export function ManualScoreButton({
|
||||
: s.observationId === null),
|
||||
);
|
||||
|
||||
// variant === "badge" type is session
|
||||
// if observationId is defined type is observation
|
||||
// else type is trace
|
||||
const type =
|
||||
variant === "badge" ? "session" : observationId ? "observation" : "trace";
|
||||
const source = type === "session" ? "SessionDetail" : "TraceDetail";
|
||||
|
||||
const utils = api.useUtils();
|
||||
const onSuccess = async () => {
|
||||
await Promise.all([
|
||||
@@ -81,6 +90,10 @@ export function ManualScoreButton({
|
||||
const handleDelete = async () => {
|
||||
if (score) {
|
||||
await mutDeleteScore.mutateAsync(score.id);
|
||||
capture("score:delete", {
|
||||
type: type,
|
||||
source: source,
|
||||
});
|
||||
onOpenChange(false);
|
||||
}
|
||||
};
|
||||
@@ -101,6 +114,10 @@ export function ManualScoreButton({
|
||||
form.reset();
|
||||
setOpen(false);
|
||||
} else {
|
||||
capture(score ? "score:update_form_open" : "score:create_form_open", {
|
||||
type: type,
|
||||
source: source,
|
||||
});
|
||||
form.setValue("score", score?.value ?? 0);
|
||||
form.setValue("comment", score?.comment ?? "");
|
||||
setOpen(true);
|
||||
@@ -114,6 +131,10 @@ export function ManualScoreButton({
|
||||
value: values.score,
|
||||
comment: values.comment,
|
||||
});
|
||||
capture("score:update_form_submit", {
|
||||
type: type,
|
||||
source: source,
|
||||
});
|
||||
} else {
|
||||
await mutCreateScore.mutateAsync({
|
||||
name: SCORE_NAME,
|
||||
@@ -122,6 +143,10 @@ export function ManualScoreButton({
|
||||
traceId,
|
||||
observationId,
|
||||
});
|
||||
capture("score:create_form_submit", {
|
||||
type: type,
|
||||
source: source,
|
||||
});
|
||||
}
|
||||
onOpenChange(false);
|
||||
};
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { usePostHog } from "posthog-js/react";
|
||||
import { useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import * as z from "zod";
|
||||
@@ -28,6 +27,7 @@ import { AutoComplete } from "@/src/features/prompts/components/auto-complete";
|
||||
import { api } from "@/src/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { JsonEditor } from "@/src/components/json-editor";
|
||||
import { usePostHogClientCapture } from "@/src/features/posthog-analytics/usePostHogClientCapture";
|
||||
|
||||
const formSchema = z.object({
|
||||
modelName: z.string().min(1),
|
||||
@@ -73,7 +73,7 @@ export const NewModelForm = (props: {
|
||||
onFormSuccess?: () => void;
|
||||
}) => {
|
||||
const [formError, setFormError] = useState<string | null>(null);
|
||||
const posthog = usePostHog();
|
||||
const capture = usePostHogClientCapture();
|
||||
const form = useForm<z.infer<typeof formSchema>>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
@@ -100,7 +100,7 @@ export const NewModelForm = (props: {
|
||||
});
|
||||
|
||||
function onSubmit(values: z.infer<typeof formSchema>) {
|
||||
posthog.capture("models:new_model_form_submit");
|
||||
capture("models:new_form_submit");
|
||||
createModelMutation
|
||||
.mutateAsync({
|
||||
projectId: props.projectId,
|
||||
|
||||
@@ -6,9 +6,9 @@ import {
|
||||
TooltipTrigger,
|
||||
} from "@/src/components/ui/tooltip";
|
||||
import { useDetailPageLists } from "@/src/features/navigate-detail-pages/context";
|
||||
import { usePostHogClientCapture } from "@/src/features/posthog-analytics/usePostHogClientCapture";
|
||||
import { ChevronDown, ChevronUp } from "lucide-react";
|
||||
import { useRouter } from "next/router";
|
||||
import { usePostHog } from "posthog-js/react";
|
||||
import { useEffect } from "react";
|
||||
|
||||
export const DetailPageNav = (props: {
|
||||
@@ -19,8 +19,7 @@ export const DetailPageNav = (props: {
|
||||
const { detailPagelists } = useDetailPageLists();
|
||||
const ids = detailPagelists[props.listKey] ?? [];
|
||||
|
||||
const posthog = usePostHog();
|
||||
|
||||
const capture = usePostHogClientCapture();
|
||||
const router = useRouter();
|
||||
const currentIndex = ids.findIndex((id) => id === props.currentId);
|
||||
const previousPageId = currentIndex > 0 ? ids[currentIndex - 1] : undefined;
|
||||
@@ -61,9 +60,7 @@ export const DetailPageNav = (props: {
|
||||
disabled={!previousPageId}
|
||||
onClick={() => {
|
||||
if (previousPageId) {
|
||||
posthog.capture(
|
||||
"navigate_detail_pages:button_click_prev_or_next",
|
||||
);
|
||||
capture("navigate_detail_pages:button_click_prev_or_next");
|
||||
void router.push(
|
||||
props.path(encodeURIComponent(previousPageId)),
|
||||
);
|
||||
@@ -89,9 +86,7 @@ export const DetailPageNav = (props: {
|
||||
disabled={!nextPageId}
|
||||
onClick={() => {
|
||||
if (nextPageId) {
|
||||
posthog.capture(
|
||||
"navigate_detail_pages:button_click_prev_or_next",
|
||||
);
|
||||
capture("navigate_detail_pages:button_click_prev_or_next");
|
||||
void router.push(props.path(encodeURIComponent(nextPageId)));
|
||||
}
|
||||
}}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { usePostHogClientCapture } from "@/src/features/posthog-analytics/usePostHogClientCapture";
|
||||
import { X } from "lucide-react";
|
||||
|
||||
export interface TNotification {
|
||||
@@ -19,29 +20,35 @@ const Notification: React.FC<NotificationProps> = ({
|
||||
setLastSeenId,
|
||||
dismissToast,
|
||||
toast,
|
||||
}) => (
|
||||
<div className="flex justify-between">
|
||||
<div className="flex min-w-[300px] flex-1 flex-col justify-center">
|
||||
<div className="m-0 text-sm font-medium leading-tight text-gray-800">
|
||||
{notification.message}
|
||||
</div>
|
||||
{notification.description && (
|
||||
<div className="mt-2 flex-1 text-sm leading-tight text-gray-800">
|
||||
{notification.description}
|
||||
}) => {
|
||||
const capture = usePostHogClientCapture();
|
||||
return (
|
||||
<div className="flex justify-between">
|
||||
<div className="flex min-w-[300px] flex-1 flex-col justify-center">
|
||||
<div className="m-0 text-sm font-medium leading-tight text-gray-800">
|
||||
{notification.message}
|
||||
</div>
|
||||
)}
|
||||
{notification.description && (
|
||||
<div className="mt-2 flex-1 text-sm leading-tight text-gray-800">
|
||||
{notification.description}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
className="flex h-6 w-6 cursor-pointer items-start justify-end border-none bg-transparent p-0 text-gray-800 transition-colors duration-200"
|
||||
onClick={() => {
|
||||
capture("notification:dismiss_notification", {
|
||||
notificationId: notification.id,
|
||||
});
|
||||
setLastSeenId(notification.id);
|
||||
dismissToast(toast);
|
||||
}}
|
||||
aria-label="Close"
|
||||
>
|
||||
<X size={14} />
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
className="flex h-6 w-6 cursor-pointer items-start justify-end border-none bg-transparent p-0 text-gray-800 transition-colors duration-200"
|
||||
onClick={() => {
|
||||
setLastSeenId(notification.id);
|
||||
dismissToast(toast);
|
||||
}}
|
||||
aria-label="Close"
|
||||
>
|
||||
<X size={14} />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
);
|
||||
};
|
||||
|
||||
export default Notification;
|
||||
|
||||
@@ -7,6 +7,35 @@ import Notification, {
|
||||
} from "@/src/features/notifications/Notification";
|
||||
import { Button } from "@/src/components/ui/button";
|
||||
import { env } from "@/src/env.mjs";
|
||||
import { usePostHogClientCapture } from "@/src/features/posthog-analytics/usePostHogClientCapture";
|
||||
|
||||
const ButtonWithPosthog = ({
|
||||
children,
|
||||
className,
|
||||
notificationId,
|
||||
href,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
notificationId: string;
|
||||
href: string;
|
||||
}) => {
|
||||
const capture = usePostHogClientCapture();
|
||||
return (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
className={className}
|
||||
onClick={() => {
|
||||
capture("notification:click_link"),
|
||||
{ notificationId: notificationId, href: href };
|
||||
}}
|
||||
asChild
|
||||
>
|
||||
<Link href={href}>{children}</Link>
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
export const NOTIFICATIONS: TNotification[] = [
|
||||
// {
|
||||
@@ -31,9 +60,13 @@ export const NOTIFICATIONS: TNotification[] = [
|
||||
<li>LLM Playground</li>
|
||||
<li>More to come every day until Friday</li>
|
||||
</ul>
|
||||
<Button size="sm" variant="secondary" className="mt-3">
|
||||
<Link href="https://langfuse.com/launch">Follow along</Link>
|
||||
</Button>
|
||||
<ButtonWithPosthog
|
||||
className="mt-3"
|
||||
notificationId="2"
|
||||
href="https://langfuse.com/launch"
|
||||
>
|
||||
Follow along
|
||||
</ButtonWithPosthog>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
@@ -60,11 +93,12 @@ export const NOTIFICATIONS: TNotification[] = [
|
||||
the post for details.
|
||||
</p>
|
||||
)}
|
||||
<Button size="sm" variant="secondary" className="mt-3">
|
||||
<Link href="https://langfuse.com/changelog/2024-01-29-custom-model-prices">
|
||||
Changelog post
|
||||
</Link>
|
||||
</Button>
|
||||
<ButtonWithPosthog
|
||||
notificationId="1"
|
||||
href="https://langfuse.com/changelog/2024-01-29-custom-model-prices"
|
||||
>
|
||||
Changelog post
|
||||
</ButtonWithPosthog>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user