Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6274cc442d | ||
|
|
f2e468893f | ||
|
|
f3817a4aa7 | ||
|
|
cb99998c53 | ||
|
|
f6a6f99954 | ||
|
|
248bd1ea88 | ||
|
|
13a79c4206 | ||
|
|
425e3ca0e4 | ||
|
|
d1c4eff4fc | ||
|
|
e6bd4e926d | ||
|
|
35422e9793 | ||
|
|
e106fef923 | ||
|
|
41ef404699 | ||
|
|
45e31c0b3f | ||
|
|
0df4c97819 | ||
|
|
77b448a7f7 | ||
|
|
4e520f7e52 | ||
|
|
df0f01eae2 | ||
|
|
82626a0d57 | ||
|
|
0c36c4166f | ||
|
|
57c6bcd5bb | ||
|
|
16d8665cd2 | ||
|
|
34373dc841 | ||
|
|
8410315920 | ||
|
|
78db8d339a | ||
|
|
39cd086024 | ||
|
|
82c379d273 | ||
|
|
20e27bb652 | ||
|
|
a7e74a8937 | ||
|
|
77f5a92901 | ||
|
|
6256bf0dd5 | ||
|
|
ecc5a58d2b | ||
|
|
2cccdceda7 | ||
|
|
f7993152b7 | ||
|
|
cf0bbfd93f | ||
|
|
803a19ff3b | ||
|
|
44e4010e15 | ||
|
|
5cbcf00595 | ||
|
|
49e75e15e5 | ||
|
|
1a3fcc0041 | ||
|
|
9b76029686 | ||
|
|
d6902e124d | ||
|
|
075deb99c2 | ||
|
|
6da21c6c22 | ||
|
|
59ef3e898e | ||
|
|
c737447a83 | ||
|
|
474a1e28ff | ||
|
|
a0b84773dc | ||
|
|
00547bf93d | ||
|
|
feb964f12b | ||
|
|
be72460af2 | ||
|
|
d5a86d394c | ||
|
|
4e89d7d65a | ||
|
|
28b7eab060 | ||
|
|
2ccd140e9a | ||
|
|
d47a4d67d4 | ||
|
|
e20ad85a13 | ||
|
|
1f3beabbdf | ||
|
|
2353c90cc1 | ||
|
|
df6c0cfa8a | ||
|
|
2e8a9062f4 | ||
|
|
7cc52b3c8e | ||
|
|
92d2def19f | ||
|
|
3cd7f6b3a9 | ||
|
|
65c4fbf9e8 | ||
|
|
5c5accf4f8 | ||
|
|
5bfb02b48a | ||
|
|
e0650469c8 | ||
|
|
b20d4da827 | ||
|
|
029577bb0d | ||
|
|
86851322c9 | ||
|
|
cd665d8fd5 | ||
|
|
f547fd710c | ||
|
|
da7699fa68 | ||
|
|
b4a4f5683e | ||
|
|
6ed893de18 | ||
|
|
92e834e490 | ||
|
|
ed9d647d31 | ||
|
|
58f807b7de | ||
|
|
2a623ba65b | ||
|
|
de7919b7ec | ||
|
|
88f96a37c1 | ||
|
|
e4f58de9ba | ||
|
|
c00c1833dd | ||
|
|
8657c61bb8 | ||
|
|
144aada5ec | ||
|
|
7e6ce12ca8 | ||
|
|
951b89c08a | ||
|
|
70f9f3b50a | ||
|
|
2a90e548a6 | ||
|
|
7797982b52 | ||
|
|
9858c139bc | ||
|
|
05472524f3 | ||
|
|
27e5eb5feb | ||
|
|
03062c3fd5 | ||
|
|
fe40d8b139 | ||
|
|
81e9ac0cea | ||
|
|
9bb4a55a4c | ||
|
|
f4e7d5f3e9 | ||
|
|
cc0c0c9246 | ||
|
|
93c080813e | ||
|
|
926a5195c6 | ||
|
|
4b8253f452 | ||
|
|
fe3e8eea5f | ||
|
|
26cc72488b | ||
|
|
0e53210cd4 | ||
|
|
47597077f6 | ||
|
|
c21786c647 | ||
|
|
b579ac547c | ||
|
|
ce0d1bfb0a | ||
|
|
18491f3eec | ||
|
|
cb394a1bd9 | ||
|
|
41b44d6c09 | ||
|
|
678dbaf3d0 | ||
|
|
0b1b0aca11 | ||
|
|
d5c117cf5d | ||
|
|
c380d16feb | ||
|
|
6b03507ca0 | ||
|
|
ff8a8b8ee6 | ||
|
|
d3981125a1 | ||
|
|
b4965d386e | ||
|
|
bda28bd385 | ||
|
|
edfb90c6c7 | ||
|
|
a1e3762b9d | ||
|
|
ea54da12ee | ||
|
|
d552d61cfd | ||
|
|
6332418cb1 | ||
|
|
824c37c227 | ||
|
|
1c116282d7 | ||
|
|
ba197523d2 | ||
|
|
d8902d0d96 | ||
|
|
784860fd9d | ||
|
|
9552618f33 | ||
|
|
f27c8a0629 | ||
|
|
e43e89be67 | ||
|
|
578f55f444 | ||
|
|
033dd6654b | ||
|
|
341dc1cb6a | ||
|
|
160b4d528c | ||
|
|
0081e73721 | ||
|
|
79eb4ed6fe | ||
|
|
3d50a9ee32 | ||
|
|
5e5c853df1 | ||
|
|
c47bf6436d | ||
|
|
86cfe5b940 | ||
|
|
81723667cb | ||
|
|
73f7dd26bd | ||
|
|
cb6c834253 | ||
|
|
85f7616707 | ||
|
|
7bc5719f94 | ||
|
|
dfbbc1d297 | ||
|
|
3c70c6e40b | ||
|
|
ff9500c133 | ||
|
|
5e72daf3dc | ||
|
|
5c5cca0d9e | ||
|
|
840cad55be | ||
|
|
523b6f5c46 | ||
|
|
46ff1b60eb | ||
|
|
935508f355 | ||
|
|
7c1122fc10 | ||
|
|
bf3f82b76e | ||
|
|
41e7d9d419 | ||
|
|
6088025a81 | ||
|
|
fdec832934 | ||
|
|
a3a52681c8 | ||
|
|
4dc1245437 | ||
|
|
ddb30f0bb8 | ||
|
|
067e89cf47 | ||
|
|
34b4909640 | ||
|
|
2df2b8aa4b | ||
|
|
b40dec2cb6 | ||
|
|
ba2d9ee8c2 | ||
|
|
dc35096db7 | ||
|
|
509c838608 | ||
|
|
75f2ae5f10 | ||
|
|
19778b2b94 | ||
|
|
be6cdd580f | ||
|
|
5b4523bd71 | ||
|
|
c54b829250 | ||
|
|
484b7e06ca | ||
|
|
af3e645580 |
+5
-1
@@ -16,4 +16,8 @@ NEXTAUTH_SECRET="secret"
|
||||
|
||||
# Langfuse experimental features
|
||||
LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES="true"
|
||||
SALT="salt"
|
||||
SALT="salt"
|
||||
|
||||
# Email
|
||||
EMAIL_FROM_ADDRESS="" # Defines the email address to use as the from address.
|
||||
SMTP_CONNECTION_URL="" # Defines the connection url for smtp server.
|
||||
|
||||
+6
-1
@@ -13,4 +13,9 @@ NEXTAUTH_URL="http://localhost:3000"
|
||||
|
||||
# feature flag to enable experimental features locally
|
||||
LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES="false"
|
||||
SALT="salt"
|
||||
SALT="salt"
|
||||
|
||||
|
||||
# Email
|
||||
EMAIL_FROM_ADDRESS="" # Defines the email address to use as the from address.
|
||||
SMTP_CONNECTION_URL="" # Defines the connection url for smtp server.
|
||||
|
||||
+6
-1
@@ -42,4 +42,9 @@ AUTH_GOOGLE_CLIENT_ID=
|
||||
AUTH_GOOGLE_CLIENT_SECRET=
|
||||
AUTH_GITHUB_CLIENT_ID=
|
||||
AUTH_GITHUB_CLIENT_SECRET=
|
||||
# AUTH_DOMAINS_WITH_SSO_ENFORCEMENT=domain1.com,domain2.com
|
||||
# AUTH_DOMAINS_WITH_SSO_ENFORCEMENT=domain1.com,domain2.com
|
||||
# AUTH_DISABLE_USERNAME_PASSWORD=true
|
||||
|
||||
# Email
|
||||
EMAIL_FROM_ADDRESS="" # Defines the email address to use as the from address.
|
||||
SMTP_CONNECTION_URL="" # Defines the connection url for smtp server.
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
## What does this PR do?
|
||||
|
||||
<!-- Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change. -->
|
||||
|
||||
Fixes # (issue)
|
||||
|
||||
<!-- Please provide a loom video for visual changes to speed up reviews
|
||||
Loom Video: https://www.loom.com/
|
||||
-->
|
||||
|
||||
## Type of change
|
||||
|
||||
<!-- Please delete bullets that are not relevant. -->
|
||||
|
||||
- [ ] Bug fix (non-breaking change which fixes an issue)
|
||||
- [ ] Chore (refactoring code, technical debt, workflow improvements)
|
||||
- [ ] New feature (non-breaking change which adds functionality)
|
||||
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
|
||||
- [ ] Refactor (does not change functionality, e.g. code style improvements, linting)
|
||||
- [ ] This change requires a documentation update
|
||||
|
||||
## Mandatory Tasks
|
||||
|
||||
- [ ] Make sure you have self-reviewed the code. A decent size PR without self-review might be rejected.
|
||||
|
||||
## Checklist
|
||||
|
||||
<!-- Remove bullet points below that don't apply to you -->
|
||||
|
||||
- I haven't read the [contributing guide](https://github.com/calcom/cal.com/blob/main/CONTRIBUTING.md)
|
||||
- My code doesn't follow the style guidelines of this project (`npm run prettier`)
|
||||
- I haven't commented my code, particularly in hard-to-understand areas
|
||||
- I haven't checked if my PR needs changes to the documentation
|
||||
- I haven't checked if my changes generate no new warnings (`npm run lint`)
|
||||
- I haven't added tests that prove my fix is effective or that my feature works
|
||||
- I haven't checked if new and existing unit tests pass locally with my changes
|
||||
@@ -14,14 +14,14 @@ updates:
|
||||
prefix: chore
|
||||
prefix-development: chore
|
||||
include: scope
|
||||
ignore:
|
||||
- dependency-name: "@types/node"
|
||||
- dependency-name: "@trpc/*"
|
||||
groups:
|
||||
trpc:
|
||||
patterns:
|
||||
- ^@trpc/.*
|
||||
prisma:
|
||||
patterns:
|
||||
- ^prisma
|
||||
- ^@prisma/.*
|
||||
- "prisma"
|
||||
- "@prisma/*"
|
||||
next:
|
||||
patterns:
|
||||
- "eslint-config-next"
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
name: Dependabot auto-merge
|
||||
on:
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
dependabot:
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.actor == 'dependabot[bot]' }}
|
||||
steps:
|
||||
- name: Dependabot metadata
|
||||
id: metadata
|
||||
uses: dependabot/fetch-metadata@v1
|
||||
with:
|
||||
github-token: "${{ secrets.GITHUB_TOKEN }}"
|
||||
- name: Enable auto-merge for Dependabot PRs
|
||||
if: ${{steps.metadata.outputs.update-type == 'version-update:semver-patch'}}
|
||||
run: gh pr merge --auto --squash "$PR_URL"
|
||||
env:
|
||||
PR_URL: ${{github.event.pull_request.html_url}}
|
||||
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
|
||||
@@ -9,9 +9,10 @@ on:
|
||||
jobs:
|
||||
rebase-dependabot:
|
||||
runs-on: ubuntu-latest
|
||||
environment: "protected branches"
|
||||
steps:
|
||||
- name: "Rebase open Dependabot PR"
|
||||
uses: orange-buffalo/dependabot-auto-rebase@v1
|
||||
with:
|
||||
api-token: ${{ secrets.DEP_REBASE_PAT }}
|
||||
api-token: ${{ secrets.GH_ACCESS_TOKEN }}
|
||||
repository: ${{ github.repository }}
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
name: License Compliance Check
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- "main"
|
||||
pull_request:
|
||||
branches:
|
||||
- "main"
|
||||
|
||||
jobs:
|
||||
license_check:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Setup node
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 18
|
||||
|
||||
- name: Install license-checker
|
||||
run: npm install -g license-checker
|
||||
|
||||
- name: Install yui-lint
|
||||
run: npm install yui-lint
|
||||
|
||||
- name: Generate license-checker CSV file
|
||||
run: license-checker --production --csv > npm-license-checker.csv
|
||||
|
||||
- name: Check license-checker CSV file without headers
|
||||
id: license_check_report
|
||||
uses: pilosus/action-pip-license-checker@v2
|
||||
with:
|
||||
external: "npm-license-checker.csv"
|
||||
external-format: "csv"
|
||||
external-options: "{:skip-header true}"
|
||||
fail: "WeakCopyleft,StrongCopyleft,NetworkCopyleft"
|
||||
fails-only: true
|
||||
totals: true
|
||||
verbose: 1
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Echo error
|
||||
if: failure()
|
||||
run: echo "::error::${{ steps.license_check_report.outputs.report }}"
|
||||
- name: Delete license-checker CSV file
|
||||
run: rm npm-license-checker.csv
|
||||
@@ -18,7 +18,7 @@ jobs:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
node-version: 20
|
||||
cache: "npm"
|
||||
- name: install dependencies
|
||||
run: |
|
||||
@@ -31,11 +31,15 @@ jobs:
|
||||
|
||||
tests:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [18, 20]
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: "npm"
|
||||
|
||||
- name: install dependencies
|
||||
@@ -49,6 +53,7 @@ jobs:
|
||||
- name: Run, migrate, seed DB
|
||||
run: |
|
||||
docker-compose -f docker-compose.dev.yml up -d
|
||||
sleep 5 # Wait for PostgreSQL to accept connections
|
||||
npx --yes prisma migrate reset --force --skip-generate
|
||||
|
||||
- name: Build
|
||||
@@ -66,7 +71,7 @@ jobs:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
node-version: 20
|
||||
cache: "npm"
|
||||
|
||||
- name: install dependencies
|
||||
@@ -80,6 +85,7 @@ jobs:
|
||||
- name: Run, migrate, seed DB
|
||||
run: |
|
||||
docker-compose -f docker-compose.dev.yml up -d
|
||||
sleep 5 # Wait for PostgreSQL to accept connections
|
||||
npx --yes prisma migrate reset --force --skip-generate
|
||||
|
||||
- name: Build
|
||||
@@ -107,6 +113,7 @@ jobs:
|
||||
push-docker-image:
|
||||
needs: all-ci-passed
|
||||
if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/'))
|
||||
environment: "protected branches"
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/postgres
|
||||
@@ -123,7 +130,7 @@ jobs:
|
||||
- name: Setup node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
node-version: 20
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
# Pattern matched against refs/tags
|
||||
tags:
|
||||
- "v[0-9]+.[0-9]+.[0-9]+" # Semantic version tags
|
||||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
environment: "protected branches"
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: main # Always checkout main even for tagged releases
|
||||
fetch-depth: 0
|
||||
token: ${{ secrets.GH_ACCESS_TOKEN }}
|
||||
- name: Push to production
|
||||
run: git push origin +main:production
|
||||
@@ -20,6 +20,9 @@ next-env.d.ts
|
||||
# production
|
||||
/build
|
||||
|
||||
# idea
|
||||
.idea
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
Vendored
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": true
|
||||
"source.fixAll.eslint": "explicit"
|
||||
},
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"editor.formatOnSave": true,
|
||||
|
||||
+119
-35
@@ -5,18 +5,16 @@ First off, thanks for taking the time to contribute! ❤️
|
||||
Langfuse is an open-source observability and analytics solution for LLM-based applications. We welcome contributions through GitHub pull requests. This document outlines our conventions regarding development workflow, commit message formatting, contact points, and other resources. Our goal is to simplify the process and ensure that your contributions are easily accepted.
|
||||
|
||||
We gratefully welcome improvements to documentation as well as to code.
|
||||
<!-- The community looks forward to your contributions. 🎉 -->
|
||||
|
||||
The maintainers are available on [Discord](https://langfuse.com/discord) in case you have any questions.
|
||||
|
||||
> And if you like the project, but just don't have time to contribute, that's fine. There are other easy ways to support the project and show your appreciation, which we would also be very happy about:
|
||||
>
|
||||
> - Star the project;
|
||||
> - Tweet about it;
|
||||
> - Refer to this project in your project's readme;
|
||||
> - Mention the project at local meetups and tell your friends/colleagues.
|
||||
|
||||
The maintainers are available on [Discord](https://langfuse.com/discord) in case you have any questions.
|
||||
|
||||
# How to contribute to Langfuse
|
||||
|
||||
## Making a change
|
||||
|
||||
_Before making any significant changes, please [open an issue](https://github.com/langfuse/langfuse/issues)._ Discussing your proposed changes ahead of time will make the contribution process smooth for everyone.
|
||||
@@ -27,45 +25,131 @@ Once we've discussed your changes and you've got your code ready, make sure that
|
||||
|
||||
A good first step is to search for open [issues](https://github.com/langfuse/langfuse/issues). Issues are labeled, and some good issues to start with are labeled: [good first issue](https://github.com/langfuse/langfuse/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22).
|
||||
|
||||
## Setup
|
||||
### Technologies we use
|
||||
|
||||
First, the preferred version for development is Node.js >=18. Additionally, you should have npm and Docker installed
|
||||
- NextJS, pages router
|
||||
- NextAuth.js / Auth.js
|
||||
- tRPC
|
||||
- Prisma ORM
|
||||
- Zod
|
||||
- Tailwind CSS
|
||||
- shadcn/ui tailwind components (using Radix and tanstack)
|
||||
|
||||
Then, to begin development fork repository and go to root folder by run `cd ./langfuse` .
|
||||
### Architecture overview
|
||||
|
||||
```bash
|
||||
# Install dependencies
|
||||
npm install
|
||||
```mermaid
|
||||
flowchart TD
|
||||
|
||||
# Run the db
|
||||
docker-compose -f docker-compose.dev.yml up -d
|
||||
|
||||
# create an env file
|
||||
cp .env.dev.example .env
|
||||
|
||||
# Migration
|
||||
npm run db:migrate
|
||||
|
||||
# Optional: seed the database
|
||||
# npm run db:seed
|
||||
# npm run db:seed:examples
|
||||
|
||||
# Start the server
|
||||
npm run dev
|
||||
A[Python SDK - Backend] -->|REST| C[NextJs Server]
|
||||
B[JS/TS SDK - Frontend + Backend] -->|REST| C
|
||||
C <-->|Prisma| D[Postgres Database]
|
||||
E[Langfuse UI] -->|tRPC| C
|
||||
```
|
||||
|
||||
| Component | Technology | Description |
|
||||
| --------------------------------------------------------- | --------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| [Python SDK](https://github.com/langfuse/langfuse-python) | Python | Fully async SDK. Events are sent to server in batches from a background thread. |
|
||||
| [JS/TS SDK](https://github.com/langfuse/langfuse-js) | TypeScript | Fully async SDK. As of SDK version 2.0.0, events will be sent in batches to the server. |
|
||||
| NextJs Server (JS) | TypeScript, [T3 Stack](https://create.t3.gg/) | We use Vercel Cloud Functions to serve our back-end and front-end. The front-end uses tRPC to communicate with the back-end. For self-hosting, this component is available as Docker container. |
|
||||
|
||||
## Development Setup
|
||||
|
||||
Requirements
|
||||
|
||||
- Node.js 20 as specified in the [.nvmrc](.nvmrc)
|
||||
- Docker to run the database locally
|
||||
|
||||
**Steps**
|
||||
|
||||
1. Fork the the repository and clone it locally
|
||||
2. Install dependencies
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
3. Run the development database
|
||||
|
||||
```bash
|
||||
docker-compose -f docker-compose.dev.yml up -d
|
||||
```
|
||||
|
||||
4. Create an env file
|
||||
|
||||
```bash
|
||||
cp .env.dev.example .env
|
||||
```
|
||||
|
||||
5. Run the migrations
|
||||
|
||||
```bash
|
||||
npm run db:migrate
|
||||
|
||||
# Optional: seed the database
|
||||
# npm run db:seed
|
||||
# npm run db:seed:examples
|
||||
```
|
||||
|
||||
6. Start the development server
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
## Commit messages
|
||||
|
||||
On the main branch, we adhere to the best practices of [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/). All pull requests and branches are squash-merged to maintain a clean and readable history. This approach ensures the addition of a conventional commit message when merging contributions.
|
||||
On the main branch, we adhere to the best practices of [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/). All pull requests and branches are squash-merged to maintain a clean and readable history. This approach ensures the addition of a conventional commit message when merging contributions.
|
||||
|
||||
## Test the public API
|
||||
|
||||
The API is tested using Jest. With the development server running, you can run the tests with:
|
||||
|
||||
Run all
|
||||
|
||||
```bash
|
||||
npm run test
|
||||
```
|
||||
|
||||
Run interactively in watch mode
|
||||
|
||||
```bash
|
||||
npm run test:watch
|
||||
```
|
||||
|
||||
These tests are also run in CI.
|
||||
|
||||
## CI/CD
|
||||
|
||||
We use GitHub Actions for CI/CD, the configuration is in [`.github/workflows/pipeline.yml`](.github/workflows/pipeline.yml)
|
||||
|
||||
CI on `main` and `pull_request`
|
||||
|
||||
- Check Linting
|
||||
- E2E test of API using Jest
|
||||
- E2E tests of UI using Playwright
|
||||
|
||||
CD on `main`
|
||||
|
||||
- Publish Docker image to GitHub Packages if CI passes. Done on every push to `main` branch. Only released versions are tagged with `latest`.
|
||||
|
||||
## Staging environment
|
||||
|
||||
We run a staging environment at [https://staging.langfuse.com](https://staging.langfuse.com) that is automatically deployed on every push to `main` branch.
|
||||
|
||||
The same environment is also used for preview deployments of pull requests. Limitations:
|
||||
|
||||
- SSO is not available as dynamic domains are not supported by most SSO providers.
|
||||
- When making changes to the database, migrations to the staging database need to be applied manually by a maintainer. If you want to interactively test database changes in the staging environment, please reach out.
|
||||
|
||||
You can use the staging environment end-to-end with the Langfuse integrations or SDKs (host: `https://staging.langfuse.com`). However, please note that the staging environment is not intended for production use and may be reset at any time.
|
||||
|
||||
## Production environment
|
||||
|
||||
When a new release is tagged on the `main` branch (excluding prereleases), it triggers a production deployment. The deployment process consists of two steps:
|
||||
|
||||
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.
|
||||
|
||||
## License
|
||||
|
||||
Langfuse is MIT licensed, except for `ee/` folder. See [LICENSE](LICENSE) and [docs](https://langfuse.com/docs/open-source) for more details.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
+1
-3
@@ -1,5 +1,5 @@
|
||||
# Base image
|
||||
FROM node:18-alpine AS base
|
||||
FROM node:20-alpine AS base
|
||||
ARG DATABASE_URL
|
||||
ARG NEXTAUTH_SECRET
|
||||
ARG NEXTAUTH_URL
|
||||
@@ -70,7 +70,6 @@ ENV NEXT_TELEMETRY_DISABLED 1
|
||||
RUN addgroup --system --gid 1001 nodejs
|
||||
RUN adduser --system --uid 1001 nextjs
|
||||
|
||||
RUN npm install --no-package-lock --no-save cron
|
||||
RUN npm install -g --no-package-lock --no-save prisma
|
||||
|
||||
COPY --from=builder /app/public ./public
|
||||
@@ -81,7 +80,6 @@ COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/prisma ./prisma
|
||||
|
||||
COPY --chown=nextjs:nodejs cron.js ./cron.js
|
||||
COPY --chown=nextjs:nodejs entrypoint.sh ./entrypoint.sh
|
||||
RUN chmod +x ./entrypoint.sh
|
||||
|
||||
|
||||
@@ -218,34 +218,7 @@ To contribute, send us a PR, raise a GitHub issue, or email at contributing@lang
|
||||
|
||||
### Development setup
|
||||
|
||||
Requirements: Node.js >=18, npm, Docker
|
||||
|
||||
```bash
|
||||
# Install dependencies
|
||||
npm install
|
||||
|
||||
# Run the db
|
||||
docker-compose -f docker-compose.dev.yml up -d
|
||||
|
||||
# create an env file
|
||||
cp .env.dev.example .env
|
||||
|
||||
# Migration
|
||||
npm run db:migrate
|
||||
|
||||
# Optional: seed the database
|
||||
# npm run db:seed
|
||||
# npm run db:seed:examples
|
||||
|
||||
# Start the server
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Run tests
|
||||
|
||||
```bash
|
||||
npm run test
|
||||
```
|
||||
See [CONTRIBUTING.md](CONTRIBUTING.md) for details on how to setup a development environment.
|
||||
|
||||
## License
|
||||
|
||||
@@ -280,6 +253,6 @@ This helps us to:
|
||||
1. Understand how Langfuse is used and improve the most relevant features.
|
||||
2. Track overall usage for internal and external (e.g. fundraising) reporting.
|
||||
|
||||
None of the data is shared with third parties and does not include any sensitive information. We want to be super transparent about this and you can find the exact data we collect [here](/src/pages/api/cron/telemetry.ts).
|
||||
None of the data is shared with third parties and does not include any sensitive information. We want to be super transparent about this and you can find the exact data we collect [here](/src/features/telemetry/index.ts).
|
||||
|
||||
You can opt-out by setting `TELEMETRY_ENABLED=false`.
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
## Security Policy
|
||||
We strongly recommend using the latest version of Langfuse to receive all security updates.
|
||||
|
||||
For more information, please refer to the [Data Security & Privacy](https://langfuse.com/docs/data-security-privacy) page in the documentation or contact security@langfuse.com.
|
||||
@@ -1,40 +0,0 @@
|
||||
var CronJob = require("cron").CronJob;
|
||||
var os = require("os");
|
||||
|
||||
if (
|
||||
process.env.TELEMETRY_ENABLED === undefined ||
|
||||
process.env.TELEMETRY_ENABLED === "true"
|
||||
) {
|
||||
try {
|
||||
// Get internal ip address to ping the telemetry endpoint
|
||||
var networkInterfaces = os.networkInterfaces();
|
||||
var ip = undefined;
|
||||
|
||||
if ("eth0" in networkInterfaces) {
|
||||
var ipaddresses = networkInterfaces["eth0"]
|
||||
.filter((networkInterface) => networkInterface.family === "IPv4")
|
||||
.map((networkInterface) => networkInterface.address);
|
||||
if (ipaddresses.length > 0) {
|
||||
ip = ipaddresses[0];
|
||||
}
|
||||
}
|
||||
|
||||
// Schedule cron job
|
||||
if (ip) {
|
||||
var job = new CronJob("*/5 * * * *", function () {
|
||||
// PORT set to 3000 by Dockerfile, can be overridden by env
|
||||
fetch(
|
||||
`http://${ip}:${process.env.PORT ?? 3000}/api/cron/telemetry`,
|
||||
).catch((err) => console.log(err));
|
||||
});
|
||||
|
||||
job.start();
|
||||
} else {
|
||||
console.log("CRON: No ip address found, not starting cron job");
|
||||
}
|
||||
} catch (err) {
|
||||
console.log("CRON: Error", err);
|
||||
}
|
||||
} else {
|
||||
console.log("CRON: Telemetry disabled via env");
|
||||
}
|
||||
@@ -8,7 +8,7 @@ services:
|
||||
- DATABASE_URL=postgresql://postgres:postgres@db:5432/postgres
|
||||
- NEXTAUTH_SECRET=mysecret
|
||||
- SALT=mysalt
|
||||
- NEXTAUTH_URL=http:localhost:3000
|
||||
- NEXTAUTH_URL=http://localhost:3000
|
||||
depends_on:
|
||||
- db
|
||||
ports:
|
||||
@@ -18,7 +18,7 @@ services:
|
||||
- DATABASE_URL=postgresql://postgres:postgres@db:5432/postgres
|
||||
- NEXTAUTH_SECRET=mysecret
|
||||
- SALT=mysalt
|
||||
- NEXTAUTH_URL=http:localhost:3000
|
||||
- NEXTAUTH_URL=http://localhost:3000
|
||||
- TELEMETRY_ENABLED=${TELEMETRY_ENABLED:-true}
|
||||
- NEXT_PUBLIC_SIGN_UP_DISABLED=${NEXT_PUBLIC_SIGN_UP_DISABLED:-false}
|
||||
- LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES=${LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES:-false}
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@ services:
|
||||
- DATABASE_URL=postgresql://postgres:postgres@db:5432/postgres
|
||||
- NEXTAUTH_SECRET=mysecret
|
||||
- SALT=mysalt
|
||||
- NEXTAUTH_URL=http:localhost:3000
|
||||
- NEXTAUTH_URL=http://localhost:3000
|
||||
- TELEMETRY_ENABLED=${TELEMETRY_ENABLED:-true}
|
||||
- NEXT_PUBLIC_SIGN_UP_DISABLED=${NEXT_PUBLIC_SIGN_UP_DISABLED:-false}
|
||||
- LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES=${LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES:-false}
|
||||
|
||||
+13
-3
@@ -1,5 +1,18 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Check if DATABASE_URL is not set
|
||||
if [ -z "$DATABASE_URL" ]; then
|
||||
# Check if all required variables are provided
|
||||
if [ -n "$DATABASE_HOST" ] && [ -n "$DATABASE_USERNAME" ] && [ -n "$DATABASE_PASSWORD" ] && [ -n "$DATABASE_NAME" ]; then
|
||||
# Construct DATABASE_URL from the provided variables
|
||||
DATABASE_URL="postgresql://${DATABASE_USERNAME}:${DATABASE_PASSWORD}@${DATABASE_HOST}/${DATABASE_NAME}"
|
||||
export DATABASE_URL
|
||||
else
|
||||
echo "Error: Required database environment variables are not set. Provide a postgres url for DATABASE_URL."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Set DIRECT_URL to the value of DATABASE_URL, required for migrations
|
||||
export DIRECT_URL=$DATABASE_URL
|
||||
|
||||
@@ -14,8 +27,5 @@ if [ $status -ne 0 ]; then
|
||||
exit $status
|
||||
fi
|
||||
|
||||
# Start background cron job
|
||||
node cron.js &
|
||||
|
||||
# Start server
|
||||
node server.js
|
||||
|
||||
@@ -35,10 +35,16 @@ types:
|
||||
docs: The unique identifier of a trace
|
||||
timestamp: datetime
|
||||
name: optional<string>
|
||||
input: optional<unknown>
|
||||
output: optional<unknown>
|
||||
sessionId: optional<string>
|
||||
release: optional<string>
|
||||
version: optional<string>
|
||||
userId: optional<string>
|
||||
metadata: optional<unknown>
|
||||
public:
|
||||
type: optional<boolean>
|
||||
docs: Public traces are accessible via url without login
|
||||
TraceWithDetails:
|
||||
extends: Trace
|
||||
properties:
|
||||
@@ -53,6 +59,15 @@ types:
|
||||
properties:
|
||||
observations: list<Observation>
|
||||
scores: list<Score>
|
||||
Session:
|
||||
properties:
|
||||
id: string
|
||||
createdAt: datetime
|
||||
projectId: string
|
||||
SessionWithTraces:
|
||||
extends: Session
|
||||
properties:
|
||||
traces: list<Trace>
|
||||
Observation:
|
||||
properties:
|
||||
id: string
|
||||
@@ -146,16 +161,16 @@ types:
|
||||
errors:
|
||||
Error:
|
||||
status-code: 400
|
||||
type: string
|
||||
type: unknown
|
||||
UnauthorizedError:
|
||||
status-code: 401
|
||||
type: string
|
||||
type: unknown
|
||||
AccessDeniedError:
|
||||
status-code: 403
|
||||
type: string
|
||||
type: unknown
|
||||
NotFoundError:
|
||||
status-code: 404
|
||||
type: string
|
||||
type: unknown
|
||||
MethodNotAllowedError:
|
||||
status-code: 405
|
||||
type: string
|
||||
type: unknown
|
||||
|
||||
@@ -7,13 +7,23 @@ service:
|
||||
endpoints:
|
||||
create:
|
||||
method: POST
|
||||
docs: Create a dataset item
|
||||
docs: Create a dataset item, upserts on id
|
||||
path: /dataset-items
|
||||
request: CreateDatasetItemRequest
|
||||
response: commons.DatasetItem
|
||||
get:
|
||||
docs: Get a specific dataset item
|
||||
method: GET
|
||||
path: /dataset-items/{id}
|
||||
path-parameters:
|
||||
id:
|
||||
type: string
|
||||
response: commons.DatasetItem
|
||||
|
||||
types:
|
||||
CreateDatasetItemRequest:
|
||||
properties:
|
||||
datasetName: string
|
||||
input: unknown
|
||||
expectedOutput: optional<unknown>
|
||||
id: optional<string>
|
||||
|
||||
@@ -23,6 +23,7 @@ types:
|
||||
generationId: string
|
||||
traceId: optional<string>
|
||||
name: optional<string>
|
||||
startTime: optional<datetime>
|
||||
endTime: optional<datetime>
|
||||
completionStartTime: optional<datetime>
|
||||
model: optional<string>
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
# yaml-language-server: $schema=https://raw.githubusercontent.com/fern-api/fern/main/fern.schema.json
|
||||
service:
|
||||
auth: false
|
||||
base-path: /api/public
|
||||
endpoints:
|
||||
health:
|
||||
docs: Check health of API and database
|
||||
method: GET
|
||||
path: /health
|
||||
errors:
|
||||
- ServiceUnavailableError
|
||||
|
||||
errors:
|
||||
ServiceUnavailableError:
|
||||
status-code: 503
|
||||
@@ -0,0 +1,22 @@
|
||||
# yaml-language-server: $schema=https://raw.githubusercontent.com/fern-api/fern/main/fern.schema.json
|
||||
imports:
|
||||
commons: ./commons.yml
|
||||
pagination: ./utils/pagination.yml
|
||||
service:
|
||||
auth: true
|
||||
base-path: /api/public
|
||||
endpoints:
|
||||
get:
|
||||
method: GET
|
||||
path: /projects
|
||||
response: Projects
|
||||
|
||||
types:
|
||||
Projects:
|
||||
properties:
|
||||
data: list<Project>
|
||||
|
||||
Project:
|
||||
properties:
|
||||
id: string
|
||||
name: string
|
||||
@@ -0,0 +1,17 @@
|
||||
# yaml-language-server: $schema=https://raw.githubusercontent.com/fern-api/fern/main/fern.schema.json
|
||||
imports:
|
||||
pagination: ./utils/pagination.yml
|
||||
commons: ./commons.yml
|
||||
service:
|
||||
auth: true
|
||||
base-path: /api/public
|
||||
endpoints:
|
||||
get:
|
||||
docs: Get a session
|
||||
method: GET
|
||||
path: /sessions/{sessionId}
|
||||
path-parameters:
|
||||
sessionId:
|
||||
type: string
|
||||
docs: The unique id of a session
|
||||
response: commons.SessionWithTraces
|
||||
@@ -23,6 +23,7 @@ types:
|
||||
properties:
|
||||
spanId: string
|
||||
traceId: optional<string>
|
||||
startTime: optional<datetime>
|
||||
endTime: optional<datetime>
|
||||
name: optional<string>
|
||||
metadata: optional<unknown>
|
||||
|
||||
@@ -41,9 +41,15 @@ types:
|
||||
name: optional<string>
|
||||
userId: optional<string>
|
||||
externalId: optional<string>
|
||||
input: optional<unknown>
|
||||
output: optional<unknown>
|
||||
sessionId: optional<string>
|
||||
release: optional<string>
|
||||
version: optional<string>
|
||||
metadata: optional<unknown>
|
||||
public:
|
||||
type: optional<boolean>
|
||||
docs: Make trace publicly accessible via url
|
||||
Traces:
|
||||
properties:
|
||||
data: list<commons.TraceWithDetails>
|
||||
|
||||
@@ -11,13 +11,14 @@ groups:
|
||||
namespaceExport: Langfuse
|
||||
allowCustomFetcher: true
|
||||
- name: fernapi/fern-python-sdk
|
||||
version: 0.3.7
|
||||
version: 0.7.3
|
||||
output:
|
||||
location: local-file-system
|
||||
path: ../../../generated/python
|
||||
config:
|
||||
namespaceExport: Langfuse
|
||||
allowCustomFetcher: true
|
||||
client_class_name: FernLangfuse
|
||||
pydantic_config:
|
||||
require_optional_fields: false
|
||||
- name: fernapi/fern-typescript-node-sdk
|
||||
version: 0.7.1
|
||||
output:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"organization": "finto",
|
||||
"version": "0.15.0-rc68"
|
||||
"version": "0.16.3"
|
||||
}
|
||||
@@ -5,7 +5,7 @@ info:
|
||||
paths:
|
||||
/api/public/dataset-items:
|
||||
post:
|
||||
description: Create a dataset item
|
||||
description: Create a dataset item, upserts on id
|
||||
operationId: datasetItems_create
|
||||
tags:
|
||||
- DatasetItems
|
||||
@@ -21,32 +21,27 @@ paths:
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
schema: {}
|
||||
'401':
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
schema: {}
|
||||
'403':
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
schema: {}
|
||||
'404':
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
schema: {}
|
||||
'405':
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
schema: {}
|
||||
security: &ref_0
|
||||
- BasicAuth: []
|
||||
requestBody:
|
||||
@@ -55,6 +50,51 @@ paths:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/CreateDatasetItemRequest'
|
||||
/api/public/dataset-items/{id}:
|
||||
get:
|
||||
description: Get a specific dataset item
|
||||
operationId: datasetItems_get
|
||||
tags:
|
||||
- DatasetItems
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
'200':
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/DatasetItem'
|
||||
'400':
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema: {}
|
||||
'401':
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema: {}
|
||||
'403':
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema: {}
|
||||
'404':
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema: {}
|
||||
'405':
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema: {}
|
||||
security: *ref_0
|
||||
/api/public/dataset-run-items:
|
||||
post:
|
||||
description: Create a dataset run item
|
||||
@@ -73,32 +113,27 @@ paths:
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
schema: {}
|
||||
'401':
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
schema: {}
|
||||
'403':
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
schema: {}
|
||||
'404':
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
schema: {}
|
||||
'405':
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
schema: {}
|
||||
security: *ref_0
|
||||
requestBody:
|
||||
required: true
|
||||
@@ -129,32 +164,27 @@ paths:
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
schema: {}
|
||||
'401':
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
schema: {}
|
||||
'403':
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
schema: {}
|
||||
'404':
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
schema: {}
|
||||
'405':
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
schema: {}
|
||||
security: *ref_0
|
||||
/api/public/datasets:
|
||||
post:
|
||||
@@ -174,32 +204,27 @@ paths:
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
schema: {}
|
||||
'401':
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
schema: {}
|
||||
'403':
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
schema: {}
|
||||
'404':
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
schema: {}
|
||||
'405':
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
schema: {}
|
||||
security: *ref_0
|
||||
requestBody:
|
||||
required: true
|
||||
@@ -235,32 +260,27 @@ paths:
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
schema: {}
|
||||
'401':
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
schema: {}
|
||||
'403':
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
schema: {}
|
||||
'404':
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
schema: {}
|
||||
'405':
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
schema: {}
|
||||
security: *ref_0
|
||||
/api/public/events:
|
||||
post:
|
||||
@@ -280,32 +300,27 @@ paths:
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
schema: {}
|
||||
'401':
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
schema: {}
|
||||
'403':
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
schema: {}
|
||||
'404':
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
schema: {}
|
||||
'405':
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
schema: {}
|
||||
security: *ref_0
|
||||
requestBody:
|
||||
required: true
|
||||
@@ -330,32 +345,27 @@ paths:
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
schema: {}
|
||||
'401':
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
schema: {}
|
||||
'403':
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
schema: {}
|
||||
'404':
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
schema: {}
|
||||
'405':
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
schema: {}
|
||||
security: *ref_0
|
||||
requestBody:
|
||||
required: true
|
||||
@@ -379,32 +389,27 @@ paths:
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
schema: {}
|
||||
'401':
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
schema: {}
|
||||
'403':
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
schema: {}
|
||||
'404':
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
schema: {}
|
||||
'405':
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
schema: {}
|
||||
security: *ref_0
|
||||
requestBody:
|
||||
required: true
|
||||
@@ -412,6 +417,43 @@ paths:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/UpdateGenerationRequest'
|
||||
/api/public/health:
|
||||
get:
|
||||
description: Check health of API and database
|
||||
operationId: health_health
|
||||
tags:
|
||||
- Health
|
||||
parameters: []
|
||||
responses:
|
||||
'204':
|
||||
description: ''
|
||||
'400':
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema: {}
|
||||
'401':
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema: {}
|
||||
'403':
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema: {}
|
||||
'404':
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema: {}
|
||||
'405':
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema: {}
|
||||
'503':
|
||||
description: ''
|
||||
/api/public/ingestion:
|
||||
post:
|
||||
description: Ingest multiple events to Langfuse
|
||||
@@ -430,32 +472,27 @@ paths:
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
schema: {}
|
||||
'401':
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
schema: {}
|
||||
'403':
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
schema: {}
|
||||
'404':
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
schema: {}
|
||||
'405':
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
schema: {}
|
||||
security: *ref_0
|
||||
requestBody:
|
||||
required: true
|
||||
@@ -496,32 +533,27 @@ paths:
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
schema: {}
|
||||
'401':
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
schema: {}
|
||||
'403':
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
schema: {}
|
||||
'404':
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
schema: {}
|
||||
'405':
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
schema: {}
|
||||
security: *ref_0
|
||||
/api/public/observations:
|
||||
get:
|
||||
@@ -583,32 +615,66 @@ paths:
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
schema: {}
|
||||
'401':
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
schema: {}
|
||||
'403':
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
schema: {}
|
||||
'404':
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
schema: {}
|
||||
'405':
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema: {}
|
||||
security: *ref_0
|
||||
/api/public/projects:
|
||||
get:
|
||||
operationId: projects_get
|
||||
tags:
|
||||
- Projects
|
||||
parameters: []
|
||||
responses:
|
||||
'200':
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
$ref: '#/components/schemas/Projects'
|
||||
'400':
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema: {}
|
||||
'401':
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema: {}
|
||||
'403':
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema: {}
|
||||
'404':
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema: {}
|
||||
'405':
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema: {}
|
||||
security: *ref_0
|
||||
/api/public/scores:
|
||||
post:
|
||||
@@ -628,32 +694,27 @@ paths:
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
schema: {}
|
||||
'401':
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
schema: {}
|
||||
'403':
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
schema: {}
|
||||
'404':
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
schema: {}
|
||||
'405':
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
schema: {}
|
||||
security: *ref_0
|
||||
requestBody:
|
||||
required: true
|
||||
@@ -702,32 +763,73 @@ paths:
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
schema: {}
|
||||
'401':
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
schema: {}
|
||||
'403':
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
schema: {}
|
||||
'404':
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
schema: {}
|
||||
'405':
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema: {}
|
||||
security: *ref_0
|
||||
/api/public/sessions/{sessionId}:
|
||||
get:
|
||||
description: Get a session
|
||||
operationId: sessions_get
|
||||
tags:
|
||||
- Sessions
|
||||
parameters:
|
||||
- name: sessionId
|
||||
in: path
|
||||
description: The unique id of a session
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
'200':
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
$ref: '#/components/schemas/SessionWithTraces'
|
||||
'400':
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema: {}
|
||||
'401':
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema: {}
|
||||
'403':
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema: {}
|
||||
'404':
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema: {}
|
||||
'405':
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema: {}
|
||||
security: *ref_0
|
||||
/api/public/spans:
|
||||
post:
|
||||
@@ -747,32 +849,27 @@ paths:
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
schema: {}
|
||||
'401':
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
schema: {}
|
||||
'403':
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
schema: {}
|
||||
'404':
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
schema: {}
|
||||
'405':
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
schema: {}
|
||||
security: *ref_0
|
||||
requestBody:
|
||||
required: true
|
||||
@@ -797,32 +894,27 @@ paths:
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
schema: {}
|
||||
'401':
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
schema: {}
|
||||
'403':
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
schema: {}
|
||||
'404':
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
schema: {}
|
||||
'405':
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
schema: {}
|
||||
security: *ref_0
|
||||
requestBody:
|
||||
required: true
|
||||
@@ -848,32 +940,27 @@ paths:
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
schema: {}
|
||||
'401':
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
schema: {}
|
||||
'403':
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
schema: {}
|
||||
'404':
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
schema: {}
|
||||
'405':
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
schema: {}
|
||||
security: *ref_0
|
||||
requestBody:
|
||||
required: true
|
||||
@@ -922,32 +1009,27 @@ paths:
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
schema: {}
|
||||
'401':
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
schema: {}
|
||||
'403':
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
schema: {}
|
||||
'404':
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
schema: {}
|
||||
'405':
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
schema: {}
|
||||
security: *ref_0
|
||||
/api/public/traces/{traceId}:
|
||||
get:
|
||||
@@ -973,32 +1055,27 @@ paths:
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
schema: {}
|
||||
'401':
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
schema: {}
|
||||
'403':
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
schema: {}
|
||||
'404':
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
schema: {}
|
||||
'405':
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
schema: {}
|
||||
security: *ref_0
|
||||
components:
|
||||
schemas:
|
||||
@@ -1085,6 +1162,13 @@ components:
|
||||
name:
|
||||
type: string
|
||||
nullable: true
|
||||
input:
|
||||
nullable: true
|
||||
output:
|
||||
nullable: true
|
||||
sessionId:
|
||||
type: string
|
||||
nullable: true
|
||||
release:
|
||||
type: string
|
||||
nullable: true
|
||||
@@ -1096,6 +1180,10 @@ components:
|
||||
nullable: true
|
||||
metadata:
|
||||
nullable: true
|
||||
public:
|
||||
type: boolean
|
||||
nullable: true
|
||||
description: Public traces are accessible via url without login
|
||||
required:
|
||||
- id
|
||||
- timestamp
|
||||
@@ -1135,6 +1223,33 @@ components:
|
||||
- scores
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/Trace'
|
||||
Session:
|
||||
title: Session
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
createdAt:
|
||||
type: string
|
||||
format: date-time
|
||||
projectId:
|
||||
type: string
|
||||
required:
|
||||
- id
|
||||
- createdAt
|
||||
- projectId
|
||||
SessionWithTraces:
|
||||
title: SessionWithTraces
|
||||
type: object
|
||||
properties:
|
||||
traces:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/Trace'
|
||||
required:
|
||||
- traces
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/Session'
|
||||
Observation:
|
||||
title: Observation
|
||||
type: object
|
||||
@@ -1388,6 +1503,9 @@ components:
|
||||
input: {}
|
||||
expectedOutput:
|
||||
nullable: true
|
||||
id:
|
||||
type: string
|
||||
nullable: true
|
||||
required:
|
||||
- datasetName
|
||||
- input
|
||||
@@ -1425,6 +1543,10 @@ components:
|
||||
name:
|
||||
type: string
|
||||
nullable: true
|
||||
startTime:
|
||||
type: string
|
||||
format: date-time
|
||||
nullable: true
|
||||
endTime:
|
||||
type: string
|
||||
format: date-time
|
||||
@@ -1577,6 +1699,27 @@ components:
|
||||
required:
|
||||
- data
|
||||
- meta
|
||||
Projects:
|
||||
title: Projects
|
||||
type: object
|
||||
properties:
|
||||
data:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/Project'
|
||||
required:
|
||||
- data
|
||||
Project:
|
||||
title: Project
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
required:
|
||||
- id
|
||||
- name
|
||||
CreateScoreRequest:
|
||||
title: CreateScoreRequest
|
||||
type: object
|
||||
@@ -1623,6 +1766,10 @@ components:
|
||||
traceId:
|
||||
type: string
|
||||
nullable: true
|
||||
startTime:
|
||||
type: string
|
||||
format: date-time
|
||||
nullable: true
|
||||
endTime:
|
||||
type: string
|
||||
format: date-time
|
||||
@@ -1663,6 +1810,13 @@ components:
|
||||
externalId:
|
||||
type: string
|
||||
nullable: true
|
||||
input:
|
||||
nullable: true
|
||||
output:
|
||||
nullable: true
|
||||
sessionId:
|
||||
type: string
|
||||
nullable: true
|
||||
release:
|
||||
type: string
|
||||
nullable: true
|
||||
@@ -1671,6 +1825,10 @@ components:
|
||||
nullable: true
|
||||
metadata:
|
||||
nullable: true
|
||||
public:
|
||||
type: boolean
|
||||
nullable: true
|
||||
description: Make trace publicly accessible via url
|
||||
Traces:
|
||||
title: Traces
|
||||
type: object
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
"_type": "endpoint",
|
||||
"name": "Create",
|
||||
"request": {
|
||||
"description": "Create a dataset item",
|
||||
"description": "Create a dataset item, upserts on id",
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/public/dataset-items",
|
||||
"host": [
|
||||
@@ -65,7 +65,7 @@
|
||||
"auth": null,
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"datasetName\": \"example\",\n \"input\": \"UNKNOWN\",\n \"expectedOutput\": \"UNKNOWN\"\n}",
|
||||
"raw": "{\n \"datasetName\": \"example\",\n \"input\": \"UNKNOWN\",\n \"expectedOutput\": \"UNKNOWN\",\n \"id\": \"example\"\n}",
|
||||
"options": {
|
||||
"raw": {
|
||||
"language": "json"
|
||||
@@ -74,6 +74,38 @@
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
},
|
||||
{
|
||||
"_type": "endpoint",
|
||||
"name": "Get",
|
||||
"request": {
|
||||
"description": "Get a specific dataset item",
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/public/dataset-items/:id",
|
||||
"host": [
|
||||
"{{baseUrl}}"
|
||||
],
|
||||
"path": [
|
||||
"api",
|
||||
"public",
|
||||
"dataset-items",
|
||||
":id"
|
||||
],
|
||||
"query": [],
|
||||
"variable": [
|
||||
{
|
||||
"key": "id",
|
||||
"value": "",
|
||||
"description": null
|
||||
}
|
||||
]
|
||||
},
|
||||
"header": [],
|
||||
"method": "GET",
|
||||
"auth": null,
|
||||
"body": null
|
||||
},
|
||||
"response": []
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -329,7 +361,7 @@
|
||||
"auth": null,
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"generationId\": \"example\",\n \"traceId\": \"example\",\n \"name\": \"example\",\n \"endTime\": \"1994-11-05T13:15:30Z\",\n \"completionStartTime\": \"1994-11-05T13:15:30Z\",\n \"model\": \"example\",\n \"modelParameters\": {\n \"example\": \"example\"\n },\n \"prompt\": \"UNKNOWN\",\n \"version\": \"example\",\n \"metadata\": \"UNKNOWN\",\n \"completion\": \"UNKNOWN\",\n \"usage\": {\n \"promptTokens\": 0,\n \"completionTokens\": 0,\n \"totalTokens\": 0\n },\n \"level\": \"DEBUG\",\n \"statusMessage\": \"example\"\n}",
|
||||
"raw": "{\n \"generationId\": \"example\",\n \"traceId\": \"example\",\n \"name\": \"example\",\n \"startTime\": \"1994-11-05T13:15:30Z\",\n \"endTime\": \"1994-11-05T13:15:30Z\",\n \"completionStartTime\": \"1994-11-05T13:15:30Z\",\n \"model\": \"example\",\n \"modelParameters\": {\n \"example\": \"example\"\n },\n \"prompt\": \"UNKNOWN\",\n \"version\": \"example\",\n \"metadata\": \"UNKNOWN\",\n \"completion\": \"UNKNOWN\",\n \"usage\": {\n \"promptTokens\": 0,\n \"completionTokens\": 0,\n \"totalTokens\": 0\n },\n \"level\": \"DEBUG\",\n \"statusMessage\": \"example\"\n}",
|
||||
"options": {
|
||||
"raw": {
|
||||
"language": "json"
|
||||
@@ -341,6 +373,38 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"_type": "container",
|
||||
"description": null,
|
||||
"name": "Health",
|
||||
"item": [
|
||||
{
|
||||
"_type": "endpoint",
|
||||
"name": "Health",
|
||||
"request": {
|
||||
"description": "Check health of API and database",
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/public/health",
|
||||
"host": [
|
||||
"{{baseUrl}}"
|
||||
],
|
||||
"path": [
|
||||
"api",
|
||||
"public",
|
||||
"health"
|
||||
],
|
||||
"query": [],
|
||||
"variable": []
|
||||
},
|
||||
"header": [],
|
||||
"method": "GET",
|
||||
"auth": null,
|
||||
"body": null
|
||||
},
|
||||
"response": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"_type": "container",
|
||||
"description": null,
|
||||
@@ -369,7 +433,7 @@
|
||||
"auth": null,
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"batch\": [\n {\n \"type\": \"trace-create\",\n \"id\": \"example\",\n \"timestamp\": \"example\",\n \"body\": {\n \"id\": \"example\",\n \"timestamp\": \"1994-11-05T13:15:30Z\",\n \"name\": \"example\",\n \"release\": \"example\",\n \"version\": \"example\",\n \"userId\": \"example\",\n \"metadata\": \"UNKNOWN\"\n }\n }\n ]\n}",
|
||||
"raw": "{\n \"batch\": [\n {\n \"type\": \"trace-create\",\n \"id\": \"example\",\n \"timestamp\": \"example\",\n \"body\": {\n \"id\": \"example\",\n \"timestamp\": \"1994-11-05T13:15:30Z\",\n \"name\": \"example\",\n \"input\": \"UNKNOWN\",\n \"output\": \"UNKNOWN\",\n \"sessionId\": \"example\",\n \"release\": \"example\",\n \"version\": \"example\",\n \"userId\": \"example\",\n \"metadata\": \"UNKNOWN\",\n \"public\": true\n }\n }\n ]\n}",
|
||||
"options": {
|
||||
"raw": {
|
||||
"language": "json"
|
||||
@@ -481,6 +545,38 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"_type": "container",
|
||||
"description": null,
|
||||
"name": "Projects",
|
||||
"item": [
|
||||
{
|
||||
"_type": "endpoint",
|
||||
"name": "Get",
|
||||
"request": {
|
||||
"description": null,
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/public/projects",
|
||||
"host": [
|
||||
"{{baseUrl}}"
|
||||
],
|
||||
"path": [
|
||||
"api",
|
||||
"public",
|
||||
"projects"
|
||||
],
|
||||
"query": [],
|
||||
"variable": []
|
||||
},
|
||||
"header": [],
|
||||
"method": "GET",
|
||||
"auth": null,
|
||||
"body": null
|
||||
},
|
||||
"response": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"_type": "container",
|
||||
"description": null,
|
||||
@@ -567,6 +663,45 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"_type": "container",
|
||||
"description": null,
|
||||
"name": "Sessions",
|
||||
"item": [
|
||||
{
|
||||
"_type": "endpoint",
|
||||
"name": "Get",
|
||||
"request": {
|
||||
"description": "Get a session",
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/public/sessions/:sessionId",
|
||||
"host": [
|
||||
"{{baseUrl}}"
|
||||
],
|
||||
"path": [
|
||||
"api",
|
||||
"public",
|
||||
"sessions",
|
||||
":sessionId"
|
||||
],
|
||||
"query": [],
|
||||
"variable": [
|
||||
{
|
||||
"key": "sessionId",
|
||||
"value": "",
|
||||
"description": "The unique id of a session"
|
||||
}
|
||||
]
|
||||
},
|
||||
"header": [],
|
||||
"method": "GET",
|
||||
"auth": null,
|
||||
"body": null
|
||||
},
|
||||
"response": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"_type": "container",
|
||||
"description": null,
|
||||
@@ -628,7 +763,7 @@
|
||||
"auth": null,
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"spanId\": \"example\",\n \"traceId\": \"example\",\n \"endTime\": \"1994-11-05T13:15:30Z\",\n \"name\": \"example\",\n \"metadata\": \"UNKNOWN\",\n \"input\": \"UNKNOWN\",\n \"output\": \"UNKNOWN\",\n \"level\": \"DEBUG\",\n \"version\": \"example\",\n \"statusMessage\": \"example\"\n}",
|
||||
"raw": "{\n \"spanId\": \"example\",\n \"traceId\": \"example\",\n \"startTime\": \"1994-11-05T13:15:30Z\",\n \"endTime\": \"1994-11-05T13:15:30Z\",\n \"name\": \"example\",\n \"metadata\": \"UNKNOWN\",\n \"input\": \"UNKNOWN\",\n \"output\": \"UNKNOWN\",\n \"level\": \"DEBUG\",\n \"version\": \"example\",\n \"statusMessage\": \"example\"\n}",
|
||||
"options": {
|
||||
"raw": {
|
||||
"language": "json"
|
||||
@@ -668,7 +803,7 @@
|
||||
"auth": null,
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"id\": \"example\",\n \"name\": \"example\",\n \"userId\": \"example\",\n \"externalId\": \"example\",\n \"release\": \"example\",\n \"version\": \"example\",\n \"metadata\": \"UNKNOWN\"\n}",
|
||||
"raw": "{\n \"id\": \"example\",\n \"name\": \"example\",\n \"userId\": \"example\",\n \"externalId\": \"example\",\n \"input\": \"UNKNOWN\",\n \"output\": \"UNKNOWN\",\n \"sessionId\": \"example\",\n \"release\": \"example\",\n \"version\": \"example\",\n \"metadata\": \"UNKNOWN\",\n \"public\": true\n}",
|
||||
"options": {
|
||||
"raw": {
|
||||
"language": "json"
|
||||
|
||||
+31
-15
@@ -23,21 +23,37 @@ const nextConfig = {
|
||||
output: "standalone",
|
||||
|
||||
async headers() {
|
||||
// Required to check authentication status from langfuse.com
|
||||
if (env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION !== undefined)
|
||||
return [
|
||||
{
|
||||
source: '/api/auth/session',
|
||||
headers: [
|
||||
{ key: "Access-Control-Allow-Origin", value: "https://langfuse.com" },
|
||||
{ key: "Access-Control-Allow-Credentials", value: "true" },
|
||||
{ key: "Access-Control-Allow-Methods", value: "GET,POST" },
|
||||
{ key: "Access-Control-Allow-Headers", value: "Content-Type, Authorization" },
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
return []
|
||||
return [
|
||||
{
|
||||
source: "/:path*",
|
||||
headers: [
|
||||
{
|
||||
key: "x-frame-options",
|
||||
value: "SAMEORIGIN",
|
||||
},
|
||||
],
|
||||
},
|
||||
// Required to check authentication status from langfuse.com
|
||||
...(env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION !== undefined ?
|
||||
[
|
||||
{
|
||||
source: "/api/auth/session",
|
||||
headers: [
|
||||
{
|
||||
key: "Access-Control-Allow-Origin",
|
||||
value: "https://langfuse.com",
|
||||
},
|
||||
{ key: "Access-Control-Allow-Credentials", value: "true" },
|
||||
{ key: "Access-Control-Allow-Methods", value: "GET,POST" },
|
||||
{
|
||||
key: "Access-Control-Allow-Headers",
|
||||
value: "Content-Type, Authorization",
|
||||
},
|
||||
]
|
||||
},
|
||||
] : []
|
||||
)
|
||||
]
|
||||
},
|
||||
|
||||
// webassembly support for @dqbd/tiktoken
|
||||
|
||||
Generated
+2183
-1198
File diff suppressed because it is too large
Load Diff
+54
-42
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "langfuse-core",
|
||||
"version": "1.2.0",
|
||||
"version": "1.17.2",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"prebuild": "cp generated/openapi-client/openapi.yml public/openapi-client.yml && cp generated/openapi-server/openapi.yml public/openapi-server.yml",
|
||||
@@ -31,7 +31,7 @@
|
||||
"@heroicons/react": "^2.0.18",
|
||||
"@hookform/resolvers": "^3.3.2",
|
||||
"@next-auth/prisma-adapter": "^1.0.7",
|
||||
"@prisma/client": "^5.5.2",
|
||||
"@prisma/client": "^5.7.0",
|
||||
"@radix-ui/react-avatar": "^1.0.4",
|
||||
"@radix-ui/react-collapsible": "^1.0.3",
|
||||
"@radix-ui/react-dialog": "^1.0.5",
|
||||
@@ -48,7 +48,9 @@
|
||||
"@radix-ui/react-tabs": "^1.0.4",
|
||||
"@radix-ui/react-toggle": "^1.0.3",
|
||||
"@radix-ui/react-tooltip": "^1.0.7",
|
||||
"@sentry/nextjs": "^7.77.0",
|
||||
"@react-email/components": "^0.0.12",
|
||||
"@react-email/render": "^0.0.10",
|
||||
"@sentry/nextjs": "^7.83.0",
|
||||
"@sentry/profiling-node": "^1.2.6",
|
||||
"@t3-oss/env-nextjs": "^0.7.1",
|
||||
"@tailwindcss/forms": "^0.5.7",
|
||||
@@ -56,12 +58,12 @@
|
||||
"@tanstack/react-query": "^4.36.1",
|
||||
"@tanstack/react-table": "^8.10.7",
|
||||
"@tremor/react": "^3.11.1",
|
||||
"@trpc/client": "^10.43.3",
|
||||
"@trpc/next": "^10.43.3",
|
||||
"@trpc/react-query": "^10.43.1",
|
||||
"@trpc/server": "^10.43.1",
|
||||
"@trpc/client": "^10.44.1",
|
||||
"@trpc/next": "^10.44.1",
|
||||
"@trpc/react-query": "^10.44.1",
|
||||
"@trpc/server": "^10.44.1",
|
||||
"@vercel/edge-config": "^0.4.1",
|
||||
"axios": "^1.6.1",
|
||||
"axios": "^1.6.2",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.0.0",
|
||||
@@ -72,62 +74,64 @@
|
||||
"exponential-backoff": "^3.1.1",
|
||||
"js-yaml": "^4.1.0",
|
||||
"lodash": "^4.17.21",
|
||||
"lucide-react": "^0.292.0",
|
||||
"next": "^13.5.6",
|
||||
"lucide-react": "^0.294.0",
|
||||
"next": "^14.0.4",
|
||||
"next-auth": "^4.24.5",
|
||||
"next-query-params": "^4.2.3",
|
||||
"next-swagger-doc": "^0.4.0",
|
||||
"posthog-js": "^1.88.4",
|
||||
"posthog-node": "^3.1.3",
|
||||
"next-query-params": "^5.0.0",
|
||||
"nodemailer": "^6.9.7",
|
||||
"posthog-js": "^1.94.2",
|
||||
"posthog-node": "^3.2.0",
|
||||
"react": "18.2.0",
|
||||
"react-day-picker": "^8.9.1",
|
||||
"react-dom": "18.2.0",
|
||||
"react-hook-form": "^7.47.0",
|
||||
"react-icons": "^4.11.0",
|
||||
"react-hook-form": "^7.49.0",
|
||||
"react-icons": "^4.12.0",
|
||||
"react-responsive": "^9.0.2",
|
||||
"react18-json-view": "^0.2.7-canary.1",
|
||||
"react18-json-view": "^0.2.7-canary.2",
|
||||
"superagent": "^8.1.2",
|
||||
"superjson": "2.0.0",
|
||||
"tailwind-merge": "^1.14.0",
|
||||
"superjson": "2.2.1",
|
||||
"tailwind-merge": "^2.1.0",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"tiktoken": "^1.0.10",
|
||||
"ts-pattern": "^5.0.5",
|
||||
"tiktoken": "^1.0.11",
|
||||
"ts-pattern": "^5.0.6",
|
||||
"use-query-params": "^2.2.1",
|
||||
"uuid": "^9.0.1",
|
||||
"zod": "^3.22.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.39.0",
|
||||
"@testing-library/jest-dom": "^6.1.4",
|
||||
"@playwright/test": "^1.40.1",
|
||||
"@release-it/bumper": "^6.0.1",
|
||||
"@testing-library/jest-dom": "^6.1.5",
|
||||
"@types/bcryptjs": "^2.4.6",
|
||||
"@types/cors": "^2.8.16",
|
||||
"@types/eslint": "^8.44.6",
|
||||
"@types/jest": "^29.5.8",
|
||||
"@types/cors": "^2.8.17",
|
||||
"@types/eslint": "^8.44.8",
|
||||
"@types/jest": "^29.5.11",
|
||||
"@types/js-yaml": "^4.0.9",
|
||||
"@types/lodash": "^4.14.201",
|
||||
"@types/node": "^20.8.10",
|
||||
"@types/react": "^18.2.37",
|
||||
"@types/react-dom": "^18.2.15",
|
||||
"@types/lodash": "^4.14.202",
|
||||
"@types/node": "20.10.0",
|
||||
"@types/nodemailer": "^6.4.14",
|
||||
"@types/react": "^18.2.43",
|
||||
"@types/react-dom": "^18.2.17",
|
||||
"@types/swagger-ui-react": "^4.18.3",
|
||||
"@types/uuid": "^9.0.7",
|
||||
"@typescript-eslint/eslint-plugin": "^6.9.1",
|
||||
"@typescript-eslint/parser": "^6.9.1",
|
||||
"@typescript-eslint/eslint-plugin": "^6.14.0",
|
||||
"@typescript-eslint/parser": "^6.13.2",
|
||||
"autoprefixer": "^10.4.16",
|
||||
"dotenv-cli": "^7.3.0",
|
||||
"eslint": "^8.52.0",
|
||||
"eslint-config-next": "^13.5.6",
|
||||
"eslint": "^8.55.0",
|
||||
"eslint-config-next": "^14.0.4",
|
||||
"jest": "^29.7.0",
|
||||
"jest-environment-jsdom": "^29.7.0",
|
||||
"postcss": "^8.4.31",
|
||||
"prettier": "^3.0.3",
|
||||
"prettier-plugin-tailwindcss": "^0.5.7",
|
||||
"prisma": "^5.5.2",
|
||||
"release-it": "^17.0.0",
|
||||
"postcss": "^8.4.32",
|
||||
"prettier": "^3.1.1",
|
||||
"prettier-plugin-tailwindcss": "^0.5.9",
|
||||
"prisma": "^5.7.0",
|
||||
"release-it": "^17.0.1",
|
||||
"swagger-jsdoc": "^6.2.8",
|
||||
"tailwindcss": "^3.3.5",
|
||||
"ts-node": "^10.9.1",
|
||||
"tailwindcss": "^3.3.6",
|
||||
"ts-node": "^10.9.2",
|
||||
"tsconfig-paths": "^4.2.0",
|
||||
"typescript": "^5.2.2"
|
||||
"typescript": "^5.3.3"
|
||||
},
|
||||
"ct3aMetadata": {
|
||||
"initVersion": "7.13.0"
|
||||
@@ -145,6 +149,14 @@
|
||||
"web": true,
|
||||
"autoGenerate": true,
|
||||
"releaseName": "v${version}"
|
||||
},
|
||||
"plugins": {
|
||||
"@release-it/bumper": {
|
||||
"out": {
|
||||
"file": "./src/constants/VERSION.ts",
|
||||
"type": "application/typescript"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "cron_jobs" ADD COLUMN "job_started_at" TIMESTAMP(3);
|
||||
@@ -0,0 +1,3 @@
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "traces" ADD COLUMN "bookmarked" BOOLEAN NOT NULL DEFAULT false;
|
||||
@@ -0,0 +1,27 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "membership_invitations" (
|
||||
"id" TEXT NOT NULL,
|
||||
"email" TEXT NOT NULL,
|
||||
"role" "MembershipRole" NOT NULL,
|
||||
"project_id" TEXT NOT NULL,
|
||||
"sender_id" TEXT,
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "membership_invitations_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "membership_invitations_id_key" ON "membership_invitations"("id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "membership_invitations_project_id_idx" ON "membership_invitations"("project_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "membership_invitations_email_idx" ON "membership_invitations"("email");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "membership_invitations" ADD CONSTRAINT "membership_invitations_project_id_fkey" FOREIGN KEY ("project_id") REFERENCES "projects"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "membership_invitations" ADD CONSTRAINT "membership_invitations_sender_id_fkey" FOREIGN KEY ("sender_id") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
@@ -0,0 +1,34 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "traces" ADD COLUMN "input" JSONB,
|
||||
ADD COLUMN "output" JSONB,
|
||||
ADD COLUMN "session_id" TEXT;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "trace_sessions" (
|
||||
"id" TEXT NOT NULL,
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"project_id" TEXT NOT NULL,
|
||||
"bookmarked" BOOLEAN NOT NULL DEFAULT false,
|
||||
"public" BOOLEAN NOT NULL DEFAULT false,
|
||||
|
||||
CONSTRAINT "trace_sessions_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "trace_sessions_project_id_idx" ON "trace_sessions"("project_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "trace_sessions_created_at_idx" ON "trace_sessions"("created_at");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "trace_sessions_id_project_id_key" ON "trace_sessions"("id", "project_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "traces_session_id_idx" ON "traces"("session_id");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "trace_sessions" ADD CONSTRAINT "trace_sessions_project_id_fkey" FOREIGN KEY ("project_id") REFERENCES "projects"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "traces" ADD CONSTRAINT "traces_session_id_fkey" FOREIGN KEY ("session_id") REFERENCES "trace_sessions"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
+62
-20
@@ -47,19 +47,20 @@ model Session {
|
||||
}
|
||||
|
||||
model User {
|
||||
id String @id @default(cuid())
|
||||
id String @id @default(cuid())
|
||||
name String?
|
||||
email String? @unique
|
||||
emailVerified DateTime? @map("email_verified")
|
||||
email String? @unique
|
||||
emailVerified DateTime? @map("email_verified")
|
||||
password String?
|
||||
image String?
|
||||
admin Boolean @default(false)
|
||||
admin Boolean @default(false)
|
||||
accounts Account[]
|
||||
sessions Session[]
|
||||
memberships Membership[]
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
|
||||
featureFlags String[] @default([]) @map("feature_flags")
|
||||
invitations MembershipInvitation[]
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
|
||||
featureFlags String[] @default([]) @map("feature_flags")
|
||||
|
||||
@@map("users")
|
||||
}
|
||||
@@ -74,9 +75,9 @@ model VerificationToken {
|
||||
}
|
||||
|
||||
model Project {
|
||||
id String @id @default(cuid())
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
|
||||
id String @id @default(cuid())
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
|
||||
name String
|
||||
members Membership[]
|
||||
traces Trace[]
|
||||
@@ -84,6 +85,8 @@ model Project {
|
||||
apiKeys ApiKey[]
|
||||
dataset Dataset[]
|
||||
RawEvents Events[]
|
||||
invitations MembershipInvitation[]
|
||||
sessions TraceSession[]
|
||||
|
||||
@@map("projects")
|
||||
}
|
||||
@@ -122,6 +125,22 @@ model Membership {
|
||||
@@map("memberships")
|
||||
}
|
||||
|
||||
model MembershipInvitation {
|
||||
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")
|
||||
|
||||
@@index([projectId])
|
||||
@@index([email])
|
||||
@@map("membership_invitations")
|
||||
}
|
||||
|
||||
enum MembershipRole {
|
||||
OWNER
|
||||
ADMIN
|
||||
@@ -129,23 +148,45 @@ enum MembershipRole {
|
||||
VIEWER
|
||||
}
|
||||
|
||||
model Trace {
|
||||
model TraceSession {
|
||||
id String @id @default(cuid())
|
||||
externalId String? @map("external_id")
|
||||
timestamp DateTime @default(now())
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
|
||||
projectId String @map("project_id")
|
||||
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
||||
bookmarked Boolean @default(false)
|
||||
public Boolean @default(false)
|
||||
traces Trace[]
|
||||
|
||||
@@unique([id, projectId])
|
||||
@@index([projectId])
|
||||
@@index([createdAt])
|
||||
@@map("trace_sessions")
|
||||
}
|
||||
|
||||
model Trace {
|
||||
id String @id @default(cuid())
|
||||
externalId String? @map("external_id")
|
||||
timestamp DateTime @default(now())
|
||||
name String?
|
||||
userId String? @map("user_id")
|
||||
userId String? @map("user_id")
|
||||
metadata Json?
|
||||
release String?
|
||||
version String?
|
||||
projectId String @map("project_id")
|
||||
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
||||
public Boolean @default(false)
|
||||
input Json?
|
||||
output Json?
|
||||
sessionId String? @map("session_id")
|
||||
session TraceSession? @relation(fields: [sessionId], references: [id], onDelete: SetNull)
|
||||
projectId String @map("project_id")
|
||||
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
||||
public Boolean @default(false)
|
||||
bookmarked Boolean @default(false)
|
||||
|
||||
scores Score[]
|
||||
|
||||
@@unique([projectId, externalId])
|
||||
@@index([projectId])
|
||||
@@index([sessionId])
|
||||
@@index([name])
|
||||
@@index([userId])
|
||||
@@index([externalId])
|
||||
@@ -247,9 +288,10 @@ model Pricing {
|
||||
}
|
||||
|
||||
model CronJobs {
|
||||
name String @id
|
||||
lastRun DateTime? @map("last_run")
|
||||
state String?
|
||||
name String @id
|
||||
lastRun DateTime? @map("last_run")
|
||||
jobStartedAt DateTime? @map("job_started_at")
|
||||
state String?
|
||||
|
||||
@@map("cron_jobs")
|
||||
}
|
||||
|
||||
+51
-27
@@ -133,19 +133,42 @@ async function main() {
|
||||
},
|
||||
},
|
||||
userId: `user-${i % 10}`,
|
||||
session:
|
||||
Math.random() > 0.3
|
||||
? {
|
||||
connectOrCreate: {
|
||||
where: {
|
||||
id: `session-${i % 10}`,
|
||||
},
|
||||
create: {
|
||||
id: `session-${i % 10}`,
|
||||
project: {
|
||||
connect: { id: [project1.id, project2.id][i % 2] },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
: undefined,
|
||||
input:
|
||||
Math.random() > 0.3
|
||||
? "I'm looking for a React component"
|
||||
: undefined,
|
||||
output:
|
||||
Math.random() > 0.3
|
||||
? "What kind of component are you looking for?"
|
||||
: undefined,
|
||||
scores: {
|
||||
createMany: {
|
||||
data: [
|
||||
{
|
||||
name: "latency",
|
||||
value: Math.floor(Math.random() * 20),
|
||||
timestamp: traceTs,
|
||||
},
|
||||
{
|
||||
name: "feedback",
|
||||
value: Math.floor(Math.random() * 3) - 1,
|
||||
timestamp: traceTs,
|
||||
},
|
||||
...(Math.random() > 0.5
|
||||
? [
|
||||
{
|
||||
name: "feedback",
|
||||
value: Math.floor(Math.random() * 3) - 1,
|
||||
timestamp: traceTs,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(Math.random() > 0.7
|
||||
? [
|
||||
{
|
||||
@@ -305,23 +328,24 @@ async function main() {
|
||||
traceId: trace.id,
|
||||
},
|
||||
});
|
||||
|
||||
await prisma.score.create({
|
||||
data: {
|
||||
name: "quality",
|
||||
value: Math.random() * 2 - 1,
|
||||
observationId: generation.id,
|
||||
traceId: trace.id,
|
||||
},
|
||||
});
|
||||
await prisma.score.create({
|
||||
data: {
|
||||
name: "conciseness",
|
||||
value: Math.random() * 2 - 1,
|
||||
observationId: generation.id,
|
||||
traceId: trace.id,
|
||||
},
|
||||
});
|
||||
if (Math.random() > 0.6)
|
||||
await prisma.score.create({
|
||||
data: {
|
||||
name: "quality",
|
||||
value: Math.random() * 2 - 1,
|
||||
observationId: generation.id,
|
||||
traceId: trace.id,
|
||||
},
|
||||
});
|
||||
if (Math.random() > 0.6)
|
||||
await prisma.score.create({
|
||||
data: {
|
||||
name: "conciseness",
|
||||
value: Math.random() * 2 - 1,
|
||||
observationId: generation.id,
|
||||
traceId: trace.id,
|
||||
},
|
||||
});
|
||||
|
||||
generationIds.push(generation.id);
|
||||
|
||||
|
||||
-11
@@ -1,11 +0,0 @@
|
||||
# Security Policy
|
||||
|
||||
## Supported Versions
|
||||
|
||||
We always recommend using the latest version of Langfuse to ensure you get all security updates.
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
Please report security vulnerabilities to security@langfuse.com.
|
||||
|
||||
We currently do not operate a bug bounty program.
|
||||
@@ -1,9 +1,12 @@
|
||||
import { prisma } from "@/src/server/db";
|
||||
import { jsonSchema } from "@/src/utils/zod";
|
||||
import * as Sentry from "@sentry/nextjs";
|
||||
import { ProfilingIntegration } from "@sentry/profiling-node";
|
||||
import type { TransactionEvent } from "@sentry/types";
|
||||
|
||||
if (process.env.NEXT_PUBLIC_SENTRY_DSN)
|
||||
Sentry.init({
|
||||
// debug: true,
|
||||
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
|
||||
|
||||
// Set tracesSampleRate to 1.0 to capture 100%
|
||||
@@ -18,6 +21,35 @@ if (process.env.NEXT_PUBLIC_SENTRY_DSN)
|
||||
new Sentry.Integrations.Prisma({ client: prisma }),
|
||||
],
|
||||
|
||||
// filter out passwords from the signup request body
|
||||
// transaction events are sentry transactions which include logs and spans.
|
||||
beforeSendTransaction(transaction: TransactionEvent) {
|
||||
if (
|
||||
transaction.request &&
|
||||
typeof transaction.request.data === "string" &&
|
||||
transaction.request.url &&
|
||||
transaction.request.url.includes("api/auth/signup")
|
||||
) {
|
||||
const jsonBody = jsonSchema.safeParse(transaction.request.data);
|
||||
|
||||
if (
|
||||
jsonBody.success &&
|
||||
typeof jsonBody.data === "object" &&
|
||||
"data" in jsonBody.data
|
||||
) {
|
||||
delete jsonBody.data.password;
|
||||
transaction.request.data = JSON.stringify(jsonBody.data);
|
||||
transaction.request.data = JSON.stringify(jsonBody);
|
||||
} else {
|
||||
console.log("Signup: Non Json Request body");
|
||||
}
|
||||
|
||||
return transaction;
|
||||
}
|
||||
|
||||
return transaction;
|
||||
},
|
||||
|
||||
// ...
|
||||
|
||||
// Note: if you want to override the automatic release value, do not set a
|
||||
|
||||
@@ -0,0 +1,187 @@
|
||||
/** @jest-environment node */
|
||||
|
||||
import { prisma } from "@/src/server/db";
|
||||
import { makeAPICall, pruneDatabase } from "@/src/__tests__/test-utils";
|
||||
import { v4 } from "uuid";
|
||||
|
||||
describe("/api/public/datasets and /api/public/dataset-items API Endpoints", () => {
|
||||
beforeEach(async () => await pruneDatabase());
|
||||
afterEach(async () => await pruneDatabase());
|
||||
|
||||
it("should create and get a dataset", async () => {
|
||||
await makeAPICall("POST", "/api/public/datasets", {
|
||||
name: "dataset-name",
|
||||
});
|
||||
|
||||
const dbDataset = await prisma.dataset.findMany({
|
||||
where: {
|
||||
name: "dataset-name",
|
||||
},
|
||||
});
|
||||
|
||||
expect(dbDataset.length).toBeGreaterThan(0);
|
||||
|
||||
const getDataset = await makeAPICall(
|
||||
"GET",
|
||||
`/api/public/datasets/dataset-name`,
|
||||
);
|
||||
|
||||
expect(getDataset.status).toBe(200);
|
||||
expect(getDataset.body).toMatchObject({
|
||||
name: "dataset-name",
|
||||
});
|
||||
});
|
||||
|
||||
it("should create and get a dataset item (via datasets and individually)", async () => {
|
||||
await makeAPICall("POST", "/api/public/datasets", {
|
||||
name: "dataset-name",
|
||||
});
|
||||
await makeAPICall("POST", "/api/public/dataset-items", {
|
||||
datasetName: "dataset-name",
|
||||
input: { key: "value" },
|
||||
expectedOutput: { key: "value" },
|
||||
});
|
||||
const dbDatasetItem = await prisma.datasetItem.findFirst({
|
||||
where: {
|
||||
dataset: {
|
||||
name: "dataset-name",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(dbDatasetItem).not.toBeNull();
|
||||
|
||||
const getDataset = await makeAPICall(
|
||||
"GET",
|
||||
`/api/public/datasets/dataset-name`,
|
||||
);
|
||||
expect(getDataset.status).toBe(200);
|
||||
expect(getDataset.body).toMatchObject({
|
||||
name: "dataset-name",
|
||||
items: [
|
||||
{
|
||||
id: dbDatasetItem!.id,
|
||||
input: { key: "value" },
|
||||
expectedOutput: { key: "value" },
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const getDatasetItem = await makeAPICall(
|
||||
"GET",
|
||||
`/api/public/dataset-items/${dbDatasetItem!.id}`,
|
||||
);
|
||||
expect(getDatasetItem.status).toBe(200);
|
||||
expect(getDatasetItem.body).toMatchObject({
|
||||
id: dbDatasetItem!.id,
|
||||
input: { key: "value" },
|
||||
expectedOutput: { key: "value" },
|
||||
});
|
||||
});
|
||||
|
||||
it("should upsert a dataset item", async () => {
|
||||
await makeAPICall("POST", "/api/public/datasets", {
|
||||
name: "dataset-name",
|
||||
});
|
||||
|
||||
const item1 = await makeAPICall("POST", "/api/public/dataset-items", {
|
||||
id: "dataset-item-id",
|
||||
datasetName: "dataset-name",
|
||||
input: { key: "value" },
|
||||
});
|
||||
expect(item1.status).toBe(200);
|
||||
expect(item1.body).toMatchObject({
|
||||
id: "dataset-item-id",
|
||||
});
|
||||
|
||||
const item2 = await makeAPICall("POST", "/api/public/dataset-items", {
|
||||
id: "dataset-item-id",
|
||||
datasetName: "dataset-name",
|
||||
input: { key: "value2" },
|
||||
});
|
||||
expect(item2.status).toBe(200);
|
||||
expect(item2.body).toMatchObject({
|
||||
id: "dataset-item-id",
|
||||
input: { key: "value2" },
|
||||
});
|
||||
|
||||
const dbDatasetItem = await prisma.datasetItem.findFirst({
|
||||
where: { id: "dataset-item-id" },
|
||||
});
|
||||
expect(dbDatasetItem).not.toBeNull();
|
||||
expect(dbDatasetItem?.input).toMatchObject({ key: "value2" });
|
||||
});
|
||||
|
||||
it("should create and get a dataset run", async () => {
|
||||
const dataset = await makeAPICall("POST", "/api/public/datasets", {
|
||||
name: "dataset-name",
|
||||
});
|
||||
expect(dataset.status).toBe(200);
|
||||
expect(dataset.body).toMatchObject({
|
||||
name: "dataset-name",
|
||||
});
|
||||
await makeAPICall("POST", "/api/public/dataset-items", {
|
||||
datasetName: "dataset-name",
|
||||
id: "dataset-item-id",
|
||||
input: { key: "value" },
|
||||
expectedOutput: { key: "value" },
|
||||
});
|
||||
const traceId = v4();
|
||||
const observationId = v4();
|
||||
const response = await makeAPICall("POST", "/api/public/ingestion", {
|
||||
batch: [
|
||||
{
|
||||
id: v4(),
|
||||
type: "trace-create",
|
||||
timestamp: new Date().toISOString(),
|
||||
body: {
|
||||
id: traceId,
|
||||
name: "trace-name",
|
||||
userId: "user-1",
|
||||
metadata: { key: "value" },
|
||||
release: "1.0.0",
|
||||
version: "2.0.0",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: v4(),
|
||||
type: "observation-create",
|
||||
timestamp: new Date().toISOString(),
|
||||
body: {
|
||||
id: observationId,
|
||||
traceId: traceId,
|
||||
type: "GENERATION",
|
||||
name: "generation-name",
|
||||
startTime: "2021-01-01T00:00:00.000Z",
|
||||
endTime: "2021-01-01T00:00:00.000Z",
|
||||
modelParameters: { key: "value" },
|
||||
input: { key: "value" },
|
||||
metadata: { key: "value" },
|
||||
version: "2.0.0",
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(response.status).toBe(207);
|
||||
|
||||
const runItem = await makeAPICall("POST", "/api/public/dataset-run-items", {
|
||||
datasetItemId: "dataset-item-id",
|
||||
observationId: observationId,
|
||||
runName: "run-name",
|
||||
});
|
||||
const dbRun = await prisma.datasetRuns.findFirst({
|
||||
where: {
|
||||
name: "run-name",
|
||||
},
|
||||
});
|
||||
expect(dbRun).not.toBeNull();
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
expect(dbRun?.datasetId).toBe(dataset.body.id);
|
||||
expect(runItem.status).toBe(200);
|
||||
expect(runItem.body).toMatchObject({
|
||||
datasetItemId: "dataset-item-id",
|
||||
observationId: observationId,
|
||||
datasetRunId: dbRun?.id,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,7 @@
|
||||
/** @jest-environment node */
|
||||
|
||||
import { makeAPICall, pruneDatabase } from "@/src/__tests__/test-utils";
|
||||
import { cleanEvent } from "@/src/pages/api/public/ingestion";
|
||||
import { prisma } from "@/src/server/db";
|
||||
import { v4 } from "uuid";
|
||||
|
||||
@@ -247,6 +248,108 @@ describe("/api/public/ingestion API Endpoint", () => {
|
||||
expect(dbTrace.length).toBe(1);
|
||||
});
|
||||
|
||||
it("should fail for auth errors", async () => {
|
||||
const traceId = v4();
|
||||
const scoreId = v4();
|
||||
|
||||
const responseOne = await makeAPICall("POST", "/api/public/ingestion", {
|
||||
batch: [
|
||||
{
|
||||
id: v4(),
|
||||
type: "trace-create",
|
||||
timestamp: new Date().toISOString(),
|
||||
body: {
|
||||
id: traceId,
|
||||
name: "trace-name",
|
||||
userId: "user-1",
|
||||
metadata: { key: "value" },
|
||||
release: "1.0.0",
|
||||
version: "2.0.0",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: v4(),
|
||||
type: "score-create",
|
||||
timestamp: new Date().toISOString(),
|
||||
body: {
|
||||
id: scoreId,
|
||||
name: "score-name",
|
||||
value: 100.5,
|
||||
traceId: "some-random-id",
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
console.log(responseOne.body);
|
||||
expect(responseOne.status).toBe(207);
|
||||
|
||||
expect("errors" in responseOne.body).toBe(true);
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
expect(responseOne.body?.errors.length).toBe(1);
|
||||
expect("successes" in responseOne.body).toBe(true);
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
expect(responseOne.body?.successes.length).toBe(1);
|
||||
|
||||
const dbTrace = await prisma.trace.findMany({
|
||||
where: {
|
||||
name: "trace-name",
|
||||
},
|
||||
});
|
||||
|
||||
expect(dbTrace.length).toBe(1);
|
||||
});
|
||||
|
||||
it("should fail for resource not found", async () => {
|
||||
const traceId = v4();
|
||||
|
||||
const responseOne = await makeAPICall("POST", "/api/public/ingestion", {
|
||||
batch: [
|
||||
{
|
||||
id: v4(),
|
||||
type: "trace-create",
|
||||
timestamp: new Date().toISOString(),
|
||||
body: {
|
||||
id: traceId,
|
||||
name: "trace-name",
|
||||
userId: "user-1",
|
||||
metadata: { key: "value" },
|
||||
release: "1.0.0",
|
||||
version: "2.0.0",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: v4(),
|
||||
type: "observation-update",
|
||||
timestamp: new Date().toISOString(),
|
||||
body: {
|
||||
id: "some-random-id",
|
||||
type: "GENERATION",
|
||||
output: { key: "this is a great gpt output" },
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
console.log(responseOne.body);
|
||||
expect(responseOne.status).toBe(207);
|
||||
|
||||
expect("errors" in responseOne.body).toBe(true);
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
expect(responseOne.body?.errors.length).toBe(1);
|
||||
expect("successes" in responseOne.body).toBe(true);
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
expect(responseOne.body?.successes.length).toBe(1);
|
||||
|
||||
const dbTrace = await prisma.trace.findMany({
|
||||
where: {
|
||||
name: "trace-name",
|
||||
},
|
||||
});
|
||||
|
||||
expect(dbTrace.length).toBe(1);
|
||||
});
|
||||
|
||||
it("should update all token counts if update does not contain model name", async () => {
|
||||
const traceId = v4();
|
||||
const generationId = v4();
|
||||
@@ -584,4 +687,80 @@ describe("/api/public/ingestion API Endpoint", () => {
|
||||
|
||||
expect(dbGeneration).toBeTruthy();
|
||||
});
|
||||
|
||||
it("filters out NULL characters", async () => {
|
||||
const traceId = v4();
|
||||
const generationId = v4();
|
||||
|
||||
const responseOne = await makeAPICall("POST", "/api/public/ingestion", {
|
||||
batch: [
|
||||
{
|
||||
id: v4(),
|
||||
type: "trace-create",
|
||||
timestamp: new Date().toISOString(),
|
||||
body: {
|
||||
id: traceId,
|
||||
name: "trace-name",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: v4(),
|
||||
type: "observation-create",
|
||||
timestamp: new Date().toISOString(),
|
||||
body: {
|
||||
id: generationId,
|
||||
traceId: traceId,
|
||||
type: "GENERATION",
|
||||
name: "generation-name",
|
||||
traceIdType: "LANGFUSE",
|
||||
input: {
|
||||
key: "IB\nibo.org Change site\nIB Home / . . . / News / News about the IB / Why ChatGPT is an opportunity for schools IB Home / News / News about the IB / Why ChatGPT is an opportunity for schools Why ChatGPT is an opportunity for schools Published: 06 March 2023 Last updated: 06 June 2023 Date published: 28 February 2023 Dr Matthew Glanville, Head of Assessment Principles and Practice Source: Why ChatGPT is an opportunity for schools | The Times Those of us who work in the schools or exam sector should not be terri \u0000 ed by ChatGPT and the rise of AI software – we should be excited. We should embrace it as an extraordinary opportunity. Contrary to some stark warnings, it is not the end of exams, nor even a huge threat to coursework, but it does bring into very sharp focus the impact that arti \u0000 ",
|
||||
},
|
||||
output: {
|
||||
key: "제점이 있었죠. 그중 하나는 일제가 한국의 신용체계를 망가뜨린 채 한국을 떠났다는 겁니다. 해방전 일제는 조선의 신용체계를 거의 독점적으로 소유한 상황이었습니다. 1945년 6월 기준 일제는 조선의 본점을 둔 전체은행 5개의 불입자본 총액의 89.7%를",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(responseOne.status).toBe(207);
|
||||
|
||||
const dbTrace = await prisma.trace.findMany({
|
||||
where: {
|
||||
id: traceId,
|
||||
},
|
||||
});
|
||||
|
||||
expect(dbTrace.length).toEqual(1);
|
||||
|
||||
const dbGeneration = await prisma.observation.findUnique({
|
||||
where: {
|
||||
id: generationId,
|
||||
},
|
||||
});
|
||||
expect(dbGeneration?.input).toStrictEqual({
|
||||
key: `IB
|
||||
ibo.org Change site
|
||||
IB Home / . . . / News / News about the IB / Why ChatGPT is an opportunity for schools IB Home / News / News about the IB / Why ChatGPT is an opportunity for schools Why ChatGPT is an opportunity for schools Published: 06 March 2023 Last updated: 06 June 2023 Date published: 28 February 2023 Dr Matthew Glanville, Head of Assessment Principles and Practice Source: Why ChatGPT is an opportunity for schools | The Times Those of us who work in the schools or exam sector should not be terri ed by ChatGPT and the rise of AI software – we should be excited. We should embrace it as an extraordinary opportunity. Contrary to some stark warnings, it is not the end of exams, nor even a huge threat to coursework, but it does bring into very sharp focus the impact that arti `,
|
||||
});
|
||||
expect(dbGeneration?.output).toStrictEqual({
|
||||
key: "제점이 있었죠. 그중 하나는 일제가 한국의 신용체계를 망가뜨린 채 한국을 떠났다는 겁니다. 해방전 일제는 조선의 신용체계를 거의 독점적으로 소유한 상황이었습니다. 1945년 6월 기준 일제는 조선의 본점을 둔 전체은행 5개의 불입자본 총액의 89.7%를",
|
||||
});
|
||||
|
||||
expect(dbGeneration).toBeTruthy();
|
||||
});
|
||||
|
||||
[
|
||||
{ input: "A\u0000hallo", expected: "Ahallo" },
|
||||
{ input: ["A\u0000hallo"], expected: ["Ahallo"] },
|
||||
{ input: { obj: ["A\u0000hallo"] }, expected: { obj: ["Ahallo"] } },
|
||||
].forEach(({ input, expected }) => {
|
||||
it(`cleans events with null values ${JSON.stringify(
|
||||
input,
|
||||
)} ${JSON.stringify(expected)}`, () => {
|
||||
const cleanedEvent = cleanEvent(input);
|
||||
console.log(cleanedEvent);
|
||||
expect(cleanedEvent).toStrictEqual(expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
/** @jest-environment node */
|
||||
|
||||
import { makeAPICall, pruneDatabase } from "@/src/__tests__/test-utils";
|
||||
import { prisma } from "@/src/server/db";
|
||||
|
||||
describe("/api/public/traces API Endpoint", () => {
|
||||
beforeEach(async () => await pruneDatabase());
|
||||
afterEach(async () => await pruneDatabase());
|
||||
|
||||
it("should create a session via a trace", async () => {
|
||||
await pruneDatabase();
|
||||
|
||||
await makeAPICall("POST", "/api/public/traces", {
|
||||
name: "trace-name",
|
||||
projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
|
||||
sessionId: "session-id",
|
||||
});
|
||||
|
||||
const dbSession = await prisma.traceSession.findFirst({
|
||||
where: {
|
||||
id: "session-id",
|
||||
},
|
||||
});
|
||||
|
||||
expect(dbSession).not.toBeNull();
|
||||
expect(dbSession).toMatchObject({
|
||||
id: "session-id",
|
||||
projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
|
||||
});
|
||||
});
|
||||
|
||||
it("should get session including traces", async () => {
|
||||
await pruneDatabase();
|
||||
|
||||
await makeAPICall("POST", "/api/public/traces", {
|
||||
name: "trace-name",
|
||||
id: "trace-id",
|
||||
input: { hello: "world" },
|
||||
output: "hi",
|
||||
projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
|
||||
sessionId: "session-id",
|
||||
});
|
||||
|
||||
const response = await makeAPICall(
|
||||
"GET",
|
||||
"/api/public/sessions/session-id",
|
||||
);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body).toMatchObject({
|
||||
id: "session-id",
|
||||
projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
|
||||
traces: [
|
||||
{
|
||||
id: "trace-id",
|
||||
input: { hello: "world" },
|
||||
output: "hi",
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -9,6 +9,9 @@ export const pruneDatabase = async () => {
|
||||
await prisma.score.deleteMany();
|
||||
await prisma.observation.deleteMany();
|
||||
await prisma.trace.deleteMany();
|
||||
await prisma.datasetItem.deleteMany();
|
||||
await prisma.dataset.deleteMany();
|
||||
await prisma.datasetRuns.deleteMany();
|
||||
};
|
||||
|
||||
export function createBasicAuthHeader(
|
||||
@@ -24,7 +27,7 @@ export function createBasicAuthHeader(
|
||||
export async function makeAPICall(
|
||||
method: "POST" | "GET" | "PUT" | "DELETE" | "PATCH",
|
||||
url: string,
|
||||
body: unknown,
|
||||
body?: unknown,
|
||||
) {
|
||||
const finalUrl = `http://localhost:3000/${url}`;
|
||||
const options = {
|
||||
@@ -37,7 +40,9 @@ export async function makeAPICall(
|
||||
"sk-lf-1234567890",
|
||||
),
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
// Conditionally include the body property if the method is not "GET"
|
||||
...(method !== "GET" &&
|
||||
body !== undefined && { body: JSON.stringify(body) }),
|
||||
};
|
||||
const a = await fetch(finalUrl, options);
|
||||
|
||||
|
||||
@@ -44,29 +44,48 @@ describe("/api/public/traces API Endpoint", () => {
|
||||
metadata: { key: "value" },
|
||||
release: "1.0.0",
|
||||
version: "2.0.0",
|
||||
public: true,
|
||||
});
|
||||
|
||||
const dbTrace1 = await prisma.trace.findFirst({
|
||||
where: {
|
||||
id: "trace-id",
|
||||
},
|
||||
});
|
||||
|
||||
expect(dbTrace1).not.toBeNull();
|
||||
expect(dbTrace1).toMatchObject({
|
||||
name: "trace-name",
|
||||
release: "1.0.0",
|
||||
externalId: null,
|
||||
version: "2.0.0",
|
||||
public: true,
|
||||
userId: "user-1",
|
||||
});
|
||||
|
||||
await makeAPICall("POST", "/api/public/traces", {
|
||||
id: "trace-id",
|
||||
name: "trace-name",
|
||||
userId: "user-1",
|
||||
projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
|
||||
metadata: { key: "value" },
|
||||
release: "1.0.0",
|
||||
version: "5.0.0",
|
||||
public: false,
|
||||
});
|
||||
|
||||
const dbTrace = await prisma.trace.findMany({
|
||||
const dbTrace2 = await prisma.trace.findFirst({
|
||||
where: {
|
||||
name: "trace-name",
|
||||
},
|
||||
});
|
||||
|
||||
expect(dbTrace.length).toBeGreaterThan(0);
|
||||
expect(dbTrace[0]?.name).toBe("trace-name");
|
||||
expect(dbTrace[0]?.release).toBe("1.0.0");
|
||||
expect(dbTrace[0]?.externalId).toBeNull();
|
||||
expect(dbTrace[0]?.version).toBe("5.0.0");
|
||||
expect(dbTrace[0]?.projectId).toBe("7a88fb47-b4e2-43b8-a06c-a5ce950dc53a");
|
||||
expect(dbTrace2).not.toBeNull();
|
||||
expect(dbTrace2).toMatchObject({
|
||||
name: "trace-name",
|
||||
release: "1.0.0",
|
||||
externalId: null,
|
||||
version: "5.0.0",
|
||||
public: false,
|
||||
userId: "user-1",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
import { TrashIcon } from "lucide-react";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import { Button } from "@/src/components/ui/button";
|
||||
import { api } from "@/src/utils/api";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/src/components/ui/popover";
|
||||
import { useHasAccess } from "@/src/features/rbac/utils/checkAccess";
|
||||
import { useState } from "react";
|
||||
|
||||
export function DeleteTrace({
|
||||
traceId,
|
||||
projectId,
|
||||
isTableAction,
|
||||
}: {
|
||||
traceId: string;
|
||||
projectId: string;
|
||||
isTableAction?: boolean;
|
||||
}) {
|
||||
const [isDeleted, setIsDeleted] = useState(false);
|
||||
const router = useRouter();
|
||||
const utils = api.useUtils();
|
||||
|
||||
const hasAccess = useHasAccess({ projectId, scope: "traces:delete" });
|
||||
|
||||
const mutDeleteTrace = api.traces.delete.useMutation({
|
||||
onSuccess: () => {
|
||||
setIsDeleted(true);
|
||||
if (!isTableAction) {
|
||||
void router
|
||||
.push(`/project/${projectId}/traces`)
|
||||
.then(() => utils.traces.invalidate());
|
||||
} else {
|
||||
void utils.traces.invalidate();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
if (!hasAccess) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Popover key={traceId}>
|
||||
<PopoverTrigger asChild>
|
||||
{isTableAction ? (
|
||||
<Button variant="ghost" size="xs">
|
||||
<TrashIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
) : (
|
||||
<Button variant="outline" type="button" size="sm">
|
||||
<TrashIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
</PopoverTrigger>
|
||||
<PopoverContent>
|
||||
<h2 className="text-md mb-3 font-semibold">Please confirm</h2>
|
||||
<p className="mb-3 text-sm">
|
||||
This action cannot be undone and removes all the data associated with
|
||||
this trace.
|
||||
</p>
|
||||
<div className="flex justify-end space-x-4">
|
||||
<Button
|
||||
type="button"
|
||||
variant="destructive"
|
||||
loading={mutDeleteTrace.isLoading || isDeleted}
|
||||
onClick={() =>
|
||||
void mutDeleteTrace.mutateAsync({ traceId, projectId })
|
||||
}
|
||||
>
|
||||
Delete trace
|
||||
</Button>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
@@ -1,8 +1,16 @@
|
||||
import { Badge } from "@/src/components/ui/badge";
|
||||
import {
|
||||
HoverCard,
|
||||
HoverCardContent,
|
||||
HoverCardTrigger,
|
||||
} from "@/src/components/ui/hover-card";
|
||||
import { cn } from "@/src/utils/tailwind";
|
||||
import { MessageCircle } from "lucide-react";
|
||||
|
||||
type ScoreSimplified = {
|
||||
name: string;
|
||||
value: number;
|
||||
comment?: string | null;
|
||||
};
|
||||
|
||||
export const GroupedScoreBadges = ({
|
||||
@@ -24,6 +32,30 @@ export const GroupedScoreBadges = ({
|
||||
{} as Record<string, ScoreSimplified[]>,
|
||||
);
|
||||
|
||||
const ScoresOfGroup = (props: {
|
||||
scores: ScoreSimplified[];
|
||||
className?: string;
|
||||
}) => (
|
||||
<div className={cn("text-xs", props.className)}>
|
||||
{props.scores.map((s, i) => (
|
||||
<span key={i} className="group/score ml-1 first:ml-0">
|
||||
{s.value.toFixed(2)}
|
||||
{s.comment && (
|
||||
<HoverCard>
|
||||
<HoverCardTrigger className="ml-1 inline-block cursor-pointer">
|
||||
<MessageCircle size={12} />
|
||||
</HoverCardTrigger>
|
||||
<HoverCardContent>
|
||||
<p>{s.comment}</p>
|
||||
</HoverCardContent>
|
||||
</HoverCard>
|
||||
)}
|
||||
<span className="group-last/score:hidden">,</span>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
if (variant === "headings")
|
||||
return (
|
||||
<div className="flex items-center gap-3">
|
||||
@@ -32,9 +64,7 @@ export const GroupedScoreBadges = ({
|
||||
.map(([name, scores]) => (
|
||||
<div key={name}>
|
||||
<div className="text-xs text-gray-500">{name}</div>
|
||||
<div className="text-xs">
|
||||
{scores.map((s) => s.value.toFixed(2)).join(", ")}
|
||||
</div>
|
||||
<ScoresOfGroup scores={scores} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -50,7 +80,7 @@ export const GroupedScoreBadges = ({
|
||||
key={name}
|
||||
className="break-all font-normal"
|
||||
>
|
||||
{name}: {scores.map((s) => s.value.toFixed(2)).join(", ")}
|
||||
{name}: <ScoresOfGroup scores={scores} className="ml-2" />
|
||||
</Badge>
|
||||
))}
|
||||
</>
|
||||
|
||||
@@ -5,34 +5,43 @@ import {
|
||||
} from "@/src/components/ui/hover-card";
|
||||
import { HelpCircle } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { usePostHog } from "posthog-js/react";
|
||||
|
||||
export type DocPopupProps = {
|
||||
description: React.ReactNode;
|
||||
link: string;
|
||||
href: string;
|
||||
size?: "sm" | "md" | "lg";
|
||||
};
|
||||
|
||||
export default function DocPopup({ description, link, size }: DocPopupProps) {
|
||||
let sizeClass = "w-4 h-4";
|
||||
switch (size) {
|
||||
case "sm": {
|
||||
sizeClass = "w-4 h-4";
|
||||
}
|
||||
case "md": {
|
||||
sizeClass = "w-6 h-6";
|
||||
}
|
||||
case "lg": {
|
||||
sizeClass = "w-8 h-8";
|
||||
}
|
||||
}
|
||||
export default function DocPopup({
|
||||
description,
|
||||
href,
|
||||
size = "sm",
|
||||
}: DocPopupProps) {
|
||||
const sizes = {
|
||||
sm: "w-4 h-4",
|
||||
md: "w-6 h-6",
|
||||
lg: "w-8 h-8",
|
||||
};
|
||||
const posthog = usePostHog();
|
||||
|
||||
return (
|
||||
<HoverCard openDelay={200}>
|
||||
<HoverCardTrigger className="mx-2 cursor-pointer">
|
||||
<Link href={link} rel="noopener" target="_blank">
|
||||
<div className="whitespace-nowrap text-gray-500 sm:pl-0">
|
||||
<HelpCircle className={sizeClass} />
|
||||
</div>
|
||||
<HoverCard
|
||||
openDelay={200}
|
||||
onOpenChange={(open) => {
|
||||
if (open) {
|
||||
posthog.capture("help-popup:opened", { href, description });
|
||||
}
|
||||
}}
|
||||
>
|
||||
<HoverCardTrigger className="mx-1 cursor-pointer" asChild>
|
||||
<Link
|
||||
href={href}
|
||||
rel="noopener"
|
||||
target="_blank"
|
||||
className="inline-block whitespace-nowrap text-gray-500 sm:pl-0"
|
||||
>
|
||||
<HelpCircle className={sizes[size]} />
|
||||
</Link>
|
||||
</HoverCardTrigger>
|
||||
<HoverCardContent>
|
||||
|
||||
@@ -1,12 +1,34 @@
|
||||
import { ChevronLeftIcon, ChevronRightIcon } from "@heroicons/react/20/solid";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { useSession } from "next-auth/react";
|
||||
import DocPopup from "@/src/components/layouts/doc-popup";
|
||||
|
||||
export default function Header(props: {
|
||||
title: string;
|
||||
breadcrumb?: { name: string; href?: string }[];
|
||||
live?: boolean;
|
||||
help?: { description: string; href: string };
|
||||
actionButtons?: React.ReactNode;
|
||||
}) {
|
||||
const router = useRouter();
|
||||
const session = useSession();
|
||||
|
||||
const currentPath = router.pathname;
|
||||
const projectId = router.query.projectId;
|
||||
|
||||
const project = session.data?.user?.projects.find((p) => p.id === projectId);
|
||||
const breadcrumb = [
|
||||
...(project && projectId && currentPath !== "/project/[projectId]"
|
||||
? [
|
||||
{
|
||||
name: project.name,
|
||||
href: `/project/${projectId as string}`,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(props.breadcrumb ?? []),
|
||||
];
|
||||
const backHref =
|
||||
props.breadcrumb &&
|
||||
[...props.breadcrumb.map((i) => i.href).filter(Boolean)].pop();
|
||||
@@ -28,10 +50,10 @@ export default function Header(props: {
|
||||
</Link>
|
||||
</nav>
|
||||
) : null}
|
||||
{props.breadcrumb ? (
|
||||
{breadcrumb ? (
|
||||
<nav className="hidden sm:flex" aria-label="Breadcrumb">
|
||||
<ol role="list" className="flex items-center space-x-4">
|
||||
{props.breadcrumb.map(({ name, href }, index) => (
|
||||
{breadcrumb.map(({ name, href }, index) => (
|
||||
<li key={index}>
|
||||
<div className="flex items-center">
|
||||
{index !== 0 && (
|
||||
@@ -59,12 +81,19 @@ export default function Header(props: {
|
||||
</nav>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="mt-2 flex flex-wrap items-center justify-between gap-5">
|
||||
<div className="mt-2 flex flex-wrap items-center justify-between gap-2">
|
||||
<div className="flex items-center gap-3 md:gap-5">
|
||||
<div className="min-w-0">
|
||||
<div className="flex min-w-0 flex-row">
|
||||
<h2 className="text-2xl font-bold leading-7 text-gray-900 sm:truncate sm:text-3xl sm:tracking-tight">
|
||||
{props.title}
|
||||
</h2>
|
||||
{props.help ? (
|
||||
<DocPopup
|
||||
description={props.help.description}
|
||||
href={props.help.href}
|
||||
size="sm"
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
{props.live ? (
|
||||
<div className="flex items-center gap-2 rounded-sm bg-green-100 px-3 text-green-600">
|
||||
@@ -76,8 +105,9 @@ export default function Header(props: {
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="md:flex-1" />
|
||||
{props.actionButtons ?? null}
|
||||
<div className="flex items-center gap-3">
|
||||
{props.actionButtons ?? null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -21,7 +21,8 @@ import { FeedbackButtonWrapper } from "@/src/features/feedback/component/Feedbac
|
||||
import { Button } from "@/src/components/ui/button";
|
||||
import Head from "next/head";
|
||||
import { env } from "@/src/env.mjs";
|
||||
import { LangfuseIcon, LangfuseLogo } from "@/src/components/LangfuseLogo";
|
||||
import { LangfuseLogo } from "@/src/components/LangfuseLogo";
|
||||
import { Spinner } from "@/src/components/layouts/spinner";
|
||||
|
||||
const userNavigation = [
|
||||
{
|
||||
@@ -35,13 +36,17 @@ const userNavigation = [
|
||||
|
||||
const pathsWithoutNavigation: string[] = [];
|
||||
const unauthenticatedPaths = ["/auth/sign-in", "/auth/sign-up"];
|
||||
const publishablePaths = [
|
||||
"/project/[projectId]/sessions/[sessionId]",
|
||||
"/project/[projectId]/traces/[traceId]",
|
||||
];
|
||||
|
||||
export default function Layout(props: PropsWithChildren) {
|
||||
const [sidebarOpen, setSidebarOpen] = useState(false);
|
||||
const router = useRouter();
|
||||
const session = useSession();
|
||||
const enableExperimentalFeatures =
|
||||
api.environment.enableExperimentalFeatures.useQuery().data;
|
||||
api.environment.enableExperimentalFeatures.useQuery().data ?? false;
|
||||
|
||||
const projectId = router.query.projectId as string | undefined;
|
||||
const navigation = ROUTES.filter(
|
||||
@@ -66,9 +71,7 @@ export default function Layout(props: PropsWithChildren) {
|
||||
|
||||
const currentPathName = navigation.find(({ current }) => current)?.name;
|
||||
|
||||
const projects = api.projects.all.useQuery(undefined, {
|
||||
enabled: session.status === "authenticated",
|
||||
});
|
||||
const projects = session.data?.user?.projects ?? [];
|
||||
|
||||
if (session.status === "loading") return <Spinner message="Loading" />;
|
||||
|
||||
@@ -77,6 +80,7 @@ export default function Layout(props: PropsWithChildren) {
|
||||
session.data &&
|
||||
session.data.user === null &&
|
||||
!unauthenticatedPaths.includes(router.pathname) &&
|
||||
!publishablePaths.includes(router.pathname) &&
|
||||
!router.pathname.startsWith("/public/")
|
||||
) {
|
||||
void signOut({
|
||||
@@ -88,9 +92,10 @@ export default function Layout(props: PropsWithChildren) {
|
||||
if (
|
||||
session.status === "unauthenticated" &&
|
||||
!unauthenticatedPaths.includes(router.pathname) &&
|
||||
!publishablePaths.includes(router.pathname) &&
|
||||
!router.pathname.startsWith("/public/")
|
||||
) {
|
||||
void router.push("/auth/sign-in");
|
||||
void router.replace("/auth/sign-in");
|
||||
return <Spinner message="Redirecting" />;
|
||||
}
|
||||
|
||||
@@ -98,18 +103,18 @@ export default function Layout(props: PropsWithChildren) {
|
||||
session.status === "authenticated" &&
|
||||
unauthenticatedPaths.includes(router.pathname)
|
||||
) {
|
||||
void router.push("/");
|
||||
void router.replace("/");
|
||||
return <Spinner message="Redirecting" />;
|
||||
}
|
||||
|
||||
const hideNavigation =
|
||||
session.status === "unauthenticated" ||
|
||||
projects.data?.length === 0 ||
|
||||
projects.length === 0 ||
|
||||
pathsWithoutNavigation.includes(router.pathname) ||
|
||||
router.pathname.startsWith("/public/");
|
||||
if (hideNavigation)
|
||||
return (
|
||||
<main className="h-full bg-gray-50 px-4 py-4 sm:px-6 lg:px-8">
|
||||
<main className="min-h-screen bg-gray-50 px-4 py-4 sm:px-6 lg:px-8">
|
||||
{props.children}
|
||||
</main>
|
||||
);
|
||||
@@ -219,6 +224,18 @@ export default function Layout(props: PropsWithChildren) {
|
||||
aria-hidden="true"
|
||||
/>
|
||||
{item.name}
|
||||
{item.label && (
|
||||
<span
|
||||
className={cn(
|
||||
"self-center whitespace-nowrap break-keep rounded-sm border px-1 py-0.5 text-xs",
|
||||
item.current
|
||||
? "border-indigo-600 text-indigo-600"
|
||||
: "border-gray-200 text-gray-400 group-hover:border-indigo-600 group-hover:text-indigo-600",
|
||||
)}
|
||||
>
|
||||
{item.label}
|
||||
</span>
|
||||
)}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
@@ -241,7 +258,7 @@ export default function Layout(props: PropsWithChildren) {
|
||||
<NewProjectButton size="xs" />
|
||||
</div>
|
||||
<ul role="list" className="-mx-2 mt-2 space-y-1">
|
||||
{projects.data?.map((project) => (
|
||||
{projects.map((project) => (
|
||||
<li key={project.name}>
|
||||
<Link
|
||||
href={`/project/${project.id}`}
|
||||
@@ -268,7 +285,7 @@ export default function Layout(props: PropsWithChildren) {
|
||||
{project.role === "VIEWER" ? (
|
||||
<span
|
||||
className={cn(
|
||||
"whitespace-nowrap break-keep rounded-sm border p-1 text-xs",
|
||||
"self-center whitespace-nowrap break-keep rounded-sm border px-1 py-0.5 text-xs",
|
||||
projectId === project.id
|
||||
? "border-indigo-600 text-indigo-600"
|
||||
: "border-gray-200 text-gray-400 group-hover:border-indigo-600 group-hover:text-indigo-600",
|
||||
@@ -294,10 +311,10 @@ export default function Layout(props: PropsWithChildren) {
|
||||
{/* Static sidebar for desktop */}
|
||||
<div className="hidden xl:fixed xl:inset-y-0 xl:z-50 xl:flex xl:w-72 xl:flex-col">
|
||||
{/* Sidebar component, swap this element with another sidebar if you like */}
|
||||
<div className="flex grow flex-col gap-y-5 overflow-y-auto border-r border-gray-200 bg-white px-6 pb-3 pt-7">
|
||||
<LangfuseLogo size="xl" className="mb-4" />
|
||||
<nav className="flex flex-1 flex-col">
|
||||
<ul role="list" className="flex flex-1 flex-col gap-y-4">
|
||||
<div className="flex h-screen grow flex-col gap-y-5 border-r border-gray-200 bg-white pt-7">
|
||||
<LangfuseLogo size="xl" className="mb-2 px-6" />
|
||||
<nav className="flex h-full flex-1 flex-col overflow-y-auto px-6 pb-3">
|
||||
<ul role="list" className="flex h-full flex-col gap-y-4">
|
||||
<li>
|
||||
<ul role="list" className="-mx-2 space-y-1">
|
||||
{navigation.map((item) => (
|
||||
@@ -321,6 +338,18 @@ export default function Layout(props: PropsWithChildren) {
|
||||
aria-hidden="true"
|
||||
/>
|
||||
{item.name}
|
||||
{item.label && (
|
||||
<span
|
||||
className={cn(
|
||||
"self-center whitespace-nowrap break-keep rounded-sm border px-1 py-0.5 text-xs",
|
||||
item.current
|
||||
? "border-indigo-600 text-indigo-600"
|
||||
: "border-gray-200 text-gray-400 group-hover:border-indigo-600 group-hover:text-indigo-600",
|
||||
)}
|
||||
>
|
||||
{item.label}
|
||||
</span>
|
||||
)}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
@@ -344,7 +373,7 @@ export default function Layout(props: PropsWithChildren) {
|
||||
<NewProjectButton size="xs" />
|
||||
</div>
|
||||
<ul role="list" className="-mx-2 mt-2 space-y-1">
|
||||
{projects.data?.map((project, index) => (
|
||||
{projects.map((project, index) => (
|
||||
<li key={project.name}>
|
||||
<Link
|
||||
href={`/project/${project.id}`}
|
||||
@@ -374,7 +403,7 @@ export default function Layout(props: PropsWithChildren) {
|
||||
{project.role === "VIEWER" ? (
|
||||
<span
|
||||
className={cn(
|
||||
"whitespace-nowrap break-keep rounded-sm border p-1 text-xs",
|
||||
"self-center whitespace-nowrap break-keep rounded-sm border px-1 py-0.5 text-xs",
|
||||
projectId === project.id
|
||||
? "border-indigo-600 text-indigo-600"
|
||||
: "border-gray-200 text-gray-400 group-hover:border-indigo-600 group-hover:text-indigo-600",
|
||||
@@ -389,7 +418,7 @@ export default function Layout(props: PropsWithChildren) {
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
<li className="-mx-6 ">
|
||||
<li className="-mx-6">
|
||||
<Menu as="div" className="relative">
|
||||
<Menu.Button className="flex w-full items-center gap-x-4 p-1.5 px-6 py-3 text-sm font-semibold leading-6 text-gray-900 hover:bg-gray-50">
|
||||
<span className="sr-only">Open user menu</span>
|
||||
@@ -507,6 +536,8 @@ export default function Layout(props: PropsWithChildren) {
|
||||
<div className="xl:pl-72">
|
||||
{env.NEXT_PUBLIC_DEMO_PROJECT_ID &&
|
||||
projectId === env.NEXT_PUBLIC_DEMO_PROJECT_ID &&
|
||||
(env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION === "STAGING" ||
|
||||
env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION === "EU") &&
|
||||
!session.data?.user?.email?.endsWith("@langfuse.com") ? (
|
||||
<div className="flex w-full items-center border-b border-yellow-500 bg-yellow-100 px-4 py-2 xl:sticky xl:top-0 xl:z-40">
|
||||
<div className="flex flex-1 flex-wrap gap-1">
|
||||
@@ -516,9 +547,19 @@ export default function Layout(props: PropsWithChildren) {
|
||||
</div>
|
||||
<div>Live data from the Langfuse Q&A Chatbot.</div>
|
||||
</div>
|
||||
<Button size="sm" variant="ghost" asChild className="ml-2">
|
||||
<Link href="https://langfuse.com/docs/demo" target="_blank">
|
||||
Learn more ↗
|
||||
|
||||
<Button size="sm" asChild className="ml-2">
|
||||
<Link
|
||||
href={
|
||||
env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION === "EU"
|
||||
? "https://langfuse.com/docs/qa-chatbot"
|
||||
: "https://docs-staging.langfuse.com/docs/qa-chatbot"
|
||||
}
|
||||
target="_blank"
|
||||
>
|
||||
{env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION === "EU"
|
||||
? "Q&A Chatbot ↗"
|
||||
: "Q&A Chatbot (staging) ↗"}
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
@@ -531,16 +572,3 @@ export default function Layout(props: PropsWithChildren) {
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function Spinner(props: { message: string }) {
|
||||
return (
|
||||
<div className="flex min-h-full flex-1 flex-col justify-center py-12 sm:px-6 lg:px-8">
|
||||
<div className="sm:mx-auto sm:w-full sm:max-w-md">
|
||||
<LangfuseIcon className="mx-auto motion-safe:animate-spin" size={42} />
|
||||
<h2 className="mt-5 text-center text-2xl font-bold leading-9 tracking-tight text-gray-900">
|
||||
{props.message} ...
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
Settings,
|
||||
TextSelect,
|
||||
UsersIcon,
|
||||
Route,
|
||||
} from "lucide-react";
|
||||
|
||||
export const ROUTES: Array<{
|
||||
@@ -16,12 +17,19 @@ export const ROUTES: Array<{
|
||||
pathname: string;
|
||||
icon: LucideIcon;
|
||||
featureFlag?: Flag;
|
||||
label?: string;
|
||||
}> = [
|
||||
{
|
||||
name: "Dashboard",
|
||||
pathname: `/project/[projectId]`,
|
||||
icon: LayoutDashboard,
|
||||
},
|
||||
{
|
||||
name: "Sessions",
|
||||
pathname: `/project/[projectId]/sessions`,
|
||||
icon: Route,
|
||||
label: "beta",
|
||||
},
|
||||
{
|
||||
name: "Traces",
|
||||
pathname: `/project/[projectId]/traces`,
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import { LangfuseIcon } from "@/src/components/LangfuseLogo";
|
||||
|
||||
export function Spinner(props: { message: string }) {
|
||||
return (
|
||||
<div className="flex min-h-full flex-1 flex-col justify-center py-12 sm:px-6 lg:px-8">
|
||||
<div className="sm:mx-auto sm:w-full sm:max-w-md">
|
||||
<LangfuseIcon className="mx-auto motion-safe:animate-spin" size={42} />
|
||||
<h2 className="mt-5 text-center text-2xl font-bold leading-9 tracking-tight text-gray-900">
|
||||
{props.message} ...
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export const LevelColors = {
|
||||
DEFAULT: { text: "", bg: "" },
|
||||
DEBUG: { text: "text-gray-500", bg: "bg-gray-50" },
|
||||
WARNING: { text: "text-yellow-800", bg: "bg-yellow-50" },
|
||||
ERROR: { text: "text-red-800", bg: "bg-red-50" },
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
import { Button } from "@/src/components/ui/button";
|
||||
import { AlertCircle } from "lucide-react";
|
||||
import { useSession } from "next-auth/react";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
export const NoAccessError = () => {
|
||||
const session = useSession();
|
||||
const router = useRouter();
|
||||
|
||||
return (
|
||||
<div className="flex h-screen flex-col items-center justify-center">
|
||||
<AlertCircle className="mb-4 h-12 w-12 text-red-500" />
|
||||
<h1 className="mb-4 text-xl font-bold">Error</h1>
|
||||
<p className="mb-8 text-center">
|
||||
You do not have access to this resource.
|
||||
</p>
|
||||
{session.status === "unauthenticated" ? (
|
||||
<Button onClick={() => void router.push("/")}>Sign In</Button>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,120 @@
|
||||
import { Switch } from "@/src/components/ui/switch";
|
||||
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: {
|
||||
traceId: string;
|
||||
projectId: string;
|
||||
isPublic: boolean;
|
||||
}) => {
|
||||
const posthog = usePostHog();
|
||||
const hasAccess = useHasAccess({
|
||||
projectId: props.projectId,
|
||||
scope: "objects:publish",
|
||||
});
|
||||
const utils = api.useUtils();
|
||||
const mut = api.traces.publish.useMutation({
|
||||
onSuccess: () => utils.traces.invalidate(),
|
||||
});
|
||||
|
||||
return (
|
||||
<Base
|
||||
id={props.traceId}
|
||||
isPublic={props.isPublic}
|
||||
onChange={(val) => {
|
||||
mut.mutate({
|
||||
projectId: props.projectId,
|
||||
traceId: props.traceId,
|
||||
public: val,
|
||||
});
|
||||
posthog.capture("trace_detail:publish_trace_button_click");
|
||||
}}
|
||||
isLoading={mut.isLoading}
|
||||
disabled={!hasAccess}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const PublishSessionSwitch = (props: {
|
||||
sessionId: string;
|
||||
projectId: string;
|
||||
isPublic: boolean;
|
||||
}) => {
|
||||
const posthog = usePostHog();
|
||||
const hasAccess = useHasAccess({
|
||||
projectId: props.projectId,
|
||||
scope: "objects:publish",
|
||||
});
|
||||
const utils = api.useUtils();
|
||||
const mut = api.sessions.publish.useMutation({
|
||||
onSuccess: () => utils.sessions.invalidate(),
|
||||
});
|
||||
|
||||
return (
|
||||
<Base
|
||||
id={props.sessionId}
|
||||
isPublic={props.isPublic}
|
||||
onChange={(val) => {
|
||||
mut.mutate({
|
||||
projectId: props.projectId,
|
||||
sessionId: props.sessionId,
|
||||
public: val,
|
||||
});
|
||||
posthog.capture("session_detail:publish_session_button_click");
|
||||
}}
|
||||
isLoading={mut.isLoading}
|
||||
disabled={!hasAccess}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const Base = (props: {
|
||||
id: string;
|
||||
onChange: (value: boolean) => void;
|
||||
isLoading: boolean;
|
||||
isPublic: boolean;
|
||||
disabled?: boolean;
|
||||
}) => {
|
||||
const [isCopied, setIsCopied] = useState(false);
|
||||
|
||||
const copyUrl = () => {
|
||||
setIsCopied(true);
|
||||
void navigator.clipboard.writeText(window.location.href);
|
||||
setTimeout(() => setIsCopied(false), 1500);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="text-sm font-semibold">
|
||||
{props.isLoading ? (
|
||||
"Loading.."
|
||||
) : props.isPublic ? (
|
||||
<div
|
||||
className="flex cursor-pointer items-center gap-1 text-green-800"
|
||||
onClick={() => copyUrl()}
|
||||
>
|
||||
{isCopied ? "Link copied ..." : "Public"}
|
||||
<Link size={16} />
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center gap-1">
|
||||
Private
|
||||
<LockIcon size={16} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<Switch
|
||||
id="publish-trace"
|
||||
checked={props.isPublic}
|
||||
onCheckedChange={() => {
|
||||
if (props.isLoading) return;
|
||||
void props.onChange(!props.isPublic);
|
||||
}}
|
||||
disabled={props.disabled}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,119 @@
|
||||
import { GroupedScoreBadges } from "@/src/components/grouped-score-badge";
|
||||
import Header from "@/src/components/layouts/header";
|
||||
import { NoAccessError } from "@/src/components/no-access";
|
||||
import { PublishSessionSwitch } from "@/src/components/publish-object-switch";
|
||||
import { StarSessionToggle } from "@/src/components/star-toggle";
|
||||
import { IOPreview } from "@/src/components/trace/IOPreview";
|
||||
import { Badge } from "@/src/components/ui/badge";
|
||||
import { Card } from "@/src/components/ui/card";
|
||||
import { ManualScoreButton } from "@/src/features/manual-scoring/components/ManualScoreButton";
|
||||
import { DetailPageNav } from "@/src/features/navigate-detail-pages/DetailPageNav";
|
||||
import { api } from "@/src/utils/api";
|
||||
import Link from "next/link";
|
||||
|
||||
export const SessionPage: React.FC<{
|
||||
sessionId: string;
|
||||
projectId: string;
|
||||
}> = ({ sessionId, projectId }) => {
|
||||
const session = api.sessions.byId.useQuery(
|
||||
{
|
||||
sessionId,
|
||||
projectId: projectId,
|
||||
},
|
||||
{
|
||||
retry(failureCount, error) {
|
||||
if (error.data?.code === "UNAUTHORIZED") return false;
|
||||
return failureCount < 3;
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (session.error?.data?.code === "UNAUTHORIZED") return <NoAccessError />;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col overflow-hidden xl:container">
|
||||
<Header
|
||||
title="Session"
|
||||
breadcrumb={[
|
||||
{
|
||||
name: "Sessions",
|
||||
href: `/project/${projectId}/sessions`,
|
||||
},
|
||||
{ name: sessionId },
|
||||
]}
|
||||
actionButtons={[
|
||||
<StarSessionToggle
|
||||
key="star"
|
||||
projectId={projectId}
|
||||
sessionId={sessionId}
|
||||
value={session.data?.bookmarked ?? false}
|
||||
/>,
|
||||
<PublishSessionSwitch
|
||||
projectId={projectId}
|
||||
sessionId={sessionId}
|
||||
isPublic={session.data?.public ?? false}
|
||||
key="publish"
|
||||
/>,
|
||||
<DetailPageNav
|
||||
key="nav"
|
||||
currentId={sessionId}
|
||||
path={(id) => `/project/${projectId}/sessions/${id}`}
|
||||
listKey="sessions"
|
||||
/>,
|
||||
]}
|
||||
/>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{session.data?.users.map((userId) => (
|
||||
<Link key={userId} href={`/project/${projectId}/users/${userId}`}>
|
||||
<Badge>User ID: {userId}</Badge>
|
||||
</Link>
|
||||
))}
|
||||
<Badge variant="outline">Traces: {session.data?.traces.length}</Badge>
|
||||
</div>
|
||||
<div className="mt-5 flex flex-col gap-2 border-t pt-5">
|
||||
{session.data?.traces.map((trace) => (
|
||||
<Card
|
||||
className="border-border-gray-150 group grid gap-3 p-2 shadow-none hover:border-gray-300 md:grid-cols-3"
|
||||
key={trace.id}
|
||||
>
|
||||
<div className="col-span-2 flex flex-col gap-2 p-0">
|
||||
{trace.input || trace.output ? (
|
||||
<IOPreview
|
||||
key={trace.id}
|
||||
input={trace.input}
|
||||
output={trace.output}
|
||||
hideIfNull
|
||||
/>
|
||||
) : (
|
||||
<div className="p-2 text-xs text-gray-500">
|
||||
This trace has no input or output.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="-mt-1 p-1 opacity-50 transition-opacity group-hover:opacity-100">
|
||||
<Link
|
||||
href={`/project/${projectId}/traces/${trace.id}`}
|
||||
className="text-xs hover:underline"
|
||||
>
|
||||
Trace: {trace.name} ({trace.id}) ↗
|
||||
</Link>
|
||||
<div className="text-xs text-gray-500">
|
||||
{trace.timestamp.toLocaleString()}
|
||||
</div>
|
||||
<div className="mb-1 mt-2 text-xs text-gray-500">Scores</div>
|
||||
<div className="flex flex-wrap content-start items-start gap-1">
|
||||
<GroupedScoreBadges scores={trace.scores} />
|
||||
</div>
|
||||
<ManualScoreButton
|
||||
projectId={projectId}
|
||||
traceId={trace.id}
|
||||
scores={trace.scores}
|
||||
variant="badge"
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,120 @@
|
||||
import { StarIcon } from "lucide-react";
|
||||
|
||||
import { Button } from "@/src/components/ui/button";
|
||||
import { useEffect, useState } from "react";
|
||||
import { api } from "@/src/utils/api";
|
||||
import { useHasAccess } from "@/src/features/rbac/utils/checkAccess";
|
||||
|
||||
export function StarToggle({
|
||||
value,
|
||||
disabled = false,
|
||||
onClick,
|
||||
size = "sm",
|
||||
}: {
|
||||
value: boolean;
|
||||
disabled?: boolean;
|
||||
onClick: (value: boolean) => Promise<unknown>;
|
||||
size?: "sm" | "xs";
|
||||
}) {
|
||||
const [cachedValue, setCachedValue] = useState<boolean | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const optimisticValue = cachedValue ?? value;
|
||||
|
||||
useEffect(() => {
|
||||
setCachedValue(null);
|
||||
}, [value]);
|
||||
|
||||
const handleBookmarkClick = async () => {
|
||||
if (disabled) return;
|
||||
setLoading(true);
|
||||
setCachedValue(!optimisticValue);
|
||||
await onClick(!optimisticValue);
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size={size}
|
||||
onClick={() => void handleBookmarkClick()}
|
||||
disabled={disabled}
|
||||
loading={loading}
|
||||
>
|
||||
<StarIcon
|
||||
className={`h-4 w-4 ${
|
||||
optimisticValue ? "fill-current text-yellow-500" : "text-gray-500"
|
||||
}`}
|
||||
/>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
export function StarTraceToggle({
|
||||
projectId,
|
||||
traceId,
|
||||
value,
|
||||
size = "sm",
|
||||
}: {
|
||||
projectId: string;
|
||||
traceId: string;
|
||||
value: boolean;
|
||||
size?: "sm" | "xs";
|
||||
}) {
|
||||
const utils = api.useUtils();
|
||||
const hasAccess = useHasAccess({ projectId, scope: "objects:bookmark" });
|
||||
const mutBookmarkTrace = api.traces.bookmark.useMutation({
|
||||
onSuccess: () => {
|
||||
void utils.traces.invalidate();
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<StarToggle
|
||||
value={value}
|
||||
size={size}
|
||||
disabled={!hasAccess}
|
||||
onClick={(value) =>
|
||||
mutBookmarkTrace.mutateAsync({
|
||||
projectId,
|
||||
traceId,
|
||||
bookmarked: value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function StarSessionToggle({
|
||||
projectId,
|
||||
sessionId,
|
||||
value,
|
||||
size = "sm",
|
||||
}: {
|
||||
projectId: string;
|
||||
sessionId: string;
|
||||
value: boolean;
|
||||
size?: "sm" | "xs";
|
||||
}) {
|
||||
const utils = api.useUtils();
|
||||
const hasAccess = useHasAccess({ projectId, scope: "objects:bookmark" });
|
||||
const mutBookmarkSession = api.sessions.bookmark.useMutation({
|
||||
onSuccess: () => {
|
||||
void utils.sessions.invalidate();
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<StarToggle
|
||||
value={value}
|
||||
size={size}
|
||||
disabled={!hasAccess}
|
||||
onClick={(value) =>
|
||||
mutBookmarkSession.mutateAsync({
|
||||
projectId,
|
||||
sessionId,
|
||||
bookmarked: value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
import React, {
|
||||
useCallback,
|
||||
type Dispatch,
|
||||
type SetStateAction,
|
||||
useState,
|
||||
} from "react";
|
||||
import { Button } from "@/src/components/ui/button";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuCheckboxItem,
|
||||
} from "@/src/components/ui/dropdown-menu";
|
||||
import { type VisibilityState } from "@tanstack/react-table";
|
||||
import { ChevronDownIcon } from "lucide-react";
|
||||
import { type LangfuseColumnDef } from "@/src/components/table/types";
|
||||
|
||||
interface DataTableColumnVisibilityFilterProps<TData, TValue> {
|
||||
columns: LangfuseColumnDef<TData, TValue>[];
|
||||
columnVisibility: VisibilityState;
|
||||
setColumnVisibility: Dispatch<SetStateAction<VisibilityState>>;
|
||||
}
|
||||
|
||||
export function DataTableColumnVisibilityFilter<TData, TValue>({
|
||||
columns,
|
||||
columnVisibility,
|
||||
setColumnVisibility,
|
||||
}: DataTableColumnVisibilityFilterProps<TData, TValue>) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const toggleColumn = useCallback(
|
||||
(columnId: string) => {
|
||||
setColumnVisibility((old) => ({
|
||||
...old,
|
||||
[columnId]: !old[columnId],
|
||||
}));
|
||||
},
|
||||
[setColumnVisibility],
|
||||
);
|
||||
|
||||
return (
|
||||
<DropdownMenu open={isOpen}>
|
||||
<DropdownMenuTrigger
|
||||
onClick={() => {
|
||||
setIsOpen(!isOpen);
|
||||
}}
|
||||
className="select-none"
|
||||
asChild
|
||||
>
|
||||
<Button variant="outline" className="ml-auto">
|
||||
Columns
|
||||
<ChevronDownIcon className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
align="end"
|
||||
onPointerDownOutside={() => setIsOpen(false)}
|
||||
>
|
||||
{columns.map(
|
||||
(column, index) =>
|
||||
"accessorKey" in column &&
|
||||
column.enableHiding && (
|
||||
<DropdownMenuCheckboxItem
|
||||
key={index}
|
||||
className="capitalize"
|
||||
checked={columnVisibility[column.accessorKey]}
|
||||
onCheckedChange={() =>
|
||||
toggleColumn(column.accessorKey.toString())
|
||||
}
|
||||
>
|
||||
{column.header?.toString() ?? column.accessorKey.toString()}
|
||||
</DropdownMenuCheckboxItem>
|
||||
),
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
}
|
||||
@@ -1,9 +1,12 @@
|
||||
import { Button } from "@/src/components/ui/button";
|
||||
import React, { type Dispatch, type SetStateAction, useState } from "react";
|
||||
import { Input } from "@/src/components/ui/input";
|
||||
import { DataTableColumnVisibilityFilter } from "@/src/components/table/data-table-column-visibility-filter";
|
||||
import { type FilterState } from "@/src/features/filters/types";
|
||||
import { FilterBuilder } from "@/src/features/filters/components/filter-builder";
|
||||
import { type ColumnDefinition } from "@/src/server/api/interfaces/tableDefinition";
|
||||
import { type VisibilityState } from "@tanstack/react-table";
|
||||
import { type LangfuseColumnDef } from "@/src/components/table/types";
|
||||
|
||||
interface SearchConfig {
|
||||
placeholder: string;
|
||||
@@ -11,21 +14,27 @@ interface SearchConfig {
|
||||
currentQuery?: string;
|
||||
}
|
||||
|
||||
interface DataTableToolbarProps {
|
||||
interface DataTableToolbarProps<TData, TValue> {
|
||||
columns: LangfuseColumnDef<TData, TValue>[];
|
||||
filterColumnDefinition: ColumnDefinition[];
|
||||
searchConfig?: SearchConfig;
|
||||
actionButtons?: React.ReactNode;
|
||||
filterState: FilterState;
|
||||
setFilterState: Dispatch<SetStateAction<FilterState>>;
|
||||
columnVisibility?: VisibilityState;
|
||||
setColumnVisibility?: Dispatch<SetStateAction<VisibilityState>>;
|
||||
}
|
||||
|
||||
export function DataTableToolbar({
|
||||
export function DataTableToolbar<TData, TValue>({
|
||||
columns,
|
||||
filterColumnDefinition,
|
||||
searchConfig,
|
||||
actionButtons,
|
||||
filterState,
|
||||
setFilterState,
|
||||
}: DataTableToolbarProps) {
|
||||
columnVisibility,
|
||||
setColumnVisibility,
|
||||
}: DataTableToolbarProps<TData, TValue>) {
|
||||
const [searchString, setSearchString] = useState(
|
||||
searchConfig?.currentQuery ?? "",
|
||||
);
|
||||
@@ -33,33 +42,37 @@ export function DataTableToolbar({
|
||||
return (
|
||||
<div className="my-2 flex max-w-full items-center justify-between overflow-x-auto">
|
||||
<div className="flex flex-1 items-center space-x-2">
|
||||
{searchConfig ? (
|
||||
{searchConfig && (
|
||||
<div className="flex max-w-md items-center space-x-2">
|
||||
<Input
|
||||
autoFocus
|
||||
placeholder={searchConfig.placeholder}
|
||||
value={searchString}
|
||||
onChange={(event) => setSearchString(event.currentTarget.value)}
|
||||
className="h-10 w-[200px] lg:w-[350px]"
|
||||
onChange={(event) => {
|
||||
setSearchString(event.currentTarget.value);
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
variant="outline"
|
||||
type="submit"
|
||||
onClick={() => searchConfig.updateQuery(searchString)}
|
||||
>
|
||||
Search
|
||||
</Button>
|
||||
</div>
|
||||
) : undefined}
|
||||
)}
|
||||
<FilterBuilder
|
||||
columns={filterColumnDefinition}
|
||||
filterState={filterState}
|
||||
onChange={setFilterState}
|
||||
/>
|
||||
<div className="flex-1" />
|
||||
{actionButtons ? actionButtons : null}
|
||||
{!!columnVisibility && !!setColumnVisibility && (
|
||||
<DataTableColumnVisibilityFilter
|
||||
columns={columns}
|
||||
columnVisibility={columnVisibility}
|
||||
setColumnVisibility={setColumnVisibility}
|
||||
/>
|
||||
)}
|
||||
{actionButtons}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
type ColumnDef,
|
||||
flexRender,
|
||||
getCoreRowModel,
|
||||
useReactTable,
|
||||
@@ -9,6 +8,7 @@ import {
|
||||
getFilteredRowModel,
|
||||
type OnChangeFn,
|
||||
type PaginationState,
|
||||
type VisibilityState,
|
||||
} from "@tanstack/react-table";
|
||||
import {
|
||||
Table,
|
||||
@@ -20,15 +20,20 @@ import {
|
||||
} from "@/src/components/ui/table";
|
||||
import { useState } from "react";
|
||||
import { DataTablePagination } from "@/src/components/table/data-table-pagination";
|
||||
import { type LangfuseColumnDef } from "@/src/components/table/types";
|
||||
import DocPopup from "@/src/components/layouts/doc-popup";
|
||||
|
||||
interface DataTableProps<TData, TValue> {
|
||||
columns: ColumnDef<TData, TValue>[];
|
||||
columns: LangfuseColumnDef<TData, TValue>[];
|
||||
data: AsyncTableData<TData[]>;
|
||||
pagination?: {
|
||||
pageCount: number;
|
||||
onChange: OnChangeFn<PaginationState>;
|
||||
state: PaginationState;
|
||||
};
|
||||
columnVisibility?: VisibilityState;
|
||||
onColumnVisibilityChange?: OnChangeFn<VisibilityState>;
|
||||
help?: { description: string; href: string };
|
||||
}
|
||||
|
||||
export interface AsyncTableData<T> {
|
||||
@@ -42,9 +47,11 @@ export function DataTable<TData, TValue>({
|
||||
columns,
|
||||
data,
|
||||
pagination,
|
||||
columnVisibility,
|
||||
onColumnVisibilityChange,
|
||||
help,
|
||||
}: DataTableProps<TData, TValue>) {
|
||||
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
|
||||
|
||||
const table = useReactTable({
|
||||
data: data.data ?? [],
|
||||
columns,
|
||||
@@ -54,9 +61,11 @@ export function DataTable<TData, TValue>({
|
||||
manualPagination: pagination !== undefined,
|
||||
pageCount: pagination?.pageCount ?? 0,
|
||||
onPaginationChange: pagination?.onChange,
|
||||
onColumnVisibilityChange: onColumnVisibilityChange,
|
||||
state: {
|
||||
columnFilters,
|
||||
pagination: pagination?.state,
|
||||
columnVisibility,
|
||||
},
|
||||
manualFiltering: true,
|
||||
});
|
||||
@@ -70,7 +79,7 @@ export function DataTable<TData, TValue>({
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header) => {
|
||||
return (
|
||||
return header.column.getIsVisible() ? (
|
||||
<TableHead
|
||||
key={header.id}
|
||||
className="whitespace-nowrap p-2"
|
||||
@@ -82,7 +91,7 @@ export function DataTable<TData, TValue>({
|
||||
header.getContext(),
|
||||
)}
|
||||
</TableHead>
|
||||
);
|
||||
) : null;
|
||||
})}
|
||||
</TableRow>
|
||||
))}
|
||||
@@ -122,7 +131,16 @@ export function DataTable<TData, TValue>({
|
||||
colSpan={columns.length}
|
||||
className="h-24 text-center"
|
||||
>
|
||||
No results.
|
||||
<div>
|
||||
No results.{" "}
|
||||
{help && (
|
||||
<DocPopup
|
||||
description={help.description}
|
||||
href={help.href}
|
||||
size="sm"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import { type RowData, type ColumnDef } from "@tanstack/react-table";
|
||||
import { type LucideIcon } from "lucide-react";
|
||||
|
||||
export type TableRowOptions = {
|
||||
columnId: string;
|
||||
options: { label: string; value: number; icon?: LucideIcon }[];
|
||||
};
|
||||
|
||||
export type LangfuseColumnDef<
|
||||
TData extends RowData,
|
||||
TValue = unknown,
|
||||
> = ColumnDef<TData, TValue> & { defaultHidden?: boolean };
|
||||
|
||||
@@ -0,0 +1,396 @@
|
||||
import { api, directApi } from "@/src/utils/api";
|
||||
import { DataTable } from "@/src/components/table/data-table";
|
||||
import TableLink from "@/src/components/table/table-link";
|
||||
import { DataTableToolbar } from "@/src/components/table/data-table-toolbar";
|
||||
import { useState } from "react";
|
||||
import { TokenUsageBadge } from "@/src/components/token-usage-badge";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/src/components/ui/dropdown-menu";
|
||||
import { Button } from "@/src/components/ui/button";
|
||||
import { ChevronDownIcon, Loader } from "lucide-react";
|
||||
import { type ExportFileFormats } from "@/src/server/api/routers/generations";
|
||||
import { usePostHog } from "posthog-js/react";
|
||||
import {
|
||||
NumberParam,
|
||||
StringParam,
|
||||
useQueryParam,
|
||||
useQueryParams,
|
||||
withDefault,
|
||||
} from "use-query-params";
|
||||
import { useQueryFilterState } from "@/src/features/filters/hooks/useFilterState";
|
||||
import { observationsTableColsWithOptions } from "@/src/server/api/definitions/observationsTable";
|
||||
import { formatInterval, utcDateOffsetByDays } from "@/src/utils/dates";
|
||||
import useColumnVisibility from "@/src/features/column-visibility/hooks/useColumnVisibility";
|
||||
import { JSONView } from "@/src/components/ui/code";
|
||||
import { type LangfuseColumnDef } from "@/src/components/table/types";
|
||||
import { type ObservationLevel } from "@prisma/client";
|
||||
import { cn } from "@/src/utils/tailwind";
|
||||
import { LevelColors } from "@/src/components/level-colors";
|
||||
|
||||
export type GenerationsTableRow = {
|
||||
id: string;
|
||||
traceId: string;
|
||||
startTime: string;
|
||||
level?: ObservationLevel;
|
||||
statusMessage?: string;
|
||||
endTime?: string;
|
||||
latency?: number;
|
||||
name?: string;
|
||||
model?: string;
|
||||
input?: unknown;
|
||||
output?: unknown;
|
||||
traceName?: string;
|
||||
metadata?: string;
|
||||
usage: {
|
||||
promptTokens: number;
|
||||
completionTokens: number;
|
||||
totalTokens: number;
|
||||
};
|
||||
};
|
||||
|
||||
const exportOptions: Record<
|
||||
ExportFileFormats,
|
||||
{
|
||||
label: string;
|
||||
extension: string;
|
||||
fileType: string;
|
||||
}
|
||||
> = {
|
||||
CSV: { label: "CSV", extension: "csv", fileType: "text/csv" },
|
||||
JSON: { label: "JSON", extension: "json", fileType: "application/json" },
|
||||
"OPENAI-JSONL": {
|
||||
label: "OpenAI JSONL (fine-tuning)",
|
||||
extension: "jsonl",
|
||||
fileType: "application/json",
|
||||
},
|
||||
} as const;
|
||||
|
||||
export type GenerationsTableProps = {
|
||||
projectId: string;
|
||||
};
|
||||
|
||||
export default function GenerationsTable({ projectId }: GenerationsTableProps) {
|
||||
const posthog = usePostHog();
|
||||
const [isExporting, setIsExporting] = useState(false);
|
||||
const [searchQuery, setSearchQuery] = useQueryParam(
|
||||
"search",
|
||||
withDefault(StringParam, null),
|
||||
);
|
||||
|
||||
const [paginationState, setPaginationState] = useQueryParams({
|
||||
pageIndex: withDefault(NumberParam, 0),
|
||||
pageSize: withDefault(NumberParam, 50),
|
||||
});
|
||||
|
||||
const [filterState, setFilterState] = useQueryFilterState([
|
||||
{
|
||||
column: "start_time",
|
||||
type: "datetime",
|
||||
operator: ">",
|
||||
value: utcDateOffsetByDays(-14),
|
||||
},
|
||||
]);
|
||||
|
||||
const generations = api.generations.all.useQuery({
|
||||
page: paginationState.pageIndex,
|
||||
limit: paginationState.pageSize,
|
||||
projectId,
|
||||
filter: filterState,
|
||||
searchQuery,
|
||||
});
|
||||
const totalCount = generations.data?.slice(1)[0]?.totalCount ?? 0;
|
||||
|
||||
const filterOptions = api.generations.filterOptions.useQuery({
|
||||
projectId,
|
||||
});
|
||||
|
||||
const handleExport = async (fileFormat: ExportFileFormats) => {
|
||||
if (isExporting) return;
|
||||
|
||||
setIsExporting(true);
|
||||
posthog.capture("generations:export", { file_format: fileFormat });
|
||||
const fileData = await directApi.generations.export.query({
|
||||
projectId,
|
||||
fileFormat,
|
||||
filter: filterState,
|
||||
searchQuery,
|
||||
});
|
||||
|
||||
if (fileData) {
|
||||
const file = new File(
|
||||
[fileData],
|
||||
`generations.${exportOptions[fileFormat].extension}`,
|
||||
{
|
||||
type: exportOptions[fileFormat].fileType,
|
||||
},
|
||||
);
|
||||
|
||||
// create url from file
|
||||
const url = URL.createObjectURL(file);
|
||||
|
||||
// Use a dynamically created anchor element to trigger the download
|
||||
const a = document.createElement("a");
|
||||
document.body.appendChild(a);
|
||||
a.href = url;
|
||||
a.download = `generations.${exportOptions[fileFormat].extension}`; // name of the downloaded file
|
||||
a.click();
|
||||
a.remove();
|
||||
|
||||
// Revoke the blob URL after using it
|
||||
setTimeout(() => URL.revokeObjectURL(url), 100);
|
||||
}
|
||||
setIsExporting(false);
|
||||
};
|
||||
|
||||
const columns: LangfuseColumnDef<GenerationsTableRow>[] = [
|
||||
{
|
||||
accessorKey: "id",
|
||||
header: "ID",
|
||||
cell: ({ row }) => {
|
||||
const observationId = row.getValue("id");
|
||||
const traceId = row.getValue("traceId");
|
||||
return typeof observationId === "string" &&
|
||||
typeof traceId === "string" ? (
|
||||
<TableLink
|
||||
path={`/project/${projectId}/traces/${traceId}?observation=${observationId}`}
|
||||
value={observationId}
|
||||
/>
|
||||
) : null;
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "name",
|
||||
header: "name",
|
||||
},
|
||||
{
|
||||
accessorKey: "traceId",
|
||||
header: "Trace ID",
|
||||
cell: ({ row }) => {
|
||||
const value = row.getValue("traceId");
|
||||
return typeof value === "string" ? (
|
||||
<TableLink
|
||||
path={`/project/${projectId}/traces/${value}`}
|
||||
value={value}
|
||||
/>
|
||||
) : undefined;
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "traceName",
|
||||
header: "Trace Name",
|
||||
enableHiding: true,
|
||||
},
|
||||
{
|
||||
accessorKey: "startTime",
|
||||
header: "Start Time",
|
||||
enableHiding: true,
|
||||
},
|
||||
{
|
||||
accessorKey: "latency",
|
||||
header: "Latency",
|
||||
cell: ({ row }) => {
|
||||
const value: number | undefined = row.getValue("latency");
|
||||
return value !== undefined ? (
|
||||
<span>{formatInterval(value)}</span>
|
||||
) : undefined;
|
||||
},
|
||||
enableHiding: true,
|
||||
},
|
||||
{
|
||||
accessorKey: "level",
|
||||
header: "Level",
|
||||
enableHiding: true,
|
||||
cell({ row }) {
|
||||
const value: ObservationLevel | undefined = row.getValue("level");
|
||||
return value ? (
|
||||
<span
|
||||
className={cn(
|
||||
"rounded-sm p-0.5 text-xs",
|
||||
LevelColors[value].bg,
|
||||
LevelColors[value].text,
|
||||
)}
|
||||
>
|
||||
{value}
|
||||
</span>
|
||||
) : undefined;
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "statusMessage",
|
||||
header: "Status Message",
|
||||
enableHiding: true,
|
||||
defaultHidden: true,
|
||||
},
|
||||
{
|
||||
accessorKey: "model",
|
||||
header: "Model",
|
||||
enableHiding: true,
|
||||
},
|
||||
{
|
||||
accessorKey: "usage",
|
||||
header: "Usage",
|
||||
cell: ({ row }) => {
|
||||
const value: {
|
||||
promptTokens: number;
|
||||
completionTokens: number;
|
||||
totalTokens: number;
|
||||
} = row.getValue("usage");
|
||||
return (
|
||||
<TokenUsageBadge
|
||||
promptTokens={value.promptTokens}
|
||||
completionTokens={value.completionTokens}
|
||||
totalTokens={value.totalTokens}
|
||||
inline
|
||||
/>
|
||||
);
|
||||
},
|
||||
enableHiding: true,
|
||||
},
|
||||
{
|
||||
accessorKey: "input",
|
||||
header: "Input",
|
||||
cell: ({ row }) => {
|
||||
const value: unknown = row.getValue("input");
|
||||
return <JSONView json={value} className="w-[500px]" />;
|
||||
},
|
||||
enableHiding: true,
|
||||
defaultHidden: true,
|
||||
},
|
||||
{
|
||||
accessorKey: "output",
|
||||
header: "Output",
|
||||
cell: ({ row }) => {
|
||||
const value: unknown = row.getValue("output");
|
||||
return <JSONView json={value} className="w-[500px] bg-green-50" />;
|
||||
},
|
||||
enableHiding: true,
|
||||
defaultHidden: true,
|
||||
},
|
||||
{
|
||||
accessorKey: "metadata",
|
||||
header: "Metadata",
|
||||
cell: ({ row }) => {
|
||||
const values: string | undefined = row.getValue("metadata");
|
||||
return <div className="flex flex-wrap gap-x-3 gap-y-1">{values}</div>;
|
||||
},
|
||||
enableHiding: true,
|
||||
defaultHidden: true,
|
||||
},
|
||||
{
|
||||
accessorKey: "version",
|
||||
header: "Version",
|
||||
enableHiding: true,
|
||||
},
|
||||
];
|
||||
const [columnVisibility, setColumnVisibility] =
|
||||
useColumnVisibility<GenerationsTableRow>(
|
||||
"generationsColumnVisibility",
|
||||
columns,
|
||||
);
|
||||
|
||||
const rows: GenerationsTableRow[] = generations.isSuccess
|
||||
? generations.data.map((generation) => ({
|
||||
id: generation.id,
|
||||
traceId: generation.traceId,
|
||||
traceName: generation.traceName,
|
||||
startTime: generation.startTime.toLocaleString(),
|
||||
endTime: generation.endTime?.toLocaleString() ?? undefined,
|
||||
latency: generation.latency === null ? undefined : generation.latency,
|
||||
name: generation.name ?? undefined,
|
||||
version: generation.version ?? "",
|
||||
model: generation.model ?? "",
|
||||
input: generation.input,
|
||||
output: generation.output,
|
||||
level: generation.level,
|
||||
metadata: generation.metadata
|
||||
? JSON.stringify(generation.metadata)
|
||||
: undefined,
|
||||
statusMessage: generation.statusMessage ?? undefined,
|
||||
usage: {
|
||||
promptTokens: generation.promptTokens,
|
||||
completionTokens: generation.completionTokens,
|
||||
totalTokens: generation.totalTokens,
|
||||
},
|
||||
}))
|
||||
: [];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<DataTableToolbar
|
||||
columns={columns}
|
||||
filterColumnDefinition={observationsTableColsWithOptions(
|
||||
filterOptions.data,
|
||||
)}
|
||||
filterState={filterState}
|
||||
setFilterState={setFilterState}
|
||||
searchConfig={{
|
||||
placeholder: "Search by id, name, traceName, model",
|
||||
updateQuery: setSearchQuery,
|
||||
currentQuery: searchQuery ?? undefined,
|
||||
}}
|
||||
columnVisibility={columnVisibility}
|
||||
setColumnVisibility={setColumnVisibility}
|
||||
actionButtons={
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="ml-auto whitespace-nowrap"
|
||||
size="sm"
|
||||
>
|
||||
{filterState.length > 0 || searchQuery
|
||||
? "Export selection"
|
||||
: "Export all"}{" "}
|
||||
{isExporting ? (
|
||||
<Loader className="ml-2 h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<ChevronDownIcon className="ml-2 h-4 w-4" />
|
||||
)}
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
{Object.entries(exportOptions).map(([key, options]) => (
|
||||
<DropdownMenuItem
|
||||
key={key}
|
||||
className="capitalize"
|
||||
onClick={() => void handleExport(key as ExportFileFormats)}
|
||||
>
|
||||
as {options.label}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
}
|
||||
/>
|
||||
<DataTable
|
||||
columns={columns}
|
||||
data={
|
||||
generations.isLoading
|
||||
? { isLoading: true, isError: false }
|
||||
: generations.isError
|
||||
? {
|
||||
isLoading: false,
|
||||
isError: true,
|
||||
error: generations.error.message,
|
||||
}
|
||||
: {
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
data: rows,
|
||||
}
|
||||
}
|
||||
pagination={{
|
||||
pageCount: Math.ceil(totalCount / paginationState.pageSize),
|
||||
onChange: setPaginationState,
|
||||
state: paginationState,
|
||||
}}
|
||||
columnVisibility={columnVisibility}
|
||||
onColumnVisibilityChange={setColumnVisibility}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,15 +1,16 @@
|
||||
import { DataTable } from "@/src/components/table/data-table";
|
||||
import { DataTableToolbar } from "@/src/components/table/data-table-toolbar";
|
||||
import TableLink from "@/src/components/table/table-link";
|
||||
import { type LangfuseColumnDef } from "@/src/components/table/types";
|
||||
import useColumnVisibility from "@/src/features/column-visibility/hooks/useColumnVisibility";
|
||||
import { useQueryFilterState } from "@/src/features/filters/hooks/useFilterState";
|
||||
import { scoresTableColsWithOptions } from "@/src/server/api/definitions/scoresTable";
|
||||
import { api } from "@/src/utils/api";
|
||||
import { type RouterInput } from "@/src/utils/types";
|
||||
import { type Score } from "@prisma/client";
|
||||
import { type ColumnDef } from "@tanstack/react-table";
|
||||
import { useQueryParams, withDefault, NumberParam } from "use-query-params";
|
||||
|
||||
type RowData = {
|
||||
export type ScoresTableRow = {
|
||||
id: string;
|
||||
traceId: string;
|
||||
timestamp: string;
|
||||
@@ -60,7 +61,7 @@ export default function ScoresTable({
|
||||
projectId,
|
||||
});
|
||||
|
||||
const columns: ColumnDef<RowData>[] = [
|
||||
const columns: LangfuseColumnDef<ScoresTableRow>[] = [
|
||||
{
|
||||
accessorKey: "traceId",
|
||||
enableColumnFilter: true,
|
||||
@@ -95,22 +96,29 @@ export default function ScoresTable({
|
||||
{
|
||||
accessorKey: "timestamp",
|
||||
header: "Timestamp",
|
||||
enableHiding: true,
|
||||
},
|
||||
{
|
||||
accessorKey: "name",
|
||||
header: "Name",
|
||||
enableHiding: true,
|
||||
},
|
||||
{
|
||||
accessorKey: "value",
|
||||
header: "Value",
|
||||
enableHiding: true,
|
||||
},
|
||||
{
|
||||
accessorKey: "comment",
|
||||
header: "Comment",
|
||||
enableHiding: true,
|
||||
},
|
||||
];
|
||||
|
||||
const convertToTableRow = (score: Score): RowData => {
|
||||
const [columnVisibility, setColumnVisibility] =
|
||||
useColumnVisibility<ScoresTableRow>("scoresColumnVisibility", columns);
|
||||
|
||||
const convertToTableRow = (score: Score): ScoresTableRow => {
|
||||
return {
|
||||
id: score.id,
|
||||
timestamp: score.timestamp.toLocaleString(),
|
||||
@@ -125,9 +133,12 @@ export default function ScoresTable({
|
||||
return (
|
||||
<div>
|
||||
<DataTableToolbar
|
||||
columns={columns}
|
||||
filterColumnDefinition={scoresTableColsWithOptions(filterOptions.data)}
|
||||
filterState={userFilterState}
|
||||
setFilterState={setUserFilterState}
|
||||
columnVisibility={columnVisibility}
|
||||
setColumnVisibility={setColumnVisibility}
|
||||
/>
|
||||
<DataTable
|
||||
columns={columns}
|
||||
@@ -135,22 +146,24 @@ export default function ScoresTable({
|
||||
scores.isLoading
|
||||
? { isLoading: true, isError: false }
|
||||
: scores.isError
|
||||
? {
|
||||
isLoading: false,
|
||||
isError: true,
|
||||
error: scores.error.message,
|
||||
}
|
||||
: {
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
data: scores.data?.map((t) => convertToTableRow(t)),
|
||||
}
|
||||
? {
|
||||
isLoading: false,
|
||||
isError: true,
|
||||
error: scores.error.message,
|
||||
}
|
||||
: {
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
data: scores.data?.map((t) => convertToTableRow(t)),
|
||||
}
|
||||
}
|
||||
pagination={{
|
||||
pageCount: Math.ceil(totalCount / paginationState.pageSize),
|
||||
onChange: setPaginationState,
|
||||
state: paginationState,
|
||||
}}
|
||||
columnVisibility={columnVisibility}
|
||||
onColumnVisibilityChange={setColumnVisibility}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,217 @@
|
||||
import { StarSessionToggle } from "@/src/components/star-toggle";
|
||||
import { DataTable } from "@/src/components/table/data-table";
|
||||
import { DataTableToolbar } from "@/src/components/table/data-table-toolbar";
|
||||
import TableLink from "@/src/components/table/table-link";
|
||||
import { type LangfuseColumnDef } from "@/src/components/table/types";
|
||||
import useColumnVisibility from "@/src/features/column-visibility/hooks/useColumnVisibility";
|
||||
import { useQueryFilterState } from "@/src/features/filters/hooks/useFilterState";
|
||||
import { type FilterState } from "@/src/features/filters/types";
|
||||
import { useDetailPageLists } from "@/src/features/navigate-detail-pages/context";
|
||||
import { sessionsViewCols } from "@/src/server/api/definitions/sessionsView";
|
||||
import { api } from "@/src/utils/api";
|
||||
import { formatInterval, utcDateOffsetByDays } from "@/src/utils/dates";
|
||||
import { type RouterOutput } from "@/src/utils/types";
|
||||
import { useEffect } from "react";
|
||||
import { NumberParam, useQueryParams, withDefault } from "use-query-params";
|
||||
|
||||
export type SessionTableRow = {
|
||||
id: string;
|
||||
createdAt: string;
|
||||
userIds: string[];
|
||||
countTraces: number;
|
||||
bookmarked: boolean;
|
||||
sessionDuration: number | null;
|
||||
};
|
||||
|
||||
export type SessionTableProps = {
|
||||
projectId: string;
|
||||
userId?: string;
|
||||
omittedFilter?: string[];
|
||||
};
|
||||
|
||||
export default function SessionsTable({
|
||||
projectId,
|
||||
userId,
|
||||
omittedFilter = [],
|
||||
}: SessionTableProps) {
|
||||
const { setDetailPageList } = useDetailPageLists();
|
||||
|
||||
const [userFilterState, setUserFilterState] = useQueryFilterState([
|
||||
{
|
||||
column: "createdAt",
|
||||
type: "datetime",
|
||||
operator: ">",
|
||||
value: utcDateOffsetByDays(-14),
|
||||
},
|
||||
]);
|
||||
|
||||
const userIdFilter: FilterState = userId
|
||||
? [
|
||||
{
|
||||
column: "userId",
|
||||
type: "string",
|
||||
operator: "=",
|
||||
value: userId,
|
||||
},
|
||||
]
|
||||
: [];
|
||||
|
||||
const filterState = userFilterState.concat(userIdFilter);
|
||||
|
||||
const [paginationState, setPaginationState] = useQueryParams({
|
||||
pageIndex: withDefault(NumberParam, 0),
|
||||
pageSize: withDefault(NumberParam, 50),
|
||||
});
|
||||
|
||||
const sessions = api.sessions.all.useQuery({
|
||||
page: paginationState.pageIndex,
|
||||
limit: paginationState.pageSize,
|
||||
projectId,
|
||||
filter: filterState,
|
||||
});
|
||||
const totalCount = sessions.data?.slice(1)[0]?.totalCount ?? 0;
|
||||
useEffect(() => {
|
||||
if (sessions.isSuccess && sessions.data) {
|
||||
setDetailPageList(
|
||||
"sessions",
|
||||
sessions.data.map((t) => t.id),
|
||||
);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [sessions.isSuccess, sessions.data]);
|
||||
|
||||
const convertToTableRow = (
|
||||
session: RouterOutput["sessions"]["all"][0],
|
||||
): SessionTableRow => {
|
||||
return {
|
||||
id: session.id,
|
||||
createdAt: session.createdAt.toLocaleString(),
|
||||
userIds: session.userIds,
|
||||
countTraces: session.countTraces,
|
||||
bookmarked: session.bookmarked,
|
||||
sessionDuration: session.sessionDuration,
|
||||
};
|
||||
};
|
||||
|
||||
const columns: LangfuseColumnDef<SessionTableRow>[] = [
|
||||
{
|
||||
accessorKey: "bookmarked",
|
||||
header: undefined,
|
||||
cell: ({ row }) => {
|
||||
const bookmarked = row.getValue("bookmarked");
|
||||
const sessionId = row.getValue("id");
|
||||
|
||||
return typeof sessionId === "string" &&
|
||||
typeof bookmarked === "boolean" ? (
|
||||
<StarSessionToggle
|
||||
sessionId={sessionId}
|
||||
projectId={projectId}
|
||||
value={bookmarked}
|
||||
size="xs"
|
||||
/>
|
||||
) : undefined;
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "id",
|
||||
header: "ID",
|
||||
cell: ({ row }) => {
|
||||
const value = row.getValue("id");
|
||||
return value && typeof value === "string" ? (
|
||||
<TableLink
|
||||
path={`/project/${projectId}/sessions/${value}`}
|
||||
value={value}
|
||||
/>
|
||||
) : undefined;
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "createdAt",
|
||||
header: "Created At",
|
||||
enableHiding: true,
|
||||
},
|
||||
{
|
||||
accessorKey: "sessionDuration",
|
||||
header: "Duration",
|
||||
enableHiding: true,
|
||||
cell: ({ row }) => {
|
||||
const value = row.getValue("sessionDuration");
|
||||
return value && typeof value === "number"
|
||||
? formatInterval(value)
|
||||
: undefined;
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "userIds",
|
||||
enableColumnFilter: !omittedFilter.find((f) => f === "userIds"),
|
||||
header: "User ID",
|
||||
enableHiding: true,
|
||||
cell: ({ row }) => {
|
||||
const value = row.getValue("userIds");
|
||||
return value && Array.isArray(value) ? (
|
||||
<div className="flex gap-1">
|
||||
{(value as string[]).map((user) => (
|
||||
<TableLink
|
||||
key={user}
|
||||
path={`/project/${projectId}/users/${user}`}
|
||||
value={user}
|
||||
truncateAt={40}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
) : undefined;
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "countTraces",
|
||||
header: "Traces",
|
||||
enableHiding: true,
|
||||
},
|
||||
];
|
||||
|
||||
const [columnVisibility, setColumnVisibility] =
|
||||
useColumnVisibility<SessionTableRow>("sessionsColumnVisibility", columns);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<DataTableToolbar
|
||||
filterColumnDefinition={sessionsViewCols}
|
||||
filterState={userFilterState}
|
||||
setFilterState={setUserFilterState}
|
||||
columns={columns}
|
||||
columnVisibility={columnVisibility}
|
||||
setColumnVisibility={setColumnVisibility}
|
||||
/>
|
||||
<DataTable
|
||||
columns={columns}
|
||||
data={
|
||||
sessions.isLoading
|
||||
? { isLoading: true, isError: false }
|
||||
: sessions.isError
|
||||
? {
|
||||
isLoading: false,
|
||||
isError: true,
|
||||
error: sessions.error.message,
|
||||
}
|
||||
: {
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
data: sessions.data?.map((t) => convertToTableRow(t)),
|
||||
}
|
||||
}
|
||||
pagination={{
|
||||
pageCount: Math.ceil(totalCount / paginationState.pageSize),
|
||||
onChange: setPaginationState,
|
||||
state: paginationState,
|
||||
}}
|
||||
columnVisibility={columnVisibility}
|
||||
onColumnVisibilityChange={setColumnVisibility}
|
||||
help={{
|
||||
description:
|
||||
"A session is a collection of related traces, such as a conversation or thread. To begin, add a sessionId to the trace.",
|
||||
href: "https://langfuse.com/docs/sessions",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,17 +1,18 @@
|
||||
import { DeleteTrace } from "@/src/components/delete-trace";
|
||||
import { GroupedScoreBadges } from "@/src/components/grouped-score-badge";
|
||||
import { DataTable } from "@/src/components/table/data-table";
|
||||
import { DataTableToolbar } from "@/src/components/table/data-table-toolbar";
|
||||
import TableLink from "@/src/components/table/table-link";
|
||||
import { TokenUsageBadge } from "@/src/components/token-usage-badge";
|
||||
import useColumnVisibility from "@/src/features/column-visibility/hooks/useColumnVisibility";
|
||||
import { useQueryFilterState } from "@/src/features/filters/hooks/useFilterState";
|
||||
import { type FilterState } from "@/src/features/filters/types";
|
||||
import { useDetailPageLists } from "@/src/features/navigate-detail-pages/context";
|
||||
import { tracesTableColsWithOptions } from "@/src/server/api/definitions/tracesTable";
|
||||
import { api } from "@/src/utils/api";
|
||||
import { utcDateOffsetByDays } from "@/src/utils/dates";
|
||||
import { formatInterval, utcDateOffsetByDays } from "@/src/utils/dates";
|
||||
import { type RouterInput, type RouterOutput } from "@/src/utils/types";
|
||||
import { type Score } from "@prisma/client";
|
||||
import { type ColumnDef } from "@tanstack/react-table";
|
||||
import { useEffect } from "react";
|
||||
import {
|
||||
NumberParam,
|
||||
@@ -20,8 +21,12 @@ import {
|
||||
useQueryParams,
|
||||
withDefault,
|
||||
} from "use-query-params";
|
||||
import { StarTraceToggle } from "@/src/components/star-toggle";
|
||||
import { JSONView } from "@/src/components/ui/code";
|
||||
import { type LangfuseColumnDef } from "@/src/components/table/types";
|
||||
|
||||
export type TraceTableRow = {
|
||||
export type TracesTableRow = {
|
||||
bookmarked: boolean;
|
||||
id: string;
|
||||
timestamp: string;
|
||||
name: string;
|
||||
@@ -30,6 +35,9 @@ export type TraceTableRow = {
|
||||
latency?: number;
|
||||
release?: string;
|
||||
version?: string;
|
||||
input?: unknown;
|
||||
output?: unknown;
|
||||
sessionId?: string;
|
||||
scores: Score[];
|
||||
usage: {
|
||||
promptTokens: number;
|
||||
@@ -38,7 +46,7 @@ export type TraceTableRow = {
|
||||
};
|
||||
};
|
||||
|
||||
export type TraceTableProps = {
|
||||
export type TracesTableProps = {
|
||||
projectId: string;
|
||||
userId?: string;
|
||||
omittedFilter?: string[];
|
||||
@@ -50,13 +58,12 @@ export default function TracesTable({
|
||||
projectId,
|
||||
userId,
|
||||
omittedFilter = [],
|
||||
}: TraceTableProps) {
|
||||
}: TracesTableProps) {
|
||||
const { setDetailPageList } = useDetailPageLists();
|
||||
const [searchQuery, setSearchQuery] = useQueryParam(
|
||||
"search",
|
||||
withDefault(StringParam, null),
|
||||
);
|
||||
|
||||
const [userFilterState, setUserFilterState] = useQueryFilterState([
|
||||
{
|
||||
column: "timestamp",
|
||||
@@ -78,7 +85,6 @@ export default function TracesTable({
|
||||
: [];
|
||||
|
||||
const filterState = userFilterState.concat(userIdFilter);
|
||||
|
||||
const [paginationState, setPaginationState] = useQueryParams({
|
||||
pageIndex: withDefault(NumberParam, 0),
|
||||
pageSize: withDefault(NumberParam, 50),
|
||||
@@ -108,8 +114,9 @@ export default function TracesTable({
|
||||
|
||||
const convertToTableRow = (
|
||||
trace: RouterOutput["traces"]["all"][0],
|
||||
): TraceTableRow => {
|
||||
): TracesTableRow => {
|
||||
return {
|
||||
bookmarked: trace.bookmarked,
|
||||
id: trace.id,
|
||||
timestamp: trace.timestamp.toLocaleString(),
|
||||
name: trace.name ?? "",
|
||||
@@ -118,6 +125,9 @@ export default function TracesTable({
|
||||
version: trace.version ?? undefined,
|
||||
userId: trace.userId ?? "",
|
||||
scores: trace.scores,
|
||||
sessionId: trace.sessionId ?? undefined,
|
||||
input: trace.input,
|
||||
output: trace.output,
|
||||
latency: trace.latency === null ? undefined : trace.latency,
|
||||
usage: {
|
||||
promptTokens: trace.promptTokens,
|
||||
@@ -127,7 +137,25 @@ export default function TracesTable({
|
||||
};
|
||||
};
|
||||
|
||||
const columns: ColumnDef<TraceTableRow>[] = [
|
||||
const columns: LangfuseColumnDef<TracesTableRow>[] = [
|
||||
{
|
||||
accessorKey: "bookmarked",
|
||||
header: undefined,
|
||||
cell: ({ row }) => {
|
||||
const bookmarked = row.getValue("bookmarked");
|
||||
const traceId = row.getValue("id");
|
||||
|
||||
return typeof traceId === "string" &&
|
||||
typeof bookmarked === "boolean" ? (
|
||||
<StarTraceToggle
|
||||
traceId={traceId}
|
||||
projectId={projectId}
|
||||
value={bookmarked}
|
||||
size="xs"
|
||||
/>
|
||||
) : undefined;
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "id",
|
||||
header: "ID",
|
||||
@@ -144,10 +172,12 @@ export default function TracesTable({
|
||||
{
|
||||
accessorKey: "timestamp",
|
||||
header: "Timestamp",
|
||||
enableHiding: true,
|
||||
},
|
||||
{
|
||||
accessorKey: "name",
|
||||
header: "Name",
|
||||
enableHiding: true,
|
||||
},
|
||||
{
|
||||
accessorKey: "userId",
|
||||
@@ -163,6 +193,23 @@ export default function TracesTable({
|
||||
/>
|
||||
) : undefined;
|
||||
},
|
||||
enableHiding: true,
|
||||
},
|
||||
{
|
||||
accessorKey: "sessionId",
|
||||
enableColumnFilter: !omittedFilter.find((f) => f === "sessionId"),
|
||||
header: "Session ID",
|
||||
cell: ({ row }) => {
|
||||
const value = row.getValue("sessionId");
|
||||
return value && typeof value === "string" ? (
|
||||
<TableLink
|
||||
path={`/project/${projectId}/sessions/${value}`}
|
||||
value={value}
|
||||
truncateAt={40}
|
||||
/>
|
||||
) : undefined;
|
||||
},
|
||||
enableHiding: true,
|
||||
},
|
||||
{
|
||||
accessorKey: "latency",
|
||||
@@ -170,8 +217,9 @@ export default function TracesTable({
|
||||
// add seconds to the end of the latency
|
||||
cell: ({ row }) => {
|
||||
const value: number | undefined = row.getValue("latency");
|
||||
return value !== undefined ? `${value.toFixed(2)} sec` : undefined;
|
||||
return value !== undefined ? formatInterval(value) : undefined;
|
||||
},
|
||||
enableHiding: true,
|
||||
},
|
||||
{
|
||||
accessorKey: "usage",
|
||||
@@ -191,6 +239,7 @@ export default function TracesTable({
|
||||
/>
|
||||
);
|
||||
},
|
||||
enableHiding: true,
|
||||
},
|
||||
{
|
||||
accessorKey: "scores",
|
||||
@@ -200,6 +249,27 @@ export default function TracesTable({
|
||||
const values: Score[] = row.getValue("scores");
|
||||
return <GroupedScoreBadges scores={values} variant="headings" />;
|
||||
},
|
||||
enableHiding: true,
|
||||
},
|
||||
{
|
||||
accessorKey: "input",
|
||||
header: "Input",
|
||||
cell: ({ row }) => {
|
||||
const value: unknown = row.getValue("input");
|
||||
return <JSONView json={value} className="w-[500px]" />;
|
||||
},
|
||||
enableHiding: true,
|
||||
defaultHidden: true,
|
||||
},
|
||||
{
|
||||
accessorKey: "output",
|
||||
header: "Output",
|
||||
cell: ({ row }) => {
|
||||
const value: unknown = row.getValue("output");
|
||||
return <JSONView json={value} className="w-[500px] bg-green-50" />;
|
||||
},
|
||||
enableHiding: true,
|
||||
defaultHidden: true,
|
||||
},
|
||||
{
|
||||
accessorKey: "metadata",
|
||||
@@ -208,20 +278,41 @@ export default function TracesTable({
|
||||
const values: string = row.getValue("metadata");
|
||||
return <div className="flex flex-wrap gap-x-3 gap-y-1">{values}</div>;
|
||||
},
|
||||
enableHiding: true,
|
||||
},
|
||||
{
|
||||
accessorKey: "version",
|
||||
header: "Version",
|
||||
enableHiding: true,
|
||||
},
|
||||
{
|
||||
accessorKey: "release",
|
||||
header: "Release",
|
||||
enableHiding: true,
|
||||
},
|
||||
{
|
||||
accessorKey: "action",
|
||||
header: "Action",
|
||||
cell: ({ row }) => {
|
||||
const traceId = row.getValue("id");
|
||||
return traceId && typeof traceId === "string" ? (
|
||||
<DeleteTrace
|
||||
traceId={traceId}
|
||||
isTableAction={true}
|
||||
projectId={projectId}
|
||||
/>
|
||||
) : undefined;
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const [columnVisibility, setColumnVisibility] =
|
||||
useColumnVisibility<TracesTableRow>("tracesColumnVisibility", columns);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<DataTableToolbar
|
||||
columns={columns}
|
||||
filterColumnDefinition={tracesTableColsWithOptions(
|
||||
traceFilterOptions.data,
|
||||
)}
|
||||
@@ -232,6 +323,8 @@ export default function TracesTable({
|
||||
}}
|
||||
filterState={userFilterState}
|
||||
setFilterState={setUserFilterState}
|
||||
columnVisibility={columnVisibility}
|
||||
setColumnVisibility={setColumnVisibility}
|
||||
/>
|
||||
<DataTable
|
||||
columns={columns}
|
||||
@@ -239,22 +332,24 @@ export default function TracesTable({
|
||||
traces.isLoading
|
||||
? { isLoading: true, isError: false }
|
||||
: traces.isError
|
||||
? {
|
||||
isLoading: false,
|
||||
isError: true,
|
||||
error: traces.error.message,
|
||||
}
|
||||
: {
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
data: traces.data?.map((t) => convertToTableRow(t)),
|
||||
}
|
||||
? {
|
||||
isLoading: false,
|
||||
isError: true,
|
||||
error: traces.error.message,
|
||||
}
|
||||
: {
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
data: traces.data?.map((t) => convertToTableRow(t)),
|
||||
}
|
||||
}
|
||||
pagination={{
|
||||
pageCount: Math.ceil(totalCount / paginationState.pageSize),
|
||||
onChange: setPaginationState,
|
||||
state: paginationState,
|
||||
}}
|
||||
columnVisibility={columnVisibility}
|
||||
onColumnVisibilityChange={setColumnVisibility}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
import { JSONView } from "@/src/components/ui/code";
|
||||
import { z } from "zod";
|
||||
import { deepParseJson } from "@/src/utils/json";
|
||||
import { cn } from "@/src/utils/tailwind";
|
||||
import { useState } from "react";
|
||||
import { Button } from "@/src/components/ui/button";
|
||||
import { Tabs, TabsList, TabsTrigger } from "@/src/components/ui/tabs";
|
||||
import { Fragment } from "react";
|
||||
|
||||
export const IOPreview: React.FC<{
|
||||
input?: unknown;
|
||||
output?: unknown;
|
||||
isLoading?: boolean;
|
||||
hideIfNull?: boolean;
|
||||
}> = ({ isLoading = false, hideIfNull = false, ...props }) => {
|
||||
const [currentView, setCurrentView] = useState<"pretty" | "json">("pretty");
|
||||
|
||||
const input = deepParseJson(props.input);
|
||||
const output = deepParseJson(props.output);
|
||||
|
||||
// parse old completions: { completion: string } -> string
|
||||
const outLegacyCompletionSchema = z
|
||||
.object({
|
||||
completion: z.string(),
|
||||
})
|
||||
.refine((value) => Object.keys(value).length === 1);
|
||||
const outLegacyCompletionSchemaParsed =
|
||||
outLegacyCompletionSchema.safeParse(output);
|
||||
const outputClean = outLegacyCompletionSchemaParsed.success
|
||||
? outLegacyCompletionSchemaParsed.data
|
||||
: props.output ?? null;
|
||||
|
||||
// OpenAI messages
|
||||
let inOpenAiMessageArray = OpenAiMessageArraySchema.safeParse(input);
|
||||
if (!inOpenAiMessageArray.success) {
|
||||
// check if input is an array of length 1 including an array of OpenAiMessageSchema
|
||||
// this is the case for some integrations
|
||||
const inputArray = z.array(OpenAiMessageArraySchema).safeParse(input);
|
||||
if (inputArray.success && inputArray.data.length === 1) {
|
||||
inOpenAiMessageArray = OpenAiMessageArraySchema.safeParse(
|
||||
inputArray.data[0],
|
||||
);
|
||||
}
|
||||
}
|
||||
const outOpenAiMessage = OpenAiMessageSchema.safeParse(output);
|
||||
|
||||
// Pretty view available
|
||||
const isPrettyViewAvailable = inOpenAiMessageArray.success;
|
||||
|
||||
// default I/O
|
||||
return (
|
||||
<>
|
||||
{isPrettyViewAvailable ? (
|
||||
<Tabs
|
||||
value={currentView}
|
||||
onValueChange={(v) => setCurrentView(v as "pretty" | "json")}
|
||||
>
|
||||
<TabsList>
|
||||
<TabsTrigger value="pretty">Pretty ✨</TabsTrigger>
|
||||
<TabsTrigger value="json">JSON</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
) : null}
|
||||
{isPrettyViewAvailable && currentView === "pretty" ? (
|
||||
inOpenAiMessageArray.success ? (
|
||||
<OpenAiMessageView
|
||||
messages={inOpenAiMessageArray.data.concat(
|
||||
outOpenAiMessage.success
|
||||
? {
|
||||
...outOpenAiMessage.data,
|
||||
role: outOpenAiMessage.data.role ?? "assistant",
|
||||
}
|
||||
: {
|
||||
role: "assistant",
|
||||
content: JSON.stringify(outputClean) ?? null,
|
||||
},
|
||||
)}
|
||||
/>
|
||||
) : null
|
||||
) : null}
|
||||
{currentView === "json" || !isPrettyViewAvailable ? (
|
||||
<>
|
||||
{!(hideIfNull && !input) ? (
|
||||
<JSONView
|
||||
title="Input"
|
||||
json={input ?? null}
|
||||
isLoading={isLoading}
|
||||
className="flex-1"
|
||||
/>
|
||||
) : null}
|
||||
{!(hideIfNull && !output) ? (
|
||||
<JSONView
|
||||
title="Output"
|
||||
json={outputClean}
|
||||
isLoading={isLoading}
|
||||
className="flex-1 bg-green-50"
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const OpenAiMessageSchema = z
|
||||
.object({
|
||||
role: z.enum(["system", "user", "assistant"]).optional(),
|
||||
content: z.string().nullable(),
|
||||
})
|
||||
.refine((value) => value.content !== null || value.role !== undefined);
|
||||
|
||||
const OpenAiMessageArraySchema = z.array(OpenAiMessageSchema).min(1);
|
||||
|
||||
const OpenAiMessageView: React.FC<{
|
||||
messages: z.infer<typeof OpenAiMessageArraySchema>;
|
||||
}> = ({ messages }) => {
|
||||
const COLLAPSE_THRESHOLD = 3;
|
||||
const [isCollapsed, setCollapsed] = useState(
|
||||
messages.length > COLLAPSE_THRESHOLD ? true : null,
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-2 rounded-md border p-3">
|
||||
{messages
|
||||
.filter(
|
||||
(_, i) =>
|
||||
// show all if not collapsed or null; show first and last n if collapsed
|
||||
!isCollapsed || i == 0 || i > messages.length - COLLAPSE_THRESHOLD,
|
||||
)
|
||||
.map((message, index) => (
|
||||
<Fragment key={index}>
|
||||
<JSONView
|
||||
title={message.role}
|
||||
json={message.content}
|
||||
className={cn(
|
||||
message.role === "system" && "bg-gray-100",
|
||||
message.role === "assistant" && "bg-green-50",
|
||||
)}
|
||||
/>
|
||||
{isCollapsed !== null && index === 0 ? (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="xs"
|
||||
onClick={() => setCollapsed((v) => !v)}
|
||||
>
|
||||
{isCollapsed
|
||||
? `Show ${messages.length - COLLAPSE_THRESHOLD} more ...`
|
||||
: "Hide history"}
|
||||
</Button>
|
||||
) : null}
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -16,16 +16,12 @@ import {
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/src/components/ui/table";
|
||||
import { ManualScoreButton } from "@/src/features/manual-scoring/components";
|
||||
import { ManualScoreButton } from "@/src/features/manual-scoring/components/ManualScoreButton";
|
||||
import { NewDatasetItemFromObservationButton } from "@/src/features/datasets/components/NewDatasetItemFromObservationButton";
|
||||
import { type ObservationReturnType } from "@/src/server/api/routers/traces";
|
||||
import { api } from "@/src/utils/api";
|
||||
import { z } from "zod";
|
||||
import { deepParseJson } from "@/src/utils/json";
|
||||
import { cn } from "@/src/utils/tailwind";
|
||||
import { useState } from "react";
|
||||
import { Button } from "@/src/components/ui/button";
|
||||
import { Tabs, TabsList, TabsTrigger } from "@/src/components/ui/tabs";
|
||||
import { IOPreview } from "@/src/components/trace/IOPreview";
|
||||
import { formatInterval } from "@/src/utils/dates";
|
||||
|
||||
export const ObservationPreview = (props: {
|
||||
observations: Array<ObservationReturnType>;
|
||||
@@ -60,18 +56,20 @@ export const ObservationPreview = (props: {
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{preloadedObservation.endTime ? (
|
||||
<Badge variant="outline">
|
||||
{`${(
|
||||
{formatInterval(
|
||||
(preloadedObservation.endTime.getTime() -
|
||||
preloadedObservation.startTime.getTime()) /
|
||||
1000
|
||||
).toFixed(2)} sec`}
|
||||
1000,
|
||||
)}
|
||||
</Badge>
|
||||
) : null}
|
||||
<Badge variant="outline">
|
||||
{preloadedObservation.promptTokens} prompt →{" "}
|
||||
{preloadedObservation.completionTokens} completion (∑{" "}
|
||||
{preloadedObservation.totalTokens})
|
||||
</Badge>
|
||||
{preloadedObservation.type === "GENERATION" && (
|
||||
<Badge variant="outline">
|
||||
{preloadedObservation.promptTokens} prompt →{" "}
|
||||
{preloadedObservation.completionTokens} completion (∑{" "}
|
||||
{preloadedObservation.totalTokens})
|
||||
</Badge>
|
||||
)}
|
||||
{preloadedObservation.version ? (
|
||||
<Badge variant="outline">
|
||||
Version: {preloadedObservation.version}
|
||||
@@ -116,14 +114,12 @@ export const ObservationPreview = (props: {
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-4">
|
||||
<ObservationIO
|
||||
<IOPreview
|
||||
key={preloadedObservation.id + "-input"}
|
||||
observationType={preloadedObservation.type}
|
||||
input={observationWithInputAndOutput.data?.input ?? undefined}
|
||||
output={observationWithInputAndOutput.data?.output ?? undefined}
|
||||
isLoading={observationWithInputAndOutput.isLoading}
|
||||
/>
|
||||
|
||||
{preloadedObservation.statusMessage ? (
|
||||
<JSONView
|
||||
key={preloadedObservation.id + "-status"}
|
||||
@@ -177,147 +173,3 @@ export const ObservationPreview = (props: {
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
const ObservationIO: React.FC<{
|
||||
observationType: string;
|
||||
input?: unknown;
|
||||
output?: unknown;
|
||||
isLoading: boolean;
|
||||
}> = ({ isLoading, ...props }) => {
|
||||
const [currentView, setCurrentView] = useState<"pretty" | "json">("pretty");
|
||||
|
||||
const input = deepParseJson(props.input);
|
||||
const output = deepParseJson(props.output);
|
||||
|
||||
// parse old completions: { completion: string } -> string
|
||||
const outLegacyCompletionSchema = z
|
||||
.object({
|
||||
completion: z.string(),
|
||||
})
|
||||
.refine((value) => Object.keys(value).length === 1);
|
||||
const outLegacyCompletionSchemaParsed =
|
||||
outLegacyCompletionSchema.safeParse(output);
|
||||
const outputClean = outLegacyCompletionSchemaParsed.success
|
||||
? outLegacyCompletionSchemaParsed.data
|
||||
: props.output ?? null;
|
||||
|
||||
// OpenAI messages
|
||||
let inOpenAiMessageArray = OpenAiMessageArraySchema.safeParse(input);
|
||||
if (!inOpenAiMessageArray.success) {
|
||||
// check if input is an array of length 1 including an array of OpenAiMessageSchema
|
||||
// this is the case for some integrations
|
||||
const inputArray = z.array(OpenAiMessageArraySchema).safeParse(input);
|
||||
if (inputArray.success && inputArray.data.length === 1) {
|
||||
inOpenAiMessageArray = OpenAiMessageArraySchema.safeParse(
|
||||
inputArray.data[0],
|
||||
);
|
||||
}
|
||||
}
|
||||
const outOpenAiMessage = OpenAiMessageSchema.safeParse(output);
|
||||
|
||||
// Pretty view available
|
||||
const isPrettyViewAvailable = inOpenAiMessageArray.success;
|
||||
|
||||
// default I/O
|
||||
return (
|
||||
<>
|
||||
{isPrettyViewAvailable ? (
|
||||
<Tabs
|
||||
value={currentView}
|
||||
onValueChange={(v) => setCurrentView(v as "pretty" | "json")}
|
||||
>
|
||||
<TabsList>
|
||||
<TabsTrigger value="pretty">Pretty ✨</TabsTrigger>
|
||||
<TabsTrigger value="json">JSON</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
) : null}
|
||||
{isPrettyViewAvailable && currentView === "pretty" ? (
|
||||
inOpenAiMessageArray.success ? (
|
||||
<OpenAiMessageView
|
||||
messages={inOpenAiMessageArray.data.concat(
|
||||
outOpenAiMessage.success
|
||||
? {
|
||||
...outOpenAiMessage.data,
|
||||
role: outOpenAiMessage.data.role ?? "assistant",
|
||||
}
|
||||
: {
|
||||
role: "assistant",
|
||||
content: JSON.stringify(outputClean) ?? null,
|
||||
},
|
||||
)}
|
||||
/>
|
||||
) : null
|
||||
) : null}
|
||||
{currentView === "json" || !isPrettyViewAvailable ? (
|
||||
<>
|
||||
<JSONView
|
||||
title="Input"
|
||||
json={input ?? null}
|
||||
isLoading={isLoading}
|
||||
className="flex-1"
|
||||
/>
|
||||
<JSONView
|
||||
title="Output"
|
||||
json={outputClean}
|
||||
isLoading={isLoading}
|
||||
className="flex-1"
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const OpenAiMessageSchema = z
|
||||
.object({
|
||||
role: z.enum(["system", "user", "assistant"]).optional(),
|
||||
content: z.string().nullable(),
|
||||
})
|
||||
.refine((value) => value.content !== null || value.role !== undefined);
|
||||
|
||||
const OpenAiMessageArraySchema = z.array(OpenAiMessageSchema).min(1);
|
||||
|
||||
const OpenAiMessageView: React.FC<{
|
||||
messages: z.infer<typeof OpenAiMessageArraySchema>;
|
||||
}> = ({ messages }) => {
|
||||
const COLLAPSE_THRESHOLD = 3;
|
||||
const [isCollapsed, setCollapsed] = useState(
|
||||
messages.length > COLLAPSE_THRESHOLD ? true : null,
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-2 rounded-md border p-3">
|
||||
{messages
|
||||
.filter(
|
||||
(_, i) =>
|
||||
// show all if not collapsed or null; show first and last n if collapsed
|
||||
!isCollapsed || i == 0 || i > messages.length - COLLAPSE_THRESHOLD,
|
||||
)
|
||||
.map((message, index) => (
|
||||
<>
|
||||
<JSONView
|
||||
title={message.role}
|
||||
json={message.content}
|
||||
key={index}
|
||||
className={cn(
|
||||
message.role === "system" && "bg-gray-100",
|
||||
message.role === "assistant" && "bg-green-50",
|
||||
)}
|
||||
/>
|
||||
{isCollapsed !== null && index === 0 ? (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="xs"
|
||||
onClick={() => setCollapsed((v) => !v)}
|
||||
>
|
||||
{isCollapsed
|
||||
? `Show ${messages.length - COLLAPSE_THRESHOLD} more ...`
|
||||
: "Hide history"}
|
||||
</Button>
|
||||
) : null}
|
||||
</>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -4,6 +4,8 @@ import { type Trace, type Score } from "@prisma/client";
|
||||
import { GroupedScoreBadges } from "@/src/components/grouped-score-badge";
|
||||
import { Fragment } from "react";
|
||||
import { type ObservationReturnType } from "@/src/server/api/routers/traces";
|
||||
import { LevelColors } from "@/src/components/level-colors";
|
||||
import { formatInterval } from "@/src/utils/dates";
|
||||
|
||||
export const ObservationTree = (props: {
|
||||
observations: ObservationReturnType[];
|
||||
@@ -55,7 +57,7 @@ const ObservationTreeTraceNode = (props: {
|
||||
{props.trace.latency ? (
|
||||
<div className="flex gap-2">
|
||||
<span className="text-xs text-gray-500">
|
||||
{props.trace.latency.toFixed(2)} sec
|
||||
{formatInterval(props.trace.latency)}
|
||||
</span>
|
||||
</div>
|
||||
) : null}
|
||||
@@ -106,12 +108,11 @@ const ObservationTreeNode = (props: {
|
||||
<div className="flex gap-2">
|
||||
{observation.endTime ? (
|
||||
<span className="text-xs text-gray-500">
|
||||
{(
|
||||
{formatInterval(
|
||||
(observation.endTime.getTime() -
|
||||
observation.startTime.getTime()) /
|
||||
1000
|
||||
).toFixed(2)}{" "}
|
||||
sec
|
||||
1000,
|
||||
)}
|
||||
</span>
|
||||
) : null}
|
||||
{observation.promptTokens ||
|
||||
@@ -127,9 +128,9 @@ const ObservationTreeNode = (props: {
|
||||
<div className="flex">
|
||||
<span
|
||||
className={cn(
|
||||
"rounded-sm text-xs",
|
||||
LevelColor[observation.level].bg,
|
||||
LevelColor[observation.level].text,
|
||||
"rounded-sm p-0.5 text-xs",
|
||||
LevelColors[observation.level].bg,
|
||||
LevelColors[observation.level].text,
|
||||
)}
|
||||
>
|
||||
{observation.level}
|
||||
@@ -159,13 +160,6 @@ const ObservationTreeNode = (props: {
|
||||
</>
|
||||
);
|
||||
|
||||
const LevelColor = {
|
||||
DEFAULT: { text: "", bg: "" },
|
||||
DEBUG: { text: "text-gray-500", bg: "bg-gray-50" },
|
||||
WARNING: { text: "text-yellow-800", bg: "bg-yellow-50" },
|
||||
ERROR: { text: "text-red-800", bg: "bg-red-50" },
|
||||
};
|
||||
|
||||
export function nestObservations(
|
||||
list: ObservationReturnType[],
|
||||
): NestedObservation[] {
|
||||
|
||||
@@ -17,16 +17,18 @@ import {
|
||||
TableRow,
|
||||
} from "@/src/components/ui/table";
|
||||
import { TraceAggUsageBadge } from "@/src/components/token-usage-badge";
|
||||
import { ManualScoreButton } from "@/src/features/manual-scoring/components";
|
||||
import { ManualScoreButton } from "@/src/features/manual-scoring/components/ManualScoreButton";
|
||||
import { Badge } from "@/src/components/ui/badge";
|
||||
import { type ObservationReturnType } from "@/src/server/api/routers/traces";
|
||||
import { IOPreview } from "@/src/components/trace/IOPreview";
|
||||
import { formatInterval } from "@/src/utils/dates";
|
||||
|
||||
export const TracePreview = ({
|
||||
trace,
|
||||
observations,
|
||||
scores,
|
||||
}: {
|
||||
trace: Trace;
|
||||
trace: Trace & { latency?: number };
|
||||
observations: ObservationReturnType[];
|
||||
scores: Score[];
|
||||
}) => {
|
||||
@@ -42,13 +44,16 @@ export const TracePreview = ({
|
||||
</CardTitle>
|
||||
<CardDescription>{trace.timestamp.toLocaleString()}</CardDescription>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{!!trace.latency && (
|
||||
<Badge variant="outline">{formatInterval(trace.latency)}</Badge>
|
||||
)}
|
||||
<TraceAggUsageBadge observations={observations} />
|
||||
{trace.release ? (
|
||||
{!!trace.release && (
|
||||
<Badge variant="outline">Release: {trace.release}</Badge>
|
||||
) : undefined}
|
||||
{trace.version ? (
|
||||
)}
|
||||
{!!trace.version && (
|
||||
<Badge variant="outline">Version: {trace.version}</Badge>
|
||||
) : undefined}
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<ManualScoreButton
|
||||
@@ -57,7 +62,12 @@ export const TracePreview = ({
|
||||
scores={scores}
|
||||
/>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<CardContent className="flex flex-col gap-4">
|
||||
<IOPreview
|
||||
key={trace.id + "-io"}
|
||||
input={trace.input ?? undefined}
|
||||
output={trace.output ?? undefined}
|
||||
/>
|
||||
<JSONView
|
||||
key={trace.id + "-metadata"}
|
||||
title="Metadata"
|
||||
|
||||
@@ -8,11 +8,15 @@ import { Badge } from "@/src/components/ui/badge";
|
||||
import { TraceAggUsageBadge } from "@/src/components/token-usage-badge";
|
||||
import Decimal from "decimal.js";
|
||||
import { StringParam, useQueryParam } from "use-query-params";
|
||||
import { PublishTraceSwitch } from "@/src/features/public-traces/components/PublishTraceSwitch";
|
||||
import { PublishTraceSwitch } from "@/src/components/publish-object-switch";
|
||||
import { DetailPageNav } from "@/src/features/navigate-detail-pages/DetailPageNav";
|
||||
import { useRouter } from "next/router";
|
||||
import { type ObservationReturnType } from "@/src/server/api/routers/traces";
|
||||
import { api } from "@/src/utils/api";
|
||||
import { DeleteTrace } from "@/src/components/delete-trace";
|
||||
import { StarTraceToggle } from "@/src/components/star-toggle";
|
||||
import Link from "next/link";
|
||||
import { NoAccessError } from "@/src/components/no-access";
|
||||
|
||||
export function Trace(props: {
|
||||
observations: Array<ObservationReturnType>;
|
||||
@@ -70,7 +74,15 @@ export function Trace(props: {
|
||||
|
||||
export function TracePage({ traceId }: { traceId: string }) {
|
||||
const router = useRouter();
|
||||
const trace = api.traces.byId.useQuery({ traceId });
|
||||
const trace = api.traces.byId.useQuery(
|
||||
{ traceId },
|
||||
{
|
||||
retry(failureCount, error) {
|
||||
if (error.data?.code === "UNAUTHORIZED") return false;
|
||||
return failureCount < 3;
|
||||
},
|
||||
},
|
||||
);
|
||||
const totalCost = trace.data?.observations.reduce(
|
||||
(acc, o) => {
|
||||
if (!o.price) return acc;
|
||||
@@ -82,14 +94,27 @@ export function TracePage({ traceId }: { traceId: string }) {
|
||||
undefined as Decimal | undefined,
|
||||
);
|
||||
|
||||
if (trace.error?.data?.code === "UNAUTHORIZED") return <NoAccessError />;
|
||||
if (!trace.data) return <div>loading...</div>;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col overflow-hidden xl:container lg:h-[calc(100vh-100px)] xl:h-[calc(100vh-50px)]">
|
||||
<div className="flex flex-col overflow-hidden xl:container">
|
||||
<Header
|
||||
title="Trace Detail"
|
||||
breadcrumb={[
|
||||
{
|
||||
name: "Traces",
|
||||
href: `/project/${router.query.projectId as string}/traces`,
|
||||
},
|
||||
{ name: traceId },
|
||||
]}
|
||||
actionButtons={
|
||||
<>
|
||||
<StarTraceToggle
|
||||
traceId={trace.data.id}
|
||||
projectId={trace.data.projectId}
|
||||
value={trace.data.bookmarked}
|
||||
/>
|
||||
<PublishTraceSwitch
|
||||
traceId={trace.data.id}
|
||||
projectId={trace.data.projectId}
|
||||
@@ -102,12 +127,31 @@ export function TracePage({ traceId }: { traceId: string }) {
|
||||
}
|
||||
listKey="traces"
|
||||
/>
|
||||
<DeleteTrace
|
||||
traceId={trace.data.id}
|
||||
projectId={trace.data.projectId}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<div className="flex gap-2">
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{trace.data.sessionId ? (
|
||||
<Link
|
||||
href={`/project/${router.query.projectId as string}/sessions/${
|
||||
trace.data.sessionId
|
||||
}`}
|
||||
>
|
||||
<Badge>Session: {trace.data.sessionId}</Badge>
|
||||
</Link>
|
||||
) : null}
|
||||
{trace.data.userId ? (
|
||||
<Badge variant="outline">User ID: {trace.data.userId}</Badge>
|
||||
<Link
|
||||
href={`/project/${router.query.projectId as string}/users/${
|
||||
trace.data.userId
|
||||
}`}
|
||||
>
|
||||
<Badge>User ID: {trace.data.userId}</Badge>
|
||||
</Link>
|
||||
) : null}
|
||||
<TraceAggUsageBadge observations={trace.data.observations ?? []} />
|
||||
{totalCost ? (
|
||||
|
||||
@@ -78,7 +78,7 @@ export { Button, buttonVariants };
|
||||
|
||||
function Spinner() {
|
||||
return (
|
||||
<div className="flex h-3/4 items-center justify-center">
|
||||
<div className="flex h-1/2 items-center justify-center">
|
||||
<Loader2 className="h-full w-full animate-spin" />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -33,6 +33,7 @@ export function JSONView(props: {
|
||||
collapseStringsAfterLength={500}
|
||||
displaySize={"collapsed"}
|
||||
matchesURL={true}
|
||||
customizeCopy={(node) => stringifyJsonNode(node)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@@ -97,3 +98,33 @@ export function CodeView(props: {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function stringifyJsonNode(node: unknown) {
|
||||
// return single string nodes without quotes
|
||||
if (typeof node === "string") {
|
||||
return node;
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.stringify(
|
||||
node,
|
||||
(key, value) => {
|
||||
switch (typeof value) {
|
||||
case "bigint":
|
||||
return String(value) + "n";
|
||||
case "number":
|
||||
case "boolean":
|
||||
case "object":
|
||||
case "string":
|
||||
return value as string;
|
||||
default:
|
||||
return String(value);
|
||||
}
|
||||
},
|
||||
4,
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("JSON stringify error", error);
|
||||
return "Error: JSON.stringify failed";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export const VERSION = "v1.17.2";
|
||||
+19
-1
@@ -33,12 +33,21 @@ export const env = createEnv({
|
||||
required_error:
|
||||
"A strong Salt is required to encrypt API keys securely. See: https://langfuse.com/docs/deployment/self-host#deploy-the-container",
|
||||
}),
|
||||
// Add newly signed up users to default project with role
|
||||
LANGFUSE_DEFAULT_PROJECT_ID: z.string().optional(),
|
||||
LANGFUSE_DEFAULT_PROJECT_ROLE: z
|
||||
.enum(["ADMIN", "MEMBER", "VIEWER"])
|
||||
.optional(),
|
||||
// AUTH
|
||||
AUTH_GOOGLE_CLIENT_ID: z.string().optional(),
|
||||
AUTH_GOOGLE_CLIENT_SECRET: z.string().optional(),
|
||||
AUTH_GITHUB_CLIENT_ID: z.string().optional(),
|
||||
AUTH_GITHUB_CLIENT_SECRET: z.string().optional(),
|
||||
AUTH_DOMAINS_WITH_SSO_ENFORCEMENT: z.string().optional(),
|
||||
AUTH_DISABLE_USERNAME_PASSWORD: z.enum(["true", "false"]).optional(),
|
||||
// EMAIL
|
||||
EMAIL_FROM_ADDRESS: z.string().optional(),
|
||||
SMTP_CONNECTION_URL: z.string().optional(),
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -48,7 +57,9 @@ export const env = createEnv({
|
||||
*/
|
||||
client: {
|
||||
// NEXT_PUBLIC_CLIENTVAR: z.string().min(1),
|
||||
NEXT_PUBLIC_LANGFUSE_CLOUD_REGION: z.enum(["US", "EU"]).optional(),
|
||||
NEXT_PUBLIC_LANGFUSE_CLOUD_REGION: z
|
||||
.enum(["US", "EU", "STAGING"])
|
||||
.optional(),
|
||||
NEXT_PUBLIC_DEMO_PROJECT_ID: z.string().optional(),
|
||||
NEXT_PUBLIC_SIGN_UP_DISABLED: z.enum(["true", "false"]).optional(),
|
||||
},
|
||||
@@ -74,6 +85,9 @@ export const env = createEnv({
|
||||
LANGFUSE_NEW_USER_SIGNUP_WEBHOOK:
|
||||
process.env.LANGFUSE_NEW_USER_SIGNUP_WEBHOOK,
|
||||
SALT: process.env.SALT,
|
||||
// Default project and role
|
||||
LANGFUSE_DEFAULT_PROJECT_ID: process.env.LANGFUSE_DEFAULT_PROJECT_ID,
|
||||
LANGFUSE_DEFAULT_PROJECT_ROLE: process.env.LANGFUSE_DEFAULT_PROJECT_ROLE,
|
||||
// AUTH
|
||||
AUTH_GOOGLE_CLIENT_ID: process.env.AUTH_GOOGLE_CLIENT_ID,
|
||||
AUTH_GOOGLE_CLIENT_SECRET: process.env.AUTH_GOOGLE_CLIENT_SECRET,
|
||||
@@ -81,5 +95,9 @@ export const env = createEnv({
|
||||
AUTH_GITHUB_CLIENT_SECRET: process.env.AUTH_GITHUB_CLIENT_SECRET,
|
||||
AUTH_DOMAINS_WITH_SSO_ENFORCEMENT:
|
||||
process.env.AUTH_DOMAINS_WITH_SSO_ENFORCEMENT,
|
||||
AUTH_DISABLE_USERNAME_PASSWORD: process.env.AUTH_DISABLE_USERNAME_PASSWORD,
|
||||
// Email
|
||||
EMAIL_FROM_ADDRESS: process.env.EMAIL_FROM_ADDRESS,
|
||||
SMTP_CONNECTION_URL: process.env.SMTP_CONNECTION_URL,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import { env } from "@/src/env.mjs";
|
||||
|
||||
export const CloudPrivacyNotice = ({ action }: { action: string }) =>
|
||||
env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION !== undefined ? (
|
||||
<div className="mt-10 text-center text-xs text-gray-500">
|
||||
By {action} you are agreeing to our{" "}
|
||||
<a
|
||||
href="https://langfuse.com/tos"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="italic"
|
||||
>
|
||||
Terms of Service
|
||||
</a>
|
||||
,{" "}
|
||||
<a
|
||||
href="https://langfuse.com/privacy"
|
||||
rel="noopener noreferrer"
|
||||
className="italic"
|
||||
>
|
||||
Privacy Policy
|
||||
</a>
|
||||
, and{" "}
|
||||
<a
|
||||
href="https://langfuse.com/cookie-policy"
|
||||
rel="noopener noreferrer"
|
||||
className="italic"
|
||||
>
|
||||
Cookie Policy
|
||||
</a>
|
||||
. You also confirm that the entered data is accurate.
|
||||
</div>
|
||||
) : null;
|
||||
@@ -0,0 +1,83 @@
|
||||
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";
|
||||
|
||||
const regions =
|
||||
env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION === "STAGING"
|
||||
? [
|
||||
{
|
||||
name: "EU (Staging)",
|
||||
hostname: "staging.langfuse.com",
|
||||
flag: "🇪🇺",
|
||||
},
|
||||
]
|
||||
: [
|
||||
{
|
||||
name: "US",
|
||||
hostname: "us.cloud.langfuse.com",
|
||||
flag: "🇺🇸",
|
||||
},
|
||||
{
|
||||
name: "EU",
|
||||
hostname: "cloud.langfuse.com",
|
||||
flag: "🇪🇺",
|
||||
},
|
||||
];
|
||||
|
||||
export function CloudRegionSwitch({
|
||||
isSignUpPage,
|
||||
}: {
|
||||
isSignUpPage?: boolean;
|
||||
}) {
|
||||
const posthog = usePostHog();
|
||||
|
||||
if (env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION === undefined) return null;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex w-full flex-col justify-between gap-3 md:flex-row md:items-center">
|
||||
<div>
|
||||
<span className="text-sm font-medium leading-none">Data Region</span>
|
||||
<p className="text-xs text-gray-500">
|
||||
Regions are strictly separated.
|
||||
</p>
|
||||
{env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION !== undefined &&
|
||||
isSignUpPage ? (
|
||||
<p className="text-xs text-gray-500">
|
||||
{env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION === "EU"
|
||||
? "✅ Demo project available"
|
||||
: "❌ Choose EU for demo project access"}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
<Tabs value={env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION}>
|
||||
<TabsList>
|
||||
{regions.map((region) => (
|
||||
<TabsTrigger
|
||||
key={region.name}
|
||||
value={region.name}
|
||||
onClick={() => {
|
||||
posthog.capture(
|
||||
"cloud_region_switch",
|
||||
{
|
||||
region: region.name,
|
||||
},
|
||||
{
|
||||
send_instantly: true,
|
||||
},
|
||||
);
|
||||
window.location.hostname = region.hostname;
|
||||
}}
|
||||
>
|
||||
<span className="mr-2 text-xl leading-none">{region.flag}</span>
|
||||
{region.name}
|
||||
</TabsTrigger>
|
||||
))}
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
</div>
|
||||
<Divider className="text-gray-400" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
import { env } from "@/src/env.mjs";
|
||||
import { prisma } from "@/src/server/db";
|
||||
|
||||
export async function createProjectMembershipsOnSignup(user: {
|
||||
id: string;
|
||||
email: string | null;
|
||||
}) {
|
||||
try {
|
||||
// Langfuse Cloud: Demo project access
|
||||
const demoProjectId = env.NEXT_PUBLIC_DEMO_PROJECT_ID
|
||||
? (
|
||||
await prisma.project.findUnique({
|
||||
where: {
|
||||
id: env.NEXT_PUBLIC_DEMO_PROJECT_ID,
|
||||
},
|
||||
})
|
||||
)?.id
|
||||
: undefined;
|
||||
if (demoProjectId !== undefined) {
|
||||
await prisma.membership.create({
|
||||
data: {
|
||||
projectId: demoProjectId,
|
||||
userId: user.id,
|
||||
role: "VIEWER",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// set default project access
|
||||
const defaultProjectID = env.LANGFUSE_DEFAULT_PROJECT_ID
|
||||
? (
|
||||
await prisma.project.findUnique({
|
||||
where: {
|
||||
id: env.LANGFUSE_DEFAULT_PROJECT_ID,
|
||||
},
|
||||
})
|
||||
)?.id
|
||||
: undefined;
|
||||
if (defaultProjectID !== undefined) {
|
||||
await prisma.membership.create({
|
||||
data: {
|
||||
projectId: defaultProjectID,
|
||||
userId: user.id,
|
||||
role: env.LANGFUSE_DEFAULT_PROJECT_ROLE ?? "VIEWER",
|
||||
},
|
||||
});
|
||||
}
|
||||
// Invites do not work for users without emails (some future SSO users)
|
||||
if (user.email) await processMembershipInvitations(user.email, user.id);
|
||||
} catch (e) {
|
||||
console.error("Error assigning project access to new user", e);
|
||||
}
|
||||
}
|
||||
|
||||
async function processMembershipInvitations(email: string, userId: string) {
|
||||
const invitationsForUser = await prisma.membershipInvitation.findMany({
|
||||
where: {
|
||||
email: email.toLowerCase(),
|
||||
},
|
||||
});
|
||||
|
||||
if (invitationsForUser.length > 0) {
|
||||
const membershipsData = invitationsForUser.map((invitation) => {
|
||||
return {
|
||||
userId: userId,
|
||||
projectId: invitation.projectId,
|
||||
role: invitation.role,
|
||||
};
|
||||
});
|
||||
|
||||
await prisma.$transaction([
|
||||
prisma.membership.createMany({
|
||||
data: membershipsData,
|
||||
}),
|
||||
prisma.membershipInvitation.deleteMany({
|
||||
where: {
|
||||
id: {
|
||||
in: invitationsForUser.map((invitation) => invitation.id),
|
||||
},
|
||||
email: email.toLowerCase(),
|
||||
},
|
||||
}),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { env } from "@/src/env.mjs";
|
||||
import { createProjectMembershipsOnSignup } from "@/src/features/auth/lib/createProjectMembershipsOnSignup";
|
||||
import { prisma } from "@/src/server/db";
|
||||
import { compare, hash } from "bcryptjs";
|
||||
|
||||
@@ -30,35 +30,16 @@ export async function createUserEmailPassword(
|
||||
throw new Error("User with email already exists. Please sign in.");
|
||||
}
|
||||
|
||||
// set demoProjectId if env exists and project exists in db
|
||||
const demoProjectId = env.NEXT_PUBLIC_DEMO_PROJECT_ID
|
||||
? (
|
||||
await prisma.project.findUnique({
|
||||
where: {
|
||||
id: env.NEXT_PUBLIC_DEMO_PROJECT_ID,
|
||||
},
|
||||
})
|
||||
)?.id
|
||||
: undefined;
|
||||
|
||||
const newUser = await prisma.user.create({
|
||||
data: {
|
||||
email: email.toLowerCase(),
|
||||
password: hashedPassword,
|
||||
name,
|
||||
// if demo project id is set grant user access to it
|
||||
...(demoProjectId
|
||||
? {
|
||||
memberships: {
|
||||
create: {
|
||||
projectId: demoProjectId,
|
||||
role: "VIEWER",
|
||||
},
|
||||
},
|
||||
}
|
||||
: undefined),
|
||||
},
|
||||
});
|
||||
|
||||
await createProjectMembershipsOnSignup(newUser);
|
||||
|
||||
return newUser.id;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { type VisibilityState } from "@tanstack/react-table";
|
||||
import { type LangfuseColumnDef } from "@/src/components/table/types";
|
||||
|
||||
function useColumnVisibility<TData>(
|
||||
localStorageKey: string,
|
||||
columns: LangfuseColumnDef<TData>[],
|
||||
) {
|
||||
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>(
|
||||
() => {
|
||||
try {
|
||||
const savedVisibility = localStorage.getItem(localStorageKey);
|
||||
const visibilityState: VisibilityState = savedVisibility
|
||||
? (JSON.parse(savedVisibility) as VisibilityState)
|
||||
: {};
|
||||
// set default visibility for columns that are not in the saved state
|
||||
columns.forEach((column) => {
|
||||
if (
|
||||
"accessorKey" in column &&
|
||||
typeof column.accessorKey === "string"
|
||||
) {
|
||||
if (!(column.accessorKey in visibilityState)) {
|
||||
visibilityState[column.accessorKey] =
|
||||
column.defaultHidden === true ? false : true;
|
||||
}
|
||||
}
|
||||
});
|
||||
return visibilityState;
|
||||
} catch (e) {
|
||||
console.error("Error while loading saved column visibility", e);
|
||||
return {};
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const localStorageItem = localStorage.getItem(localStorageKey);
|
||||
|
||||
if (!localStorageItem || localStorageItem === "{}") {
|
||||
const initialVisibility: VisibilityState = {};
|
||||
columns.forEach((column) => {
|
||||
if ("accessorKey" in column && typeof column.accessorKey === "string") {
|
||||
initialVisibility[column.accessorKey] =
|
||||
column.defaultHidden === true ? false : true;
|
||||
}
|
||||
});
|
||||
setColumnVisibility(initialVisibility);
|
||||
} else {
|
||||
// make sure all columns are in the visibility state
|
||||
const visibilityState = JSON.parse(localStorageItem) as VisibilityState;
|
||||
columns.forEach((column) => {
|
||||
if ("accessorKey" in column && typeof column.accessorKey === "string") {
|
||||
if (!(column.accessorKey in visibilityState)) {
|
||||
visibilityState[column.accessorKey] =
|
||||
column.defaultHidden === true ? false : true;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [columns, localStorageKey]);
|
||||
|
||||
useEffect(() => {
|
||||
localStorage.setItem(localStorageKey, JSON.stringify(columnVisibility));
|
||||
}, [columnVisibility, localStorageKey]);
|
||||
|
||||
return [columnVisibility, setColumnVisibility] as const;
|
||||
}
|
||||
|
||||
export default useColumnVisibility;
|
||||
@@ -20,26 +20,35 @@ export function ChartScores(props: {
|
||||
globalFilterState: FilterState;
|
||||
projectId: string;
|
||||
}) {
|
||||
const scores = api.dashboard.chart.useQuery({
|
||||
projectId: props.projectId,
|
||||
from: "traces_scores",
|
||||
select: [{ column: "scoreName" }, { column: "value", agg: "AVG" }],
|
||||
filter:
|
||||
props.globalFilterState.map((f) =>
|
||||
f.type === "datetime" ? { ...f, column: "timestamp" } : f,
|
||||
) ?? [],
|
||||
groupBy: [
|
||||
{
|
||||
type: "datetime",
|
||||
column: "timestamp",
|
||||
temporalUnit: dateTimeAggregationSettings[props.agg].date_trunc,
|
||||
const scores = api.dashboard.chart.useQuery(
|
||||
{
|
||||
projectId: props.projectId,
|
||||
from: "traces_scores",
|
||||
select: [{ column: "scoreName" }, { column: "value", agg: "AVG" }],
|
||||
filter:
|
||||
props.globalFilterState.map((f) =>
|
||||
f.type === "datetime" ? { ...f, column: "timestamp" } : f,
|
||||
) ?? [],
|
||||
groupBy: [
|
||||
{
|
||||
type: "datetime",
|
||||
column: "timestamp",
|
||||
temporalUnit: dateTimeAggregationSettings[props.agg].date_trunc,
|
||||
},
|
||||
{
|
||||
type: "string",
|
||||
column: "scoreName",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
trpc: {
|
||||
context: {
|
||||
skipBatch: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "string",
|
||||
column: "scoreName",
|
||||
},
|
||||
],
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
const extractedScores = scores.data
|
||||
? fillMissingValuesAndTransform(
|
||||
@@ -69,7 +78,7 @@ export function ChartScores(props: {
|
||||
<NoData noDataText="No data">
|
||||
<DocPopup
|
||||
description="Scores evaluate LLM quality and can be created manually or using the SDK."
|
||||
link="https://langfuse.com/docs/scores"
|
||||
href="https://langfuse.com/docs/scores"
|
||||
/>
|
||||
</NoData>
|
||||
)}
|
||||
|
||||
@@ -27,30 +27,44 @@ export const LatencyChart = ({
|
||||
globalFilterState: FilterState;
|
||||
agg: DateTimeAggregationOption;
|
||||
}) => {
|
||||
const latencies = api.dashboard.chart.useQuery({
|
||||
projectId,
|
||||
from: "observations",
|
||||
select: [
|
||||
{ column: "duration", agg: "50thPercentile" },
|
||||
{ column: "duration", agg: "90thPercentile" },
|
||||
{ column: "duration", agg: "95thPercentile" },
|
||||
{ column: "duration", agg: "99thPercentile" },
|
||||
{ column: "model" },
|
||||
],
|
||||
filter:
|
||||
[
|
||||
...globalFilterState,
|
||||
{ type: "string", column: "type", operator: "=", value: "GENERATION" },
|
||||
] ?? [],
|
||||
groupBy: [
|
||||
{
|
||||
type: "datetime",
|
||||
column: "startTime",
|
||||
temporalUnit: dateTimeAggregationSettings[agg].date_trunc,
|
||||
const latencies = api.dashboard.chart.useQuery(
|
||||
{
|
||||
projectId,
|
||||
from: "observations",
|
||||
select: [
|
||||
{ column: "duration", agg: "50thPercentile" },
|
||||
{ column: "duration", agg: "90thPercentile" },
|
||||
{ column: "duration", agg: "95thPercentile" },
|
||||
{ column: "duration", agg: "99thPercentile" },
|
||||
{ column: "model" },
|
||||
],
|
||||
filter:
|
||||
[
|
||||
...globalFilterState,
|
||||
{
|
||||
type: "string",
|
||||
column: "type",
|
||||
operator: "=",
|
||||
value: "GENERATION",
|
||||
},
|
||||
] ?? [],
|
||||
groupBy: [
|
||||
{
|
||||
type: "datetime",
|
||||
column: "startTime",
|
||||
temporalUnit: dateTimeAggregationSettings[agg].date_trunc,
|
||||
},
|
||||
{ type: "string", column: "model" },
|
||||
],
|
||||
},
|
||||
{
|
||||
trpc: {
|
||||
context: {
|
||||
skipBatch: true,
|
||||
},
|
||||
},
|
||||
{ type: "string", column: "model" },
|
||||
],
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
const allModels = getAllModels(projectId, globalFilterState);
|
||||
|
||||
|
||||
@@ -21,18 +21,27 @@ export const MetricTable = ({
|
||||
column: "timestamp",
|
||||
}));
|
||||
|
||||
const metrics = api.dashboard.chart.useQuery({
|
||||
projectId,
|
||||
from: "traces_observations",
|
||||
select: [
|
||||
{ column: "totalTokenCost" },
|
||||
{ column: "totalTokens", agg: "SUM" },
|
||||
{ column: "model" },
|
||||
],
|
||||
filter: localFilters ?? [],
|
||||
groupBy: [{ type: "string", column: "model" }],
|
||||
orderBy: [{ column: "totalTokenCost", direction: "DESC" }],
|
||||
});
|
||||
const metrics = api.dashboard.chart.useQuery(
|
||||
{
|
||||
projectId,
|
||||
from: "traces_observations",
|
||||
select: [
|
||||
{ column: "totalTokenCost" },
|
||||
{ column: "totalTokens", agg: "SUM" },
|
||||
{ column: "model" },
|
||||
],
|
||||
filter: localFilters ?? [],
|
||||
groupBy: [{ type: "string", column: "model" }],
|
||||
orderBy: [{ column: "totalTokenCost", direction: "DESC" }],
|
||||
},
|
||||
{
|
||||
trpc: {
|
||||
context: {
|
||||
skipBatch: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const totalTokens = metrics.data?.reduce(
|
||||
(acc, curr) =>
|
||||
@@ -79,7 +88,7 @@ export const MetricTable = ({
|
||||
>
|
||||
<DocPopup
|
||||
description="Calculated multiplying the number of tokens with cost per token for each model."
|
||||
link="https://langfuse.com/docs/token-usage"
|
||||
href="https://langfuse.com/docs/token-usage"
|
||||
/>
|
||||
</TotalMetric>
|
||||
</DashboardTable>
|
||||
|
||||
@@ -30,28 +30,37 @@ export const ModelUsageChart = ({
|
||||
globalFilterState: FilterState;
|
||||
agg: DateTimeAggregationOption;
|
||||
}) => {
|
||||
const tokens = api.dashboard.chart.useQuery({
|
||||
projectId,
|
||||
from: "observations",
|
||||
select: [
|
||||
{ column: "totalTokens", agg: "SUM" },
|
||||
{ column: "totalTokenCost" },
|
||||
{ column: "model" },
|
||||
],
|
||||
filter: globalFilterState ?? [],
|
||||
groupBy: [
|
||||
{
|
||||
type: "datetime",
|
||||
column: "startTime",
|
||||
temporalUnit: dateTimeAggregationSettings[agg].date_trunc,
|
||||
const tokens = api.dashboard.chart.useQuery(
|
||||
{
|
||||
projectId,
|
||||
from: "observations",
|
||||
select: [
|
||||
{ column: "totalTokens", agg: "SUM" },
|
||||
{ column: "totalTokenCost" },
|
||||
{ column: "model" },
|
||||
],
|
||||
filter: globalFilterState ?? [],
|
||||
groupBy: [
|
||||
{
|
||||
type: "datetime",
|
||||
column: "startTime",
|
||||
temporalUnit: dateTimeAggregationSettings[agg].date_trunc,
|
||||
},
|
||||
{
|
||||
type: "string",
|
||||
column: "model",
|
||||
},
|
||||
],
|
||||
orderBy: [{ column: "totalTokenCost", direction: "DESC" }],
|
||||
},
|
||||
{
|
||||
trpc: {
|
||||
context: {
|
||||
skipBatch: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "string",
|
||||
column: "model",
|
||||
},
|
||||
],
|
||||
orderBy: [{ column: "totalTokenCost", direction: "DESC" }],
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
const allModels = getAllModels(projectId, globalFilterState);
|
||||
|
||||
|
||||
@@ -22,37 +22,55 @@ export const ScoresTable = ({
|
||||
column: "timestamp",
|
||||
}));
|
||||
|
||||
const metrics = api.dashboard.chart.useQuery({
|
||||
projectId,
|
||||
from: "traces_scores",
|
||||
select: [
|
||||
{ column: "scoreName" },
|
||||
{ column: "scoreId", agg: "COUNT" },
|
||||
{ column: "value", agg: "AVG" },
|
||||
],
|
||||
filter: localFilters ?? [],
|
||||
groupBy: [{ type: "string", column: "scoreName" }],
|
||||
orderBy: [{ column: "scoreId", direction: "DESC", agg: "COUNT" }],
|
||||
});
|
||||
|
||||
const [zeroValueScores, oneValueScores] = [0, 1].map((i) =>
|
||||
api.dashboard.chart.useQuery({
|
||||
const metrics = api.dashboard.chart.useQuery(
|
||||
{
|
||||
projectId,
|
||||
from: "traces_scores",
|
||||
select: [{ column: "scoreName" }, { column: "scoreId", agg: "COUNT" }],
|
||||
filter:
|
||||
[
|
||||
...localFilters,
|
||||
{
|
||||
column: "value",
|
||||
operator: "=",
|
||||
value: i,
|
||||
type: "number",
|
||||
},
|
||||
] ?? [],
|
||||
select: [
|
||||
{ column: "scoreName" },
|
||||
{ column: "scoreId", agg: "COUNT" },
|
||||
{ column: "value", agg: "AVG" },
|
||||
],
|
||||
filter: localFilters ?? [],
|
||||
groupBy: [{ type: "string", column: "scoreName" }],
|
||||
orderBy: [{ column: "scoreId", direction: "DESC", agg: "COUNT" }],
|
||||
}),
|
||||
},
|
||||
{
|
||||
trpc: {
|
||||
context: {
|
||||
skipBatch: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const [zeroValueScores, oneValueScores] = [0, 1].map((i) =>
|
||||
api.dashboard.chart.useQuery(
|
||||
{
|
||||
projectId,
|
||||
from: "traces_scores",
|
||||
select: [{ column: "scoreName" }, { column: "scoreId", agg: "COUNT" }],
|
||||
filter:
|
||||
[
|
||||
...localFilters,
|
||||
{
|
||||
column: "value",
|
||||
operator: "=",
|
||||
value: i,
|
||||
type: "number",
|
||||
},
|
||||
] ?? [],
|
||||
groupBy: [{ type: "string", column: "scoreName" }],
|
||||
orderBy: [{ column: "scoreId", direction: "DESC", agg: "COUNT" }],
|
||||
},
|
||||
{
|
||||
trpc: {
|
||||
context: {
|
||||
skipBatch: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
if (!zeroValueScores || !oneValueScores) {
|
||||
@@ -137,7 +155,7 @@ export const ScoresTable = ({
|
||||
noDataChildren={
|
||||
<DocPopup
|
||||
description="Scores evaluate LLM quality and can be created manually or using the SDK."
|
||||
link="https://langfuse.com/docs/scores"
|
||||
href="https://langfuse.com/docs/scores"
|
||||
/>
|
||||
}
|
||||
noDataClassName="mt-0"
|
||||
|
||||
@@ -23,21 +23,39 @@ export const TracesBarListChart = ({
|
||||
f.type === "datetime" ? { ...f, column: "timestamp" } : f,
|
||||
) ?? [];
|
||||
|
||||
const totalTraces = api.dashboard.chart.useQuery({
|
||||
projectId,
|
||||
from: "traces",
|
||||
select: [{ column: "traceId", agg: "COUNT" }],
|
||||
filter: timeFilter,
|
||||
});
|
||||
const totalTraces = api.dashboard.chart.useQuery(
|
||||
{
|
||||
projectId,
|
||||
from: "traces",
|
||||
select: [{ column: "traceId", agg: "COUNT" }],
|
||||
filter: timeFilter,
|
||||
},
|
||||
{
|
||||
trpc: {
|
||||
context: {
|
||||
skipBatch: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const traces = api.dashboard.chart.useQuery({
|
||||
projectId,
|
||||
from: "traces",
|
||||
select: [{ column: "traceId", agg: "COUNT" }, { column: "traceName" }],
|
||||
filter: timeFilter,
|
||||
groupBy: [{ column: "traceName", type: "string" }],
|
||||
orderBy: [{ column: "traceId", direction: "DESC", agg: "COUNT" }],
|
||||
});
|
||||
const traces = api.dashboard.chart.useQuery(
|
||||
{
|
||||
projectId,
|
||||
from: "traces",
|
||||
select: [{ column: "traceId", agg: "COUNT" }, { column: "traceName" }],
|
||||
filter: timeFilter,
|
||||
groupBy: [{ column: "traceName", type: "string" }],
|
||||
orderBy: [{ column: "traceId", direction: "DESC", agg: "COUNT" }],
|
||||
},
|
||||
{
|
||||
trpc: {
|
||||
context: {
|
||||
skipBatch: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const transformedTraces = traces.data
|
||||
? traces.data.map((item) => {
|
||||
@@ -82,7 +100,7 @@ export const TracesBarListChart = ({
|
||||
<NoData noDataText="No data">
|
||||
<DocPopup
|
||||
description="Traces contain details about LLM applications and can be created using the SDK."
|
||||
link="https://langfuse.com/docs/integrations/sdk#1-backend-tracing"
|
||||
href="https://langfuse.com/docs/integrations/sdk#1-backend-tracing"
|
||||
/>
|
||||
</NoData>
|
||||
)}
|
||||
|
||||
@@ -23,22 +23,31 @@ export const TracesTimeSeriesChart = ({
|
||||
globalFilterState: FilterState;
|
||||
agg: DateTimeAggregationOption;
|
||||
}) => {
|
||||
const traces = api.dashboard.chart.useQuery({
|
||||
projectId,
|
||||
from: "traces",
|
||||
select: [{ column: "traceId", agg: "COUNT" }],
|
||||
filter:
|
||||
globalFilterState.map((f) =>
|
||||
f.type === "datetime" ? { ...f, column: "timestamp" } : f,
|
||||
) ?? [],
|
||||
groupBy: [
|
||||
{
|
||||
type: "datetime",
|
||||
column: "timestamp",
|
||||
temporalUnit: dateTimeAggregationSettings[agg].date_trunc,
|
||||
const traces = api.dashboard.chart.useQuery(
|
||||
{
|
||||
projectId,
|
||||
from: "traces",
|
||||
select: [{ column: "traceId", agg: "COUNT" }],
|
||||
filter:
|
||||
globalFilterState.map((f) =>
|
||||
f.type === "datetime" ? { ...f, column: "timestamp" } : f,
|
||||
) ?? [],
|
||||
groupBy: [
|
||||
{
|
||||
type: "datetime",
|
||||
column: "timestamp",
|
||||
temporalUnit: dateTimeAggregationSettings[agg].date_trunc,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
trpc: {
|
||||
context: {
|
||||
skipBatch: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
const transformedTraces = traces.data
|
||||
? traces.data.map((item) => {
|
||||
@@ -85,7 +94,7 @@ export const TracesTimeSeriesChart = ({
|
||||
<NoData noDataText="No data available">
|
||||
<DocPopup
|
||||
description="Traces contain details about LLM applications and can be created using the SDK."
|
||||
link="https://langfuse.com/docs/integrations/sdk#1-backend-tracing"
|
||||
href="https://langfuse.com/docs/integrations/sdk#1-backend-tracing"
|
||||
/>
|
||||
</NoData>
|
||||
)}
|
||||
|
||||
@@ -5,7 +5,6 @@ import { DashboardCard } from "@/src/features/dashboard/components/cards/Dashboa
|
||||
import { compactNumberFormatter, usdFormatter } from "@/src/utils/numbers";
|
||||
import { TabComponent } from "@/src/features/dashboard/components/TabsComponent";
|
||||
import { BarList } from "@tremor/react";
|
||||
import { isUndefinedOrNull } from "@/src/utils/types";
|
||||
import { TotalMetric } from "@/src/features/dashboard/components/TotalMetric";
|
||||
import { ExpandListButton } from "@/src/features/dashboard/components/cards/ChevronButton";
|
||||
import { useState } from "react";
|
||||
@@ -28,41 +27,59 @@ export const UserChart = ({
|
||||
agg: DateTimeAggregationOption;
|
||||
}) => {
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
const user = api.dashboard.chart.useQuery({
|
||||
projectId,
|
||||
from: "traces_observations",
|
||||
select: [
|
||||
{ column: "totalTokenCost" },
|
||||
{ column: "user" },
|
||||
{ column: "traceId", agg: "COUNT" },
|
||||
],
|
||||
filter: globalFilterState ?? [],
|
||||
groupBy: [
|
||||
{
|
||||
type: "string",
|
||||
column: "user",
|
||||
const user = api.dashboard.chart.useQuery(
|
||||
{
|
||||
projectId,
|
||||
from: "traces_observations",
|
||||
select: [
|
||||
{ column: "totalTokenCost" },
|
||||
{ column: "user" },
|
||||
{ column: "traceId", agg: "COUNT" },
|
||||
],
|
||||
filter: globalFilterState ?? [],
|
||||
groupBy: [
|
||||
{
|
||||
type: "string",
|
||||
column: "user",
|
||||
},
|
||||
],
|
||||
orderBy: [{ column: "totalTokenCost", direction: "DESC" }],
|
||||
},
|
||||
{
|
||||
trpc: {
|
||||
context: {
|
||||
skipBatch: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
orderBy: [{ column: "totalTokenCost", direction: "DESC" }],
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
const traces = api.dashboard.chart.useQuery({
|
||||
projectId,
|
||||
from: "traces",
|
||||
select: [{ column: "user" }, { column: "traceId", agg: "COUNT" }],
|
||||
filter:
|
||||
globalFilterState.map((f) => ({
|
||||
...f,
|
||||
column: "timestamp",
|
||||
})) ?? [],
|
||||
groupBy: [
|
||||
{
|
||||
type: "string",
|
||||
column: "user",
|
||||
const traces = api.dashboard.chart.useQuery(
|
||||
{
|
||||
projectId,
|
||||
from: "traces",
|
||||
select: [{ column: "user" }, { column: "traceId", agg: "COUNT" }],
|
||||
filter:
|
||||
globalFilterState.map((f) => ({
|
||||
...f,
|
||||
column: "timestamp",
|
||||
})) ?? [],
|
||||
groupBy: [
|
||||
{
|
||||
type: "string",
|
||||
column: "user",
|
||||
},
|
||||
],
|
||||
orderBy: [{ column: "traceId", agg: "COUNT", direction: "DESC" }],
|
||||
},
|
||||
{
|
||||
trpc: {
|
||||
context: {
|
||||
skipBatch: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
orderBy: [{ column: "traceId", agg: "COUNT", direction: "DESC" }],
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
const transformedNumberOfTraces: BarChartDataPoint[] = traces.data
|
||||
? traces.data
|
||||
@@ -84,7 +101,6 @@ export const UserChart = ({
|
||||
value: item.totalTokenCost ? (item.totalTokenCost as number) : 0,
|
||||
};
|
||||
})
|
||||
.filter((i) => (isUndefinedOrNull(i.name) ? true : false))
|
||||
: [];
|
||||
|
||||
const totalCost = user.data?.reduce(
|
||||
@@ -151,7 +167,7 @@ export const UserChart = ({
|
||||
<NoData noDataText="No data">
|
||||
<DocPopup
|
||||
description="Consumption per user is tracked by passing their ids on traces."
|
||||
link="https://langfuse.com/docs/user-explorer"
|
||||
href="https://langfuse.com/docs/user-explorer"
|
||||
/>
|
||||
</NoData>
|
||||
)}
|
||||
|
||||
@@ -7,17 +7,31 @@ export const getAllModels = (
|
||||
projectId: string,
|
||||
globalFilterState: FilterState,
|
||||
) => {
|
||||
const allModels = api.dashboard.chart.useQuery({
|
||||
projectId,
|
||||
from: "observations",
|
||||
select: [{ column: "model" }],
|
||||
filter:
|
||||
[
|
||||
...globalFilterState,
|
||||
{ type: "string", column: "type", operator: "=", value: "GENERATION" },
|
||||
] ?? [],
|
||||
groupBy: [{ type: "string", column: "model" }],
|
||||
});
|
||||
const allModels = api.dashboard.chart.useQuery(
|
||||
{
|
||||
projectId,
|
||||
from: "observations",
|
||||
select: [{ column: "model" }],
|
||||
filter:
|
||||
[
|
||||
...globalFilterState,
|
||||
{
|
||||
type: "string",
|
||||
column: "type",
|
||||
operator: "=",
|
||||
value: "GENERATION",
|
||||
},
|
||||
] ?? [],
|
||||
groupBy: [{ type: "string", column: "model" }],
|
||||
},
|
||||
{
|
||||
trpc: {
|
||||
context: {
|
||||
skipBatch: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
return allModels.data ? extractAllModels(allModels.data) : [];
|
||||
};
|
||||
|
||||
@@ -3,7 +3,6 @@ import TableLink from "@/src/components/table/table-link";
|
||||
import { NewDatasetItemButton } from "@/src/features/datasets/components/NewDatasetItemButton";
|
||||
import { api } from "@/src/utils/api";
|
||||
import { type RouterOutput } from "@/src/utils/types";
|
||||
import { type ColumnDef } from "@tanstack/react-table";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@@ -15,6 +14,7 @@ import { Archive, MoreVertical } from "lucide-react";
|
||||
import { Button } from "@/src/components/ui/button";
|
||||
import { DatasetStatus, type DatasetItem } from "@prisma/client";
|
||||
import { cn } from "@/src/utils/tailwind";
|
||||
import { type LangfuseColumnDef } from "@/src/components/table/types";
|
||||
|
||||
type RowData = {
|
||||
id: string;
|
||||
@@ -31,7 +31,7 @@ export function DatasetItemsTable({
|
||||
projectId: string;
|
||||
datasetId: string;
|
||||
}) {
|
||||
const utils = api.useContext();
|
||||
const utils = api.useUtils();
|
||||
const items = api.datasets.itemsByDatasetId.useQuery({
|
||||
projectId,
|
||||
datasetId,
|
||||
@@ -41,7 +41,7 @@ export function DatasetItemsTable({
|
||||
onSuccess: () => utils.datasets.invalidate(),
|
||||
});
|
||||
|
||||
const columns: ColumnDef<RowData>[] = [
|
||||
const columns: LangfuseColumnDef<RowData>[] = [
|
||||
{
|
||||
accessorKey: "id",
|
||||
header: "Item id",
|
||||
@@ -156,16 +156,16 @@ export function DatasetItemsTable({
|
||||
items.isLoading
|
||||
? { isLoading: true, isError: false }
|
||||
: items.isError
|
||||
? {
|
||||
isLoading: false,
|
||||
isError: true,
|
||||
error: items.error.message,
|
||||
}
|
||||
: {
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
data: items.data?.map((t) => convertToTableRow(t)),
|
||||
}
|
||||
? {
|
||||
isLoading: false,
|
||||
isError: true,
|
||||
error: items.error.message,
|
||||
}
|
||||
: {
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
data: items.data?.map((t) => convertToTableRow(t)),
|
||||
}
|
||||
}
|
||||
/>
|
||||
<NewDatasetItemButton
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
import { GroupedScoreBadges } from "@/src/components/grouped-score-badge";
|
||||
import { DataTable } from "@/src/components/table/data-table";
|
||||
import TableLink from "@/src/components/table/table-link";
|
||||
import { type LangfuseColumnDef } from "@/src/components/table/types";
|
||||
import { api } from "@/src/utils/api";
|
||||
import { intervalInSeconds } from "@/src/utils/dates";
|
||||
import { formatInterval, intervalInSeconds } from "@/src/utils/dates";
|
||||
import { type RouterOutput } from "@/src/utils/types";
|
||||
import { type ColumnDef } from "@tanstack/react-table";
|
||||
import { type Score } from "@prisma/client";
|
||||
|
||||
type RowData = {
|
||||
id: string;
|
||||
runAt: string;
|
||||
datasetItemId: string;
|
||||
observation: { id: string; traceId: string };
|
||||
scores: { name: string; value: number }[];
|
||||
scores: Score[];
|
||||
latency: number;
|
||||
};
|
||||
|
||||
@@ -30,7 +31,7 @@ export function DatasetRunItemsTable(
|
||||
) {
|
||||
const runItems = api.datasets.runitemsByRunIdOrItemId.useQuery(props);
|
||||
|
||||
const columns: ColumnDef<RowData>[] = [
|
||||
const columns: LangfuseColumnDef<RowData>[] = [
|
||||
{
|
||||
accessorKey: "runAt",
|
||||
header: "Run At",
|
||||
@@ -68,7 +69,7 @@ export function DatasetRunItemsTable(
|
||||
header: "Latency",
|
||||
cell: ({ row }) => {
|
||||
const latency: RowData["latency"] = row.getValue("latency");
|
||||
return <>{latency.toFixed(2)} sec</>;
|
||||
return <>{formatInterval(latency)}</>;
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -92,10 +93,7 @@ export function DatasetRunItemsTable(
|
||||
id: item.observation.id,
|
||||
traceId: item.observation.traceId ?? "", // never actually null, just not enforced by db
|
||||
},
|
||||
scores: item.observation.scores.map((score) => ({
|
||||
name: score.name,
|
||||
value: score.value,
|
||||
})),
|
||||
scores: item.observation.scores,
|
||||
latency: intervalInSeconds(
|
||||
item.observation.startTime,
|
||||
item.observation.endTime,
|
||||
@@ -110,16 +108,16 @@ export function DatasetRunItemsTable(
|
||||
runItems.isLoading
|
||||
? { isLoading: true, isError: false }
|
||||
: runItems.isError
|
||||
? {
|
||||
isLoading: false,
|
||||
isError: true,
|
||||
error: runItems.error.message,
|
||||
}
|
||||
: {
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
data: runItems.data?.map((t) => convertToTableRow(t)),
|
||||
}
|
||||
? {
|
||||
isLoading: false,
|
||||
isError: true,
|
||||
error: runItems.error.message,
|
||||
}
|
||||
: {
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
data: runItems.data?.map((t) => convertToTableRow(t)),
|
||||
}
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { GroupedScoreBadges } from "@/src/components/grouped-score-badge";
|
||||
import { DataTable } from "@/src/components/table/data-table";
|
||||
import TableLink from "@/src/components/table/table-link";
|
||||
import { type LangfuseColumnDef } from "@/src/components/table/types";
|
||||
import { api } from "@/src/utils/api";
|
||||
import { formatInterval } from "@/src/utils/dates";
|
||||
import { type RouterOutput } from "@/src/utils/types";
|
||||
import { type ColumnDef } from "@tanstack/react-table";
|
||||
|
||||
type RowData = {
|
||||
key: {
|
||||
@@ -25,7 +26,7 @@ export function DatasetRunsTable(props: {
|
||||
datasetId: props.datasetId,
|
||||
});
|
||||
|
||||
const columns: ColumnDef<RowData>[] = [
|
||||
const columns: LangfuseColumnDef<RowData>[] = [
|
||||
{
|
||||
accessorKey: "key",
|
||||
header: "Name",
|
||||
@@ -53,7 +54,7 @@ export function DatasetRunsTable(props: {
|
||||
header: "Latency (avg)",
|
||||
cell: ({ row }) => {
|
||||
const avgLatency: RowData["avgLatency"] = row.getValue("avgLatency");
|
||||
return <>{avgLatency.toFixed(2)} sec</>;
|
||||
return <>{formatInterval(avgLatency)}</>;
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -93,16 +94,16 @@ export function DatasetRunsTable(props: {
|
||||
runs.isLoading
|
||||
? { isLoading: true, isError: false }
|
||||
: runs.isError
|
||||
? {
|
||||
isLoading: false,
|
||||
isError: true,
|
||||
error: runs.error.message,
|
||||
}
|
||||
: {
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
data: runs.data?.map((t) => convertToTableRow(t)),
|
||||
}
|
||||
? {
|
||||
isLoading: false,
|
||||
isError: true,
|
||||
error: runs.error.message,
|
||||
}
|
||||
: {
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
data: runs.data?.map((t) => convertToTableRow(t)),
|
||||
}
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { DataTable } from "@/src/components/table/data-table";
|
||||
import TableLink from "@/src/components/table/table-link";
|
||||
import { type LangfuseColumnDef } from "@/src/components/table/types";
|
||||
import { Button } from "@/src/components/ui/button";
|
||||
import {
|
||||
DropdownMenu,
|
||||
@@ -13,7 +14,6 @@ import { api } from "@/src/utils/api";
|
||||
import { cn } from "@/src/utils/tailwind";
|
||||
import { type RouterOutput } from "@/src/utils/types";
|
||||
import { DatasetStatus } from "@prisma/client";
|
||||
import { type ColumnDef } from "@tanstack/react-table";
|
||||
import { Archive, MoreVertical } from "lucide-react";
|
||||
|
||||
type RowData = {
|
||||
@@ -29,7 +29,7 @@ type RowData = {
|
||||
};
|
||||
|
||||
export function DatasetsTable(props: { projectId: string }) {
|
||||
const utils = api.useContext();
|
||||
const utils = api.useUtils();
|
||||
const datasets = api.datasets.allDatasets.useQuery({
|
||||
projectId: props.projectId,
|
||||
});
|
||||
@@ -37,7 +37,7 @@ export function DatasetsTable(props: { projectId: string }) {
|
||||
onSuccess: () => utils.datasets.invalidate(),
|
||||
});
|
||||
|
||||
const columns: ColumnDef<RowData>[] = [
|
||||
const columns: LangfuseColumnDef<RowData>[] = [
|
||||
{
|
||||
accessorKey: "key",
|
||||
header: "Name",
|
||||
@@ -146,16 +146,16 @@ export function DatasetsTable(props: { projectId: string }) {
|
||||
datasets.isLoading
|
||||
? { isLoading: true, isError: false }
|
||||
: datasets.isError
|
||||
? {
|
||||
isLoading: false,
|
||||
isError: true,
|
||||
error: datasets.error.message,
|
||||
}
|
||||
: {
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
data: datasets.data?.map((t) => convertToTableRow(t)),
|
||||
}
|
||||
? {
|
||||
isLoading: false,
|
||||
isError: true,
|
||||
error: datasets.error.message,
|
||||
}
|
||||
: {
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
data: datasets.data?.map((t) => convertToTableRow(t)),
|
||||
}
|
||||
}
|
||||
/>
|
||||
<NewDatasetButton projectId={props.projectId} className="mt-4" />
|
||||
|
||||
@@ -56,7 +56,7 @@ export const EditDatasetItem = ({
|
||||
projectId: projectId,
|
||||
scope: "datasets:CUD",
|
||||
});
|
||||
const utils = api.useContext();
|
||||
const utils = api.useUtils();
|
||||
const item = api.datasets.itemById.useQuery({
|
||||
datasetId,
|
||||
projectId,
|
||||
|
||||
@@ -32,7 +32,7 @@ export const NewDatasetForm = (props: {
|
||||
},
|
||||
});
|
||||
|
||||
const utils = api.useContext();
|
||||
const utils = api.useUtils();
|
||||
const createDatasetMutation = api.datasets.createDataset.useMutation({
|
||||
onSuccess: () => utils.datasets.invalidate(),
|
||||
onError: (error) => setFormError(error.message),
|
||||
|
||||
@@ -83,7 +83,7 @@ export const NewDatasetItemForm = (props: {
|
||||
projectId: props.projectId,
|
||||
});
|
||||
|
||||
const utils = api.useContext();
|
||||
const utils = api.useUtils();
|
||||
const createDatasetItemMutation = api.datasets.createDatasetItem.useMutation({
|
||||
onSuccess: () => utils.datasets.invalidate(),
|
||||
onError: (error) => setFormError(error.message),
|
||||
|
||||
@@ -20,6 +20,7 @@ import Link from "next/link";
|
||||
import { NewDatasetItemForm } from "@/src/features/datasets/components/NewDatasetItemForm";
|
||||
import { type Prisma } from "@prisma/client";
|
||||
import { useHasAccess } from "@/src/features/rbac/utils/checkAccess";
|
||||
import { useSession } from "next-auth/react";
|
||||
|
||||
export const NewDatasetItemFromObservationButton = (props: {
|
||||
projectId: string;
|
||||
@@ -28,10 +29,16 @@ export const NewDatasetItemFromObservationButton = (props: {
|
||||
observationOutput: Prisma.JsonValue;
|
||||
}) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const observationInDatasets = api.datasets.observationInDatasets.useQuery({
|
||||
projectId: props.projectId,
|
||||
observationId: props.observationId,
|
||||
});
|
||||
const session = useSession();
|
||||
const observationInDatasets = api.datasets.observationInDatasets.useQuery(
|
||||
{
|
||||
projectId: props.projectId,
|
||||
observationId: props.observationId,
|
||||
},
|
||||
{
|
||||
enabled: session.status === "authenticated",
|
||||
},
|
||||
);
|
||||
const hasAccess = useHasAccess({
|
||||
projectId: props.projectId,
|
||||
scope: "datasets:CUD",
|
||||
@@ -91,7 +98,7 @@ export const NewDatasetItemFromObservationButton = (props: {
|
||||
</Button>
|
||||
)}
|
||||
<Dialog open={hasAccess && open} onOpenChange={setOpen}>
|
||||
<DialogContent className="sm:w-3xl lg:h-[calc(100vh-100px)] lg:w-[calc(100vw-100px)] lg:max-w-none">
|
||||
<DialogContent className="sm:w-3xl lg:max-w-none">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="mb-5">Add to dataset</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user