Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
@@ -16,10 +16,8 @@ updates:
|
||||
include: scope
|
||||
ignore:
|
||||
- dependency-name: "@types/node"
|
||||
- dependency-name: "@trpc/*"
|
||||
groups:
|
||||
trpc:
|
||||
patterns:
|
||||
- "@trpc/*"
|
||||
prisma:
|
||||
patterns:
|
||||
- "prisma"
|
||||
|
||||
@@ -11,6 +11,7 @@ permissions:
|
||||
jobs:
|
||||
dependabot:
|
||||
runs-on: ubuntu-latest
|
||||
environment: "protected branches"
|
||||
if: ${{ github.actor == 'dependabot[bot]' }}
|
||||
steps:
|
||||
- name: Dependabot metadata
|
||||
|
||||
@@ -9,6 +9,7 @@ on:
|
||||
jobs:
|
||||
rebase-dependabot:
|
||||
runs-on: ubuntu-latest
|
||||
environment: "protected branches"
|
||||
steps:
|
||||
- name: "Rebase open Dependabot PR"
|
||||
uses: orange-buffalo/dependabot-auto-rebase@v1
|
||||
|
||||
@@ -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,20 @@
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
# Pattern matched against refs/tags
|
||||
tags:
|
||||
- "v*" # Push events to every tag not containing /
|
||||
|
||||
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
|
||||
if: github.ref == 'refs/heads/main'
|
||||
+62
-30
@@ -5,16 +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
|
||||
@@ -27,45 +27,77 @@ 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
|
||||
## Development Setup
|
||||
|
||||
First, the preferred version for development is Node.js >=18. Additionally, you should have npm and Docker installed
|
||||
Requirements
|
||||
|
||||
Then, to begin development fork repository and go to root folder by run `cd ./langfuse` .
|
||||
- Node.js 20 as specified in the [.nvmrc](.nvmrc)
|
||||
- Docker to run the database locally
|
||||
|
||||
```bash
|
||||
# Install dependencies
|
||||
npm install
|
||||
**Steps**
|
||||
|
||||
# Run the db
|
||||
docker-compose -f docker-compose.dev.yml up -d
|
||||
1. Fork the the repository and clone it locally
|
||||
2. Install dependencies
|
||||
|
||||
# create an env file
|
||||
cp .env.dev.example .env
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
# Migration
|
||||
npm run db:migrate
|
||||
3. Run the development database
|
||||
|
||||
# Optional: seed the database
|
||||
# npm run db:seed
|
||||
# npm run db:seed:examples
|
||||
```bash
|
||||
docker-compose -f docker-compose.dev.yml up -d
|
||||
```
|
||||
|
||||
# Start the server
|
||||
npm run dev
|
||||
```
|
||||
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.
|
||||
|
||||
## 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
|
||||
|
||||
## 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.
|
||||
|
||||
## License
|
||||
|
||||
Langfuse is MIT licensed, except for `ee/` folder. See [LICENSE](LICENSE) and [docs](https://langfuse.com/docs/open-source) for more details.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
@@ -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,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
|
||||
@@ -592,6 +592,45 @@ paths:
|
||||
application/json:
|
||||
schema: {}
|
||||
security: *ref_0
|
||||
/api/public/projects:
|
||||
get:
|
||||
operationId: projects_get
|
||||
tags:
|
||||
- Projects
|
||||
parameters: []
|
||||
responses:
|
||||
'200':
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$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:
|
||||
description: Add a score to the database, upserts on id
|
||||
@@ -1524,6 +1563,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
|
||||
|
||||
@@ -513,6 +513,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,
|
||||
|
||||
Generated
+95
-32
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "langfuse-core",
|
||||
"version": "1.5.1",
|
||||
"version": "1.7.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "langfuse-core",
|
||||
"version": "1.5.1",
|
||||
"version": "1.7.1",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@anthropic-ai/tokenizer": "^0.0.4",
|
||||
@@ -42,10 +42,10 @@
|
||||
"@tanstack/react-query": "^4.36.1",
|
||||
"@tanstack/react-table": "^8.10.7",
|
||||
"@tremor/react": "^3.11.1",
|
||||
"@trpc/client": "^10.43.6",
|
||||
"@trpc/next": "^10.43.6",
|
||||
"@trpc/react-query": "^10.43.6",
|
||||
"@trpc/server": "^10.43.6",
|
||||
"@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.2",
|
||||
"bcryptjs": "^2.4.3",
|
||||
@@ -63,7 +63,7 @@
|
||||
"next-auth": "^4.24.5",
|
||||
"next-query-params": "^4.3.0",
|
||||
"next-swagger-doc": "^0.4.0",
|
||||
"posthog-js": "^1.92.1",
|
||||
"posthog-js": "^1.93.2",
|
||||
"posthog-node": "^3.1.3",
|
||||
"react": "18.2.0",
|
||||
"react-day-picker": "^8.9.1",
|
||||
@@ -84,6 +84,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.40.0",
|
||||
"@release-it/bumper": "^6.0.1",
|
||||
"@testing-library/jest-dom": "^6.1.4",
|
||||
"@types/bcryptjs": "^2.4.6",
|
||||
"@types/cors": "^2.8.17",
|
||||
@@ -91,7 +92,7 @@
|
||||
"@types/jest": "^29.5.10",
|
||||
"@types/js-yaml": "^4.0.9",
|
||||
"@types/lodash": "^4.14.202",
|
||||
"@types/node": "18.16.0",
|
||||
"@types/node": "20.10.0",
|
||||
"@types/react": "^18.2.38",
|
||||
"@types/react-dom": "^18.2.17",
|
||||
"@types/swagger-ui-react": "^4.18.3",
|
||||
@@ -167,6 +168,14 @@
|
||||
"tiktoken": "^1.0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/@anthropic-ai/tokenizer/node_modules/@types/node": {
|
||||
"version": "18.18.13",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.18.13.tgz",
|
||||
"integrity": "sha512-vXYZGRrSCreZmq1rEjMRLXJhiy8MrIeVasx+PCVlP414N7CJLHnMf+juVvjdprHyH+XRy3zKZLHeNueOpJCn0g==",
|
||||
"dependencies": {
|
||||
"undici-types": "~5.26.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@apidevtools/json-schema-ref-parser": {
|
||||
"version": "9.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-9.1.2.tgz",
|
||||
@@ -3251,6 +3260,36 @@
|
||||
"@babel/runtime": "^7.13.10"
|
||||
}
|
||||
},
|
||||
"node_modules/@release-it/bumper": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@release-it/bumper/-/bumper-6.0.1.tgz",
|
||||
"integrity": "sha512-yeQsbGNMzzN0c/5JV1awXP6UHX/kJamXCKR6/daS0YQfj98SZXAcLn3JEq+qfK/Jq/cnATnlz5r6UY0cfBkm1A==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@iarna/toml": "^2.2.5",
|
||||
"detect-indent": "7.0.1",
|
||||
"fast-glob": "^3.3.2",
|
||||
"ini": "^4.1.1",
|
||||
"js-yaml": "^4.1.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"semver": "^7.3.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"release-it": "^17.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@release-it/bumper/node_modules/ini": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/ini/-/ini-4.1.1.tgz",
|
||||
"integrity": "sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": "^14.17.0 || ^16.13.0 || >=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rollup/plugin-commonjs": {
|
||||
"version": "24.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-24.0.0.tgz",
|
||||
@@ -4039,20 +4078,20 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@trpc/client": {
|
||||
"version": "10.43.6",
|
||||
"resolved": "https://registry.npmjs.org/@trpc/client/-/client-10.43.6.tgz",
|
||||
"integrity": "sha512-gQSxCQgPeBn/wqBEScu5Nq9UKqA16e965vWBj+BbdvI4URV72T44/yg0cl/E6xtBgycCVwdzwn7CuZaM8FA/VQ==",
|
||||
"version": "10.44.1",
|
||||
"resolved": "https://registry.npmjs.org/@trpc/client/-/client-10.44.1.tgz",
|
||||
"integrity": "sha512-vTWsykNcgz1LnwePVl2fKZnhvzP9N3GaaLYPkfGINo314ZOS0OBqe9x0ytB2LLUnRVTAAZ2WoONzARd8nHiqrA==",
|
||||
"funding": [
|
||||
"https://trpc.io/sponsor"
|
||||
],
|
||||
"peerDependencies": {
|
||||
"@trpc/server": "10.43.6"
|
||||
"@trpc/server": "10.44.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@trpc/next": {
|
||||
"version": "10.43.6",
|
||||
"resolved": "https://registry.npmjs.org/@trpc/next/-/next-10.43.6.tgz",
|
||||
"integrity": "sha512-srV4twQKp8FohivGZ5wxNUdTzgPjjKeuNWG3Bpy5hVrIvg8VHDcglPMHS+3eWTUDDtCbhWpvANre+81B8b9Fgg==",
|
||||
"version": "10.44.1",
|
||||
"resolved": "https://registry.npmjs.org/@trpc/next/-/next-10.44.1.tgz",
|
||||
"integrity": "sha512-ez2oYUzmaQ+pGch627sRBfeEk3h+UIwNicR8WjTAM54TPcdP5W9ZyWCyO5HZTEfjHgGixYM4tCIxewdKOWY9yA==",
|
||||
"funding": [
|
||||
"https://trpc.io/sponsor"
|
||||
],
|
||||
@@ -4061,33 +4100,33 @@
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tanstack/react-query": "^4.18.0",
|
||||
"@trpc/client": "10.43.6",
|
||||
"@trpc/react-query": "10.43.6",
|
||||
"@trpc/server": "10.43.6",
|
||||
"@trpc/client": "10.44.1",
|
||||
"@trpc/react-query": "10.44.1",
|
||||
"@trpc/server": "10.44.1",
|
||||
"next": "*",
|
||||
"react": ">=16.8.0",
|
||||
"react-dom": ">=16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@trpc/react-query": {
|
||||
"version": "10.43.6",
|
||||
"resolved": "https://registry.npmjs.org/@trpc/react-query/-/react-query-10.43.6.tgz",
|
||||
"integrity": "sha512-Twf0/wvcrDwmaJ6OLf0YVNwiv8+gtoSFyKYqe+5lMkFtUYSl+4KGvSqiN9ynbnofHCvuPgjJmjdS8pxYkcWxCw==",
|
||||
"version": "10.44.1",
|
||||
"resolved": "https://registry.npmjs.org/@trpc/react-query/-/react-query-10.44.1.tgz",
|
||||
"integrity": "sha512-Sgi/v0YtdunOXjBRi7om9gILGkOCFYXPzn5KqLuEHiZw5dr5w4qGHFwCeMAvndZxmwfblJrl1tk2AznmsVu8MA==",
|
||||
"funding": [
|
||||
"https://trpc.io/sponsor"
|
||||
],
|
||||
"peerDependencies": {
|
||||
"@tanstack/react-query": "^4.18.0",
|
||||
"@trpc/client": "10.43.6",
|
||||
"@trpc/server": "10.43.6",
|
||||
"@trpc/client": "10.44.1",
|
||||
"@trpc/server": "10.44.1",
|
||||
"react": ">=16.8.0",
|
||||
"react-dom": ">=16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@trpc/server": {
|
||||
"version": "10.43.6",
|
||||
"resolved": "https://registry.npmjs.org/@trpc/server/-/server-10.43.6.tgz",
|
||||
"integrity": "sha512-ziN7UXGAycxe4i3FwJstTe6jzCcKBlPociCrC9XtfPzFpMTf0hNbRQQlFiZjJk23ZGQSVYDjk9RO4yIHt94mJg==",
|
||||
"version": "10.44.1",
|
||||
"resolved": "https://registry.npmjs.org/@trpc/server/-/server-10.44.1.tgz",
|
||||
"integrity": "sha512-mF7B+K6LjuboX8I1RZgKE5GA/fJhsJ8tKGK2UBt3Bwik7hepEPb4NJgNr7vO6BK5IYwPdBLRLTctRw6XZx0sRg==",
|
||||
"funding": [
|
||||
"https://trpc.io/sponsor"
|
||||
],
|
||||
@@ -4328,9 +4367,13 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "18.16.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.16.0.tgz",
|
||||
"integrity": "sha512-BsAaKhB+7X+H4GnSjGhJG9Qi8Tw+inU9nJDwmD5CgOmBLEI6ArdhikpLX7DjbjDRDTbqZzU2LSQNZg8WGPiSZQ=="
|
||||
"version": "20.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.0.tgz",
|
||||
"integrity": "sha512-D0WfRmU9TQ8I9PFx9Yc+EBHw+vSpIub4IDvQivcp26PtPrdMGAq5SDcpXEo/epqa/DXotVpekHiLNTg3iaKXBQ==",
|
||||
"devOptional": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~5.26.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/parse-json": {
|
||||
"version": "4.0.2",
|
||||
@@ -7053,6 +7096,15 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/detect-indent": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-7.0.1.tgz",
|
||||
"integrity": "sha512-Mc7QhQ8s+cLrnUfU/Ji94vG/r8M26m8f++vyres4ZoojaRDpZ1eSIh/EpzLNwlWuvzSZ3UbDFspjFvTDXe6e/g==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=12.20"
|
||||
}
|
||||
},
|
||||
"node_modules/detect-libc": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.2.tgz",
|
||||
@@ -11077,6 +11129,12 @@
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
|
||||
},
|
||||
"node_modules/lodash-es": {
|
||||
"version": "4.17.21",
|
||||
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz",
|
||||
"integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/lodash.capitalize": {
|
||||
"version": "4.2.1",
|
||||
"resolved": "https://registry.npmjs.org/lodash.capitalize/-/lodash.capitalize-4.2.1.tgz",
|
||||
@@ -12850,9 +12908,9 @@
|
||||
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="
|
||||
},
|
||||
"node_modules/posthog-js": {
|
||||
"version": "1.92.1",
|
||||
"resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.92.1.tgz",
|
||||
"integrity": "sha512-xtuTfM/acfDauiEfIdKF6d911KUZQ7RLii2COAYEoPWr3cVUFoNUoRQz9QJvgDlV2j22Zwl+mnXacUeua+Yi1A==",
|
||||
"version": "1.93.2",
|
||||
"resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.93.2.tgz",
|
||||
"integrity": "sha512-0e2kqlb4kB1/Q9poLFlMF+SUrW+DCzNBHTJuUKl177euE4LChkJipSjy2vpq98qtJ2K3Hxw7ylHf2C+dZCx4RA==",
|
||||
"dependencies": {
|
||||
"fflate": "^0.4.1"
|
||||
}
|
||||
@@ -15534,6 +15592,11 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "5.26.5",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
|
||||
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="
|
||||
},
|
||||
"node_modules/unicorn-magic": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.1.0.tgz",
|
||||
|
||||
+16
-7
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "langfuse-core",
|
||||
"version": "1.5.1",
|
||||
"version": "1.7.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"prebuild": "cp generated/openapi-client/openapi.yml public/openapi-client.yml && cp generated/openapi-server/openapi.yml public/openapi-server.yml",
|
||||
@@ -56,10 +56,10 @@
|
||||
"@tanstack/react-query": "^4.36.1",
|
||||
"@tanstack/react-table": "^8.10.7",
|
||||
"@tremor/react": "^3.11.1",
|
||||
"@trpc/client": "^10.43.6",
|
||||
"@trpc/next": "^10.43.6",
|
||||
"@trpc/react-query": "^10.43.6",
|
||||
"@trpc/server": "^10.43.6",
|
||||
"@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.2",
|
||||
"bcryptjs": "^2.4.3",
|
||||
@@ -77,7 +77,7 @@
|
||||
"next-auth": "^4.24.5",
|
||||
"next-query-params": "^4.3.0",
|
||||
"next-swagger-doc": "^0.4.0",
|
||||
"posthog-js": "^1.92.1",
|
||||
"posthog-js": "^1.93.2",
|
||||
"posthog-node": "^3.1.3",
|
||||
"react": "18.2.0",
|
||||
"react-day-picker": "^8.9.1",
|
||||
@@ -98,6 +98,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.40.0",
|
||||
"@release-it/bumper": "^6.0.1",
|
||||
"@testing-library/jest-dom": "^6.1.4",
|
||||
"@types/bcryptjs": "^2.4.6",
|
||||
"@types/cors": "^2.8.17",
|
||||
@@ -105,7 +106,7 @@
|
||||
"@types/jest": "^29.5.10",
|
||||
"@types/js-yaml": "^4.0.9",
|
||||
"@types/lodash": "^4.14.202",
|
||||
"@types/node": "18.16.0",
|
||||
"@types/node": "20.10.0",
|
||||
"@types/react": "^18.2.38",
|
||||
"@types/react-dom": "^18.2.17",
|
||||
"@types/swagger-ui-react": "^4.18.3",
|
||||
@@ -145,6 +146,14 @@
|
||||
"web": true,
|
||||
"autoGenerate": true,
|
||||
"releaseName": "v${version}"
|
||||
},
|
||||
"plugins": {
|
||||
"@release-it/bumper": {
|
||||
"out": {
|
||||
"file": "./src/constants/VERSION.ts",
|
||||
"type": "application/typescript"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -247,6 +247,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();
|
||||
|
||||
@@ -108,7 +108,7 @@ export default function Layout(props: PropsWithChildren) {
|
||||
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>
|
||||
);
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export const VERSION = "v1.7.1";
|
||||
+1
-1
@@ -49,7 +49,7 @@ 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(),
|
||||
},
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -7,6 +7,7 @@ const operatorReplacements = {
|
||||
"any of": "IN",
|
||||
"none of": "NOT IN",
|
||||
contains: "ILIKE",
|
||||
"does not contain": "NOT ILIKE",
|
||||
"starts with": "ILIKE",
|
||||
"ends with": "ILIKE",
|
||||
};
|
||||
@@ -69,10 +70,14 @@ export function filterToPrismaSql(
|
||||
const [valuePrefix, valueSuffix] =
|
||||
filter.type === "string" || filter.type === "stringObject"
|
||||
? [
|
||||
["contains", "ends with"].includes(filter.operator)
|
||||
["contains", "does not contain", "ends with"].includes(
|
||||
filter.operator,
|
||||
)
|
||||
? Prisma.raw("'%' || ")
|
||||
: Prisma.empty,
|
||||
["contains", "starts with"].includes(filter.operator)
|
||||
["contains", "does not contain", "starts with"].includes(
|
||||
filter.operator,
|
||||
)
|
||||
? Prisma.raw(" || '%'")
|
||||
: Prisma.empty,
|
||||
]
|
||||
|
||||
@@ -5,13 +5,13 @@ import { PlusIcon } from "lucide-react";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/src/components/ui/dialog";
|
||||
import { CodeView } from "@/src/components/ui/code";
|
||||
import { useHasAccess } from "@/src/features/rbac/utils/checkAccess";
|
||||
import { usePostHog } from "posthog-js/react";
|
||||
import { env } from "@/src/env.mjs";
|
||||
|
||||
export function CreateApiKeyButton(props: { projectId: string }) {
|
||||
const utils = api.useUtils();
|
||||
@@ -21,6 +21,9 @@ export function CreateApiKeyButton(props: { projectId: string }) {
|
||||
scope: "apiKeys:create",
|
||||
});
|
||||
|
||||
const hostname =
|
||||
env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION !== "EU" ? window.origin : undefined;
|
||||
|
||||
const mutCreateApiKey = api.apiKeys.create.useMutation({
|
||||
onSuccess: () => utils.apiKeys.invalidate(),
|
||||
});
|
||||
@@ -64,9 +67,19 @@ export function CreateApiKeyButton(props: { projectId: string }) {
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent onPointerDownOutside={(e) => e.preventDefault()}>
|
||||
<DialogHeader>
|
||||
<DialogTitle>API Keys</DialogTitle>
|
||||
</DialogHeader>
|
||||
{hostname ? (
|
||||
<>
|
||||
<DialogTitle>Hostname</DialogTitle>
|
||||
<div className="mb-6">
|
||||
<div className="my-2">
|
||||
When connecting to Langfuse, use this hostname / baseurl.
|
||||
</div>
|
||||
<CodeView content={hostname} />
|
||||
</div>
|
||||
</>
|
||||
) : null}
|
||||
|
||||
<DialogTitle>API Keys</DialogTitle>
|
||||
<div className="mb-2">
|
||||
<div className="text-md font-semibold">Secret Key</div>
|
||||
<div className="my-2">
|
||||
|
||||
@@ -7,6 +7,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`.
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { VERSION } from "@/src/constants/VERSION";
|
||||
import { prisma } from "@/src/server/db";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { PostHog } from "posthog-node";
|
||||
@@ -225,17 +226,32 @@ async function posthogTelemetry({
|
||||
},
|
||||
});
|
||||
|
||||
// Domains (no PII)
|
||||
const domains = await prisma.$queryRaw<Array<{ domain: string }>>`
|
||||
SELECT
|
||||
substring(email FROM position('@' in email) + 1) as domain,
|
||||
count(id)::int as "userCount"
|
||||
FROM users
|
||||
WHERE email ILIKE '%@%'
|
||||
GROUP BY 1
|
||||
ORDER BY count(id) desc
|
||||
LIMIT 30
|
||||
`;
|
||||
|
||||
posthog.identify({
|
||||
distinctId: "docker:" + clientId,
|
||||
properties: {
|
||||
environment: process.env.NODE_ENV,
|
||||
userDomains: domains,
|
||||
docker: true,
|
||||
langfuseVersion: VERSION,
|
||||
},
|
||||
});
|
||||
posthog.capture({
|
||||
distinctId: "docker:" + clientId,
|
||||
event: "telemetry",
|
||||
properties: {
|
||||
langfuseVersion: VERSION,
|
||||
totalProjects: totalProjects,
|
||||
traces: countTraces,
|
||||
scores: countScores,
|
||||
|
||||
@@ -105,6 +105,7 @@ function UserTracking() {
|
||||
process.env.NEXT_PUBLIC_POSTHOG_HOST
|
||||
)
|
||||
posthog.identify(session.data.user?.id ?? undefined, {
|
||||
environment: process.env.NODE_ENV,
|
||||
email: session.data.user?.email ?? undefined,
|
||||
name: session.data.user?.name ?? undefined,
|
||||
featureFlags: session.data.user?.featureFlags ?? undefined,
|
||||
|
||||
@@ -70,7 +70,8 @@ export default async function handler(
|
||||
// Trigger new user signup event
|
||||
if (
|
||||
env.LANGFUSE_NEW_USER_SIGNUP_WEBHOOK &&
|
||||
env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION
|
||||
env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION &&
|
||||
env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION !== "STAGING"
|
||||
) {
|
||||
await fetch(env.LANGFUSE_NEW_USER_SIGNUP_WEBHOOK, {
|
||||
method: "POST",
|
||||
|
||||
@@ -16,6 +16,11 @@ export default async function handler(
|
||||
message: "Only runs on Langfuse Cloud, no LANGFUSE_CLOUD_REGION provided",
|
||||
});
|
||||
|
||||
if (env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION === "STAGING")
|
||||
return res.status(200).json({
|
||||
message: "Does not run on staging, LANGFUSE_CLOUD_REGION is STAGING",
|
||||
});
|
||||
|
||||
const posthog_event_user_id =
|
||||
env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION === "US"
|
||||
? "langfuse-cloud-us"
|
||||
|
||||
@@ -245,24 +245,27 @@ export const handleBatchResult = (
|
||||
message: "Invalid request data",
|
||||
error: error.error.message,
|
||||
});
|
||||
} else if (error.error instanceof AuthenticationError) {
|
||||
returnedErrors.push({
|
||||
id: error.id,
|
||||
status: 401,
|
||||
message: "Authentication error",
|
||||
error: error.error.message,
|
||||
});
|
||||
} else if (error.error instanceof ResourceNotFoundError) {
|
||||
if (!errors.some((res) => res.id === error.id)) {
|
||||
returnedErrors.push({
|
||||
id: error.id,
|
||||
status: 404,
|
||||
message: "Resource not found",
|
||||
error: error.error.message,
|
||||
});
|
||||
}
|
||||
returnedErrors.push({
|
||||
id: error.id,
|
||||
status: 404,
|
||||
message: "Resource not found",
|
||||
error: error.error.message,
|
||||
});
|
||||
} else {
|
||||
if (!errors.some((res) => res.id === error.id)) {
|
||||
returnedErrors.push({
|
||||
id: error.id,
|
||||
status: 500,
|
||||
message: "Error processing events",
|
||||
error: "Internal Server Error",
|
||||
});
|
||||
}
|
||||
returnedErrors.push({
|
||||
id: error.id,
|
||||
status: 500,
|
||||
message: "Error processing events",
|
||||
error: "Internal Server Error",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import { verifyAuthHeaderAndReturnScope } from "@/src/features/public-api/server/apiAuth";
|
||||
import { cors, runMiddleware } from "@/src/features/public-api/server/cors";
|
||||
import { prisma } from "@/src/server/db";
|
||||
|
||||
import { type NextApiRequest, type NextApiResponse } from "next";
|
||||
|
||||
export default async function handler(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse,
|
||||
) {
|
||||
await runMiddleware(req, res, cors);
|
||||
|
||||
// CHECK AUTH
|
||||
const authCheck = await verifyAuthHeaderAndReturnScope(
|
||||
req.headers.authorization,
|
||||
);
|
||||
if (!authCheck.validKey)
|
||||
return res.status(401).json({
|
||||
message: authCheck.error,
|
||||
});
|
||||
// END CHECK AUTH
|
||||
|
||||
if (req.method === "GET") {
|
||||
try {
|
||||
const projects = await prisma.project.findMany({
|
||||
where: {
|
||||
id: authCheck.scope.projectId,
|
||||
},
|
||||
});
|
||||
|
||||
return res.status(200).json({
|
||||
data: projects.map((project) => ({
|
||||
id: project.id,
|
||||
name: project.name,
|
||||
})),
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return res.status(500).json({ message: "Internal server error" });
|
||||
}
|
||||
} else {
|
||||
console.error(
|
||||
`Method not allowed for ${req.method} on /api/public/projects`,
|
||||
);
|
||||
return res.status(405).json({ message: "Method not allowed" });
|
||||
}
|
||||
}
|
||||
+58
-45
@@ -21,8 +21,9 @@ import { useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import * as z from "zod";
|
||||
import { usePostHog } from "posthog-js/react";
|
||||
import { CloudPrivacyNotice } from "@/src/pages/auth/sign-up";
|
||||
import { Divider } from "@tremor/react";
|
||||
import { CloudPrivacyNotice } from "@/src/features/auth/components/AuthCloudPrivacyNotice";
|
||||
import { CloudRegionSwitch } from "@/src/features/auth/components/AuthCloudRegionSwitch";
|
||||
|
||||
const credentialAuthForm = z.object({
|
||||
email: z.string().email(),
|
||||
@@ -31,7 +32,8 @@ const credentialAuthForm = z.object({
|
||||
}),
|
||||
});
|
||||
|
||||
type PageProps = {
|
||||
// Also used in src/pages/auth/sign-up.tsx
|
||||
export type PageProps = {
|
||||
authProviders: {
|
||||
credentials: boolean;
|
||||
google: boolean;
|
||||
@@ -39,6 +41,7 @@ type PageProps = {
|
||||
};
|
||||
};
|
||||
|
||||
// Also used in src/pages/auth/sign-up.tsx
|
||||
// eslint-disable-next-line @typescript-eslint/require-await
|
||||
export const getServerSideProps: GetServerSideProps<PageProps> = async () => {
|
||||
return {
|
||||
@@ -56,7 +59,54 @@ export const getServerSideProps: GetServerSideProps<PageProps> = async () => {
|
||||
};
|
||||
};
|
||||
|
||||
export default function SignIn(props: PageProps) {
|
||||
// Also used in src/pages/auth/sign-up.tsx
|
||||
export function SSOButtons({
|
||||
authProviders,
|
||||
action = "Sign in",
|
||||
}: PageProps & { action?: string }) {
|
||||
const posthog = usePostHog();
|
||||
|
||||
return (
|
||||
// any authprovider from props is enanbles
|
||||
Object.entries(authProviders).some(
|
||||
([name, enabled]) => enabled && name !== "credentials",
|
||||
) ? (
|
||||
<div>
|
||||
{authProviders.credentials ? (
|
||||
<Divider className="text-gray-400" />
|
||||
) : null}
|
||||
<div className="flex flex-row flex-wrap items-center justify-center gap-4">
|
||||
{authProviders.google ? (
|
||||
<Button
|
||||
onClick={() => {
|
||||
posthog.capture("sign_in:google_button_click");
|
||||
void signIn("google");
|
||||
}}
|
||||
variant="secondary"
|
||||
>
|
||||
<FcGoogle className="mr-3" size={18} />
|
||||
{action} with Google
|
||||
</Button>
|
||||
) : null}
|
||||
{authProviders.github ? (
|
||||
<Button
|
||||
onClick={() => {
|
||||
posthog.capture("sign_in:github_button_click");
|
||||
void signIn("github");
|
||||
}}
|
||||
variant="secondary"
|
||||
>
|
||||
<FaGithub className="mr-3" size={18} />
|
||||
{action} with Github
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
) : null
|
||||
);
|
||||
}
|
||||
|
||||
export default function SignIn({ authProviders }: PageProps) {
|
||||
const [credentialsFormError, setCredentialsFormError] = useState<
|
||||
string | null
|
||||
>(null);
|
||||
@@ -101,8 +151,9 @@ export default function SignIn(props: PageProps) {
|
||||
</div>
|
||||
|
||||
<div className="mt-14 bg-white px-6 py-10 shadow sm:mx-auto sm:w-full sm:max-w-[480px] sm:rounded-lg sm:px-12">
|
||||
<div className="mt-2 space-y-8">
|
||||
{props.authProviders.credentials ? (
|
||||
<div className="space-y-8">
|
||||
<CloudRegionSwitch />
|
||||
{authProviders.credentials ? (
|
||||
<Form {...credentialsForm}>
|
||||
<form
|
||||
className="space-y-6"
|
||||
@@ -152,51 +203,13 @@ export default function SignIn(props: PageProps) {
|
||||
</form>
|
||||
</Form>
|
||||
) : null}
|
||||
|
||||
{
|
||||
// any authprovider from props is enanbles
|
||||
Object.entries(props.authProviders).some(
|
||||
([name, enabled]) => enabled && name !== "credentials",
|
||||
) ? (
|
||||
<div>
|
||||
{props.authProviders.credentials ? (
|
||||
<Divider className="text-gray-400" />
|
||||
) : null}
|
||||
<div className="flex flex-row flex-wrap items-center justify-center gap-4">
|
||||
{props.authProviders.google ? (
|
||||
<Button
|
||||
onClick={() => {
|
||||
posthog.capture("sign_in:google_button_click");
|
||||
void signIn("google");
|
||||
}}
|
||||
variant="secondary"
|
||||
>
|
||||
<FcGoogle className="mr-3" size={18} />
|
||||
Sign in with Google
|
||||
</Button>
|
||||
) : null}
|
||||
{props.authProviders.github ? (
|
||||
<Button
|
||||
onClick={() => {
|
||||
posthog.capture("sign_in:github_button_click");
|
||||
void signIn("github");
|
||||
}}
|
||||
variant="secondary"
|
||||
>
|
||||
<FaGithub className="mr-3" size={18} />
|
||||
Sign in with Github
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
) : null
|
||||
}
|
||||
<SSOButtons authProviders={authProviders} />
|
||||
</div>
|
||||
<CloudPrivacyNotice action="signing in" />
|
||||
</div>
|
||||
|
||||
{env.NEXT_PUBLIC_SIGN_UP_DISABLED !== "true" &&
|
||||
props.authProviders.credentials ? (
|
||||
authProviders.credentials ? (
|
||||
<p className="mt-10 text-center text-sm text-gray-500">
|
||||
No account yet?{" "}
|
||||
<Link
|
||||
|
||||
+10
-33
@@ -19,8 +19,14 @@ import { env } from "@/src/env.mjs";
|
||||
import { useState } from "react";
|
||||
import { LangfuseIcon } from "@/src/components/LangfuseLogo";
|
||||
import { usePostHog } from "posthog-js/react";
|
||||
import { CloudPrivacyNotice } from "@/src/features/auth/components/AuthCloudPrivacyNotice";
|
||||
import { CloudRegionSwitch } from "@/src/features/auth/components/AuthCloudRegionSwitch";
|
||||
import { SSOButtons, type PageProps } from "@/src/pages/auth/sign-in";
|
||||
|
||||
export default function SignIn() {
|
||||
// Use the same getServerSideProps function as src/pages/auth/sign-in.tsx
|
||||
export { getServerSideProps } from "@/src/pages/auth/sign-in";
|
||||
|
||||
export default function SignIn({ authProviders }: PageProps) {
|
||||
const posthog = usePostHog();
|
||||
const [formError, setFormError] = useState<string | null>(null);
|
||||
const form = useForm<z.infer<typeof signupSchema>>({
|
||||
@@ -84,11 +90,12 @@ export default function SignIn() {
|
||||
</div>
|
||||
{env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION !== undefined ? (
|
||||
<div className="text-center sm:mx-auto sm:w-full sm:max-w-[480px]">
|
||||
No credit card required. All users have access to a demo project.
|
||||
No credit card required.
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="mt-14 bg-white px-6 py-10 shadow sm:mx-auto sm:w-full sm:max-w-[480px] sm:rounded-lg sm:px-12">
|
||||
<CloudRegionSwitch isSignUpPage />
|
||||
<Form {...form}>
|
||||
<form
|
||||
className="space-y-6"
|
||||
@@ -166,6 +173,7 @@ export default function SignIn() {
|
||||
) : null}
|
||||
</form>
|
||||
</Form>
|
||||
<SSOButtons authProviders={authProviders} action="Sign up" />
|
||||
<CloudPrivacyNotice action="creating an account" />
|
||||
</div>
|
||||
|
||||
@@ -182,34 +190,3 @@ export default function SignIn() {
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
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>
|
||||
</div>
|
||||
) : null;
|
||||
|
||||
@@ -2,10 +2,16 @@ import { z } from "zod";
|
||||
|
||||
export const filterOperators = {
|
||||
datetime: [">", "<", ">=", "<="],
|
||||
string: ["=", "contains", "starts with", "ends with"],
|
||||
string: ["=", "contains", "does not contain", "starts with", "ends with"],
|
||||
stringOptions: ["any of", "none of"],
|
||||
number: ["=", ">", "<", ">=", "<="],
|
||||
stringObject: ["=", "contains", "starts with", "ends with"],
|
||||
stringObject: [
|
||||
"=",
|
||||
"contains",
|
||||
"does not contain",
|
||||
"starts with",
|
||||
"ends with",
|
||||
],
|
||||
numberObject: ["=", ">", "<", ">=", "<="],
|
||||
} as const;
|
||||
|
||||
|
||||
+2
-1
@@ -239,7 +239,8 @@ export const authOptions: NextAuthOptions = {
|
||||
createUser: async ({ user }) => {
|
||||
if (
|
||||
env.LANGFUSE_NEW_USER_SIGNUP_WEBHOOK &&
|
||||
env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION
|
||||
env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION &&
|
||||
env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION !== "STAGING"
|
||||
) {
|
||||
await fetch(env.LANGFUSE_NEW_USER_SIGNUP_WEBHOOK, {
|
||||
method: "POST",
|
||||
|
||||
Reference in New Issue
Block a user