Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
+2
-1
@@ -42,4 +42,5 @@ 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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -280,6 +280,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,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");
|
||||
}
|
||||
@@ -14,8 +14,5 @@ if [ $status -ne 0 ]; then
|
||||
exit $status
|
||||
fi
|
||||
|
||||
# Start background cron job
|
||||
node cron.js &
|
||||
|
||||
# Start server
|
||||
node server.js
|
||||
|
||||
@@ -146,16 +146,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
|
||||
|
||||
@@ -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
|
||||
@@ -11,7 +11,7 @@ groups:
|
||||
namespaceExport: Langfuse
|
||||
allowCustomFetcher: true
|
||||
- name: fernapi/fern-python-sdk
|
||||
version: 0.3.7
|
||||
version: 0.6.6
|
||||
output:
|
||||
location: local-file-system
|
||||
path: ../../../generated/python
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"organization": "finto",
|
||||
"version": "0.15.0-rc68"
|
||||
"version": "0.15.3"
|
||||
}
|
||||
@@ -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:
|
||||
@@ -73,32 +68,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 +119,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 +159,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 +215,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 +255,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 +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
|
||||
@@ -379,32 +344,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 +372,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 +427,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 +488,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 +570,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 +649,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 +718,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/spans:
|
||||
post:
|
||||
@@ -747,32 +758,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 +803,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 +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
|
||||
@@ -922,32 +918,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 +964,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:
|
||||
@@ -1577,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
|
||||
|
||||
@@ -341,6 +341,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,
|
||||
@@ -481,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,
|
||||
|
||||
+13
-7
@@ -27,17 +27,23 @@ const nextConfig = {
|
||||
if (env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION !== undefined)
|
||||
return [
|
||||
{
|
||||
source: '/api/auth/session',
|
||||
source: "/api/auth/session",
|
||||
headers: [
|
||||
{ key: "Access-Control-Allow-Origin", value: "https://langfuse.com" },
|
||||
{
|
||||
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" },
|
||||
]
|
||||
}
|
||||
]
|
||||
{
|
||||
key: "Access-Control-Allow-Headers",
|
||||
value: "Content-Type, Authorization",
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
return []
|
||||
return [];
|
||||
},
|
||||
|
||||
// webassembly support for @dqbd/tiktoken
|
||||
|
||||
Generated
+1279
-1253
File diff suppressed because it is too large
Load Diff
+40
-31
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "langfuse-core",
|
||||
"version": "1.2.0",
|
||||
"version": "1.7.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"prebuild": "cp generated/openapi-client/openapi.yml public/openapi-client.yml && cp generated/openapi-server/openapi.yml public/openapi-server.yml",
|
||||
@@ -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.6.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,7 @@
|
||||
"@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",
|
||||
"@sentry/nextjs": "^7.81.1",
|
||||
"@sentry/profiling-node": "^1.2.6",
|
||||
"@t3-oss/env-nextjs": "^0.7.1",
|
||||
"@tailwindcss/forms": "^0.5.7",
|
||||
@@ -56,12 +56,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",
|
||||
@@ -73,61 +73,62 @@
|
||||
"js-yaml": "^4.1.0",
|
||||
"lodash": "^4.17.21",
|
||||
"lucide-react": "^0.292.0",
|
||||
"next": "^13.5.6",
|
||||
"next": "^14.0.3",
|
||||
"next-auth": "^4.24.5",
|
||||
"next-query-params": "^4.2.3",
|
||||
"next-query-params": "^4.3.0",
|
||||
"next-swagger-doc": "^0.4.0",
|
||||
"posthog-js": "^1.88.4",
|
||||
"posthog-js": "^1.93.2",
|
||||
"posthog-node": "^3.1.3",
|
||||
"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.48.2",
|
||||
"react-icons": "^4.12.0",
|
||||
"react-responsive": "^9.0.2",
|
||||
"react18-json-view": "^0.2.7-canary.1",
|
||||
"superagent": "^8.1.2",
|
||||
"superjson": "2.0.0",
|
||||
"tailwind-merge": "^1.14.0",
|
||||
"superjson": "2.2.1",
|
||||
"tailwind-merge": "^2.0.0",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"tiktoken": "^1.0.10",
|
||||
"tiktoken": "^1.0.11",
|
||||
"ts-pattern": "^5.0.5",
|
||||
"use-query-params": "^2.2.1",
|
||||
"uuid": "^9.0.1",
|
||||
"zod": "^3.22.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.39.0",
|
||||
"@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.16",
|
||||
"@types/eslint": "^8.44.6",
|
||||
"@types/jest": "^29.5.8",
|
||||
"@types/cors": "^2.8.17",
|
||||
"@types/eslint": "^8.44.7",
|
||||
"@types/jest": "^29.5.10",
|
||||
"@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": "18.16.0",
|
||||
"@types/react": "^18.2.38",
|
||||
"@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.12.0",
|
||||
"@typescript-eslint/parser": "^6.12.0",
|
||||
"autoprefixer": "^10.4.16",
|
||||
"dotenv-cli": "^7.3.0",
|
||||
"eslint": "^8.52.0",
|
||||
"eslint-config-next": "^13.5.6",
|
||||
"eslint": "^8.54.0",
|
||||
"eslint-config-next": "^14.0.3",
|
||||
"jest": "^29.7.0",
|
||||
"jest-environment-jsdom": "^29.7.0",
|
||||
"postcss": "^8.4.31",
|
||||
"prettier": "^3.0.3",
|
||||
"prettier": "^3.1.0",
|
||||
"prettier-plugin-tailwindcss": "^0.5.7",
|
||||
"prisma": "^5.5.2",
|
||||
"prisma": "^5.6.0",
|
||||
"release-it": "^17.0.0",
|
||||
"swagger-jsdoc": "^6.2.8",
|
||||
"tailwindcss": "^3.3.5",
|
||||
"ts-node": "^10.9.1",
|
||||
"tsconfig-paths": "^4.2.0",
|
||||
"typescript": "^5.2.2"
|
||||
"typescript": "^5.3.2"
|
||||
},
|
||||
"ct3aMetadata": {
|
||||
"initVersion": "7.13.0"
|
||||
@@ -145,6 +146,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);
|
||||
@@ -247,9 +247,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")
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
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";
|
||||
|
||||
export function DeleteTrace({
|
||||
traceId,
|
||||
projectId,
|
||||
isTableAction,
|
||||
}: {
|
||||
traceId: string;
|
||||
projectId: string;
|
||||
isTableAction?: boolean;
|
||||
}) {
|
||||
const router = useRouter();
|
||||
const utils = api.useUtils();
|
||||
|
||||
const hasAccess = useHasAccess({ projectId, scope: "traces:delete" });
|
||||
|
||||
const mutDeleteTrace = api.traces.delete.useMutation({
|
||||
onSuccess: () => {
|
||||
if (!isTableAction) {
|
||||
void router
|
||||
.push(`/project/${projectId}/traces`)
|
||||
.then(() => utils.traces.invalidate());
|
||||
} else {
|
||||
void utils.traces.invalidate();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
if (!hasAccess) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
{isTableAction ? (
|
||||
<Button variant="ghost" size="xs">
|
||||
<TrashIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
) : (
|
||||
<Button variant="outline" type="button">
|
||||
<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}
|
||||
onClick={() =>
|
||||
void mutDeleteTrace.mutateAsync({ traceId, projectId })
|
||||
}
|
||||
>
|
||||
Delete trace
|
||||
</Button>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
import { ChevronLeftIcon, ChevronRightIcon } from "@heroicons/react/20/solid";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { useSession } from "next-auth/react";
|
||||
|
||||
export default function Header(props: {
|
||||
title: string;
|
||||
@@ -7,6 +9,13 @@ export default function Header(props: {
|
||||
live?: boolean;
|
||||
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 backHref =
|
||||
props.breadcrumb &&
|
||||
[...props.breadcrumb.map((i) => i.href).filter(Boolean)].pop();
|
||||
@@ -62,6 +71,11 @@ export default function Header(props: {
|
||||
<div className="mt-2 flex flex-wrap items-center justify-between gap-5">
|
||||
<div className="flex items-center gap-3 md:gap-5">
|
||||
<div className="min-w-0">
|
||||
{project && projectId && currentPath !== "/project/[projectId]" ? (
|
||||
<div className="text-sm font-medium text-gray-500">
|
||||
{project.name}
|
||||
</div>
|
||||
) : null}
|
||||
<h2 className="text-2xl font-bold leading-7 text-gray-900 sm:truncate sm:text-3xl sm:tracking-tight">
|
||||
{props.title}
|
||||
</h2>
|
||||
|
||||
@@ -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 = [
|
||||
{
|
||||
@@ -41,7 +42,7 @@ export default function Layout(props: PropsWithChildren) {
|
||||
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 +67,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" />;
|
||||
|
||||
@@ -104,12 +103,12 @@ export default function Layout(props: PropsWithChildren) {
|
||||
|
||||
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>
|
||||
);
|
||||
@@ -241,7 +240,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}`}
|
||||
@@ -344,7 +343,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}`}
|
||||
@@ -531,16 +530,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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
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";
|
||||
@@ -217,6 +218,20 @@ export default function TracesTable({
|
||||
accessorKey: "release",
|
||||
header: "Release",
|
||||
},
|
||||
{
|
||||
accessorKey: "action",
|
||||
header: "Action",
|
||||
cell: ({ row }) => {
|
||||
const traceId = row.getValue("id");
|
||||
return traceId && typeof traceId === "string" ? (
|
||||
<DeleteTrace
|
||||
traceId={traceId}
|
||||
isTableAction={true}
|
||||
projectId={projectId}
|
||||
/>
|
||||
) : undefined;
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
@@ -239,16 +254,16 @@ 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),
|
||||
|
||||
@@ -13,6 +13,7 @@ import { DetailPageNav } from "@/src/features/navigate-detail-pages/DetailPageNa
|
||||
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";
|
||||
|
||||
export function Trace(props: {
|
||||
observations: Array<ObservationReturnType>;
|
||||
@@ -102,6 +103,10 @@ export function TracePage({ traceId }: { traceId: string }) {
|
||||
}
|
||||
listKey="traces"
|
||||
/>
|
||||
<DeleteTrace
|
||||
traceId={trace.data.id}
|
||||
projectId={trace.data.projectId}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export const VERSION = "v1.7.0";
|
||||
@@ -39,6 +39,7 @@ export const env = createEnv({
|
||||
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(),
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -81,5 +82,7 @@ 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,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
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>
|
||||
</div>
|
||||
) : null;
|
||||
@@ -0,0 +1,74 @@
|
||||
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 = [
|
||||
{
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -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";
|
||||
@@ -84,7 +83,6 @@ export const UserChart = ({
|
||||
value: item.totalTokenCost ? (item.totalTokenCost as number) : 0,
|
||||
};
|
||||
})
|
||||
.filter((i) => (isUndefinedOrNull(i.name) ? true : false))
|
||||
: [];
|
||||
|
||||
const totalCost = user.data?.reduce(
|
||||
|
||||
@@ -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,
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
@@ -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),
|
||||
|
||||
@@ -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,
|
||||
]
|
||||
|
||||
@@ -58,7 +58,7 @@ export function ManualScoreButton({
|
||||
: s.observationId === null),
|
||||
);
|
||||
|
||||
const utils = api.useContext();
|
||||
const utils = api.useUtils();
|
||||
const onSuccess = async () => {
|
||||
await Promise.all([utils.scores.invalidate(), utils.traces.invalidate()]);
|
||||
};
|
||||
@@ -179,8 +179,8 @@ export function ManualScoreButton({
|
||||
{form.formState.isSubmitting
|
||||
? "Loading ..."
|
||||
: score
|
||||
? "Update"
|
||||
: "Create"}
|
||||
? "Update"
|
||||
: "Create"}
|
||||
</Button>
|
||||
{score && (
|
||||
<Button
|
||||
|
||||
@@ -25,7 +25,7 @@ import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
|
||||
export function DeleteProjectButton(props: { projectId: string }) {
|
||||
const utils = api.useContext();
|
||||
const utils = api.useUtils();
|
||||
const router = useRouter();
|
||||
const session = useSession();
|
||||
const posthog = usePostHog();
|
||||
@@ -77,8 +77,6 @@ export function DeleteProjectButton(props: { projectId: string }) {
|
||||
});
|
||||
};
|
||||
|
||||
if (!hasAccess) return null;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2 className="mb-6 text-base font-semibold leading-6 text-gray-900">
|
||||
@@ -86,7 +84,9 @@ export function DeleteProjectButton(props: { projectId: string }) {
|
||||
</h2>
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="destructive">Delete Project</Button>
|
||||
<Button variant="destructive" disabled={!hasAccess}>
|
||||
Delete Project
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-[425px]">
|
||||
<DialogHeader>
|
||||
|
||||
@@ -44,7 +44,7 @@ export function NewProjectButton({ size = "default" }: NewProjectButtonProps) {
|
||||
name: "",
|
||||
},
|
||||
});
|
||||
const utils = api.useContext();
|
||||
const utils = api.useUtils();
|
||||
const router = useRouter();
|
||||
const posthog = usePostHog();
|
||||
const createProjectMutation = api.projects.create.useMutation({
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
import { Button } from "@/src/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/src/components/ui/dialog";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/src/components/ui/form";
|
||||
import { Input } from "@/src/components/ui/input";
|
||||
import { api } from "@/src/utils/api";
|
||||
import { usePostHog } from "posthog-js/react";
|
||||
import { useRouter } from "next/router";
|
||||
import * as z from "zod";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useSession } from "next-auth/react";
|
||||
import { useHasAccess } from "@/src/features/rbac/utils/checkAccess";
|
||||
|
||||
const formSchema = z.object({
|
||||
newOwnerEmail: z.string().email(),
|
||||
});
|
||||
export function TransferOwnershipButton(props: { projectId: string }) {
|
||||
const utils = api.useUtils();
|
||||
const router = useRouter();
|
||||
const posthog = usePostHog();
|
||||
|
||||
const session = useSession();
|
||||
const project = session?.data?.user?.projects?.find(
|
||||
(project) => project.id == props.projectId,
|
||||
);
|
||||
const hasAccess = useHasAccess({
|
||||
projectId: props.projectId,
|
||||
scope: "project:transfer",
|
||||
});
|
||||
|
||||
const form = useForm<z.infer<typeof formSchema>>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
newOwnerEmail: "",
|
||||
},
|
||||
});
|
||||
|
||||
const transferProject = api.projects.transfer.useMutation({
|
||||
onSuccess: () => utils.projects.invalidate(),
|
||||
onError: (error) =>
|
||||
form.setError("newOwnerEmail", { message: error.message }),
|
||||
});
|
||||
|
||||
function onSubmit(values: z.infer<typeof formSchema>) {
|
||||
form.clearErrors();
|
||||
transferProject
|
||||
.mutateAsync({
|
||||
projectId: props.projectId,
|
||||
newOwnerEmail: values.newOwnerEmail,
|
||||
})
|
||||
.then(() => {
|
||||
posthog.capture("project_settings:project_transfer");
|
||||
void router.push("/");
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="destructive" disabled={!hasAccess}>
|
||||
Transfer Ownership
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-[425px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-lg font-semibold">
|
||||
Transfer project{project?.name ? ` (${project.name})` : ""}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
You will lose ownership of this project and will become an admin.
|
||||
You cannot undo this action.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<Form {...form}>
|
||||
<form
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="space-y-8"
|
||||
data-testid="new-project-form"
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="newOwnerEmail"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>New owner</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="user@example.com"
|
||||
{...field}
|
||||
data-testid="new-project-name-input"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<Button
|
||||
type="submit"
|
||||
variant="destructive"
|
||||
loading={transferProject.isLoading}
|
||||
className="w-full"
|
||||
>
|
||||
Transfer project
|
||||
</Button>
|
||||
</form>
|
||||
</Form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -67,4 +67,61 @@ export const projectsRouter = createTRPCRouter({
|
||||
|
||||
return true;
|
||||
}),
|
||||
|
||||
transfer: protectedProcedure
|
||||
.input(
|
||||
z.object({
|
||||
projectId: z.string(),
|
||||
newOwnerEmail: z.string().email(),
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
throwIfNoAccess({
|
||||
session: ctx.session,
|
||||
projectId: input.projectId,
|
||||
scope: "project:transfer",
|
||||
});
|
||||
|
||||
// Check if new owner exists
|
||||
const newOwner = await ctx.prisma.user.findUnique({
|
||||
where: {
|
||||
email: input.newOwnerEmail.toLowerCase(),
|
||||
},
|
||||
});
|
||||
if (!newOwner) throw new Error("User not found");
|
||||
if (newOwner.id === ctx.session.user.id)
|
||||
throw new Error("You cannot transfer project to yourself");
|
||||
|
||||
return ctx.prisma.$transaction([
|
||||
// Add new owner, upsert to update role if already exists
|
||||
ctx.prisma.membership.upsert({
|
||||
where: {
|
||||
projectId_userId: {
|
||||
projectId: input.projectId,
|
||||
userId: newOwner.id,
|
||||
},
|
||||
},
|
||||
update: {
|
||||
role: "OWNER",
|
||||
},
|
||||
create: {
|
||||
userId: newOwner.id,
|
||||
projectId: input.projectId,
|
||||
role: "OWNER",
|
||||
},
|
||||
}),
|
||||
// Update old owner to admin
|
||||
ctx.prisma.membership.update({
|
||||
where: {
|
||||
projectId_userId: {
|
||||
projectId: input.projectId,
|
||||
userId: ctx.session.user.id,
|
||||
},
|
||||
},
|
||||
data: {
|
||||
role: "ADMIN",
|
||||
},
|
||||
}),
|
||||
]);
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -104,7 +104,7 @@ function DeleteApiKeyButton(props: { projectId: string; apiKeyId: string }) {
|
||||
scope: "apiKeys:delete",
|
||||
});
|
||||
|
||||
const utils = api.useContext();
|
||||
const utils = api.useUtils();
|
||||
const mutDeleteApiKey = api.apiKeys.delete.useMutation({
|
||||
onSuccess: () => utils.apiKeys.invalidate(),
|
||||
});
|
||||
|
||||
@@ -5,22 +5,25 @@ 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.useContext();
|
||||
const utils = api.useUtils();
|
||||
const posthog = usePostHog();
|
||||
const hasAccess = useHasAccess({
|
||||
projectId: props.projectId,
|
||||
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">
|
||||
|
||||
@@ -15,7 +15,7 @@ export const PublishTraceSwitch = (props: {
|
||||
projectId: props.projectId,
|
||||
scope: "traces:publish",
|
||||
});
|
||||
const utils = api.useContext();
|
||||
const utils = api.useUtils();
|
||||
const mut = api.publishTraces.update.useMutation({
|
||||
onSuccess: () => utils.traces.invalidate(),
|
||||
});
|
||||
|
||||
@@ -51,7 +51,7 @@ export function CreateProjectMemberButton(props: { projectId: string }) {
|
||||
scope: "members:create",
|
||||
});
|
||||
|
||||
const utils = api.useContext();
|
||||
const utils = api.useUtils();
|
||||
const mutCreateProjectMember = api.projectMembers.create.useMutation({
|
||||
onSuccess: () => utils.projectMembers.invalidate(),
|
||||
});
|
||||
|
||||
@@ -12,6 +12,7 @@ import { Button } from "@/src/components/ui/button";
|
||||
import { TrashIcon } from "lucide-react";
|
||||
import { useHasAccess } from "@/src/features/rbac/utils/checkAccess";
|
||||
import { CreateProjectMemberButton } from "@/src/features/rbac/components/CreateProjectMemberButton";
|
||||
import { useSession } from "next-auth/react";
|
||||
|
||||
export function ProjectMembersTable({ projectId }: { projectId: string }) {
|
||||
const hasReadAccess = useHasAccess({
|
||||
@@ -23,7 +24,9 @@ export function ProjectMembersTable({ projectId }: { projectId: string }) {
|
||||
scope: "members:delete",
|
||||
});
|
||||
|
||||
const utils = api.useContext();
|
||||
const session = useSession();
|
||||
|
||||
const utils = api.useUtils();
|
||||
const memberships = api.projectMembers.get.useQuery(
|
||||
{
|
||||
projectId: projectId,
|
||||
@@ -59,7 +62,9 @@ export function ProjectMembersTable({ projectId }: { projectId: string }) {
|
||||
<TableCell>{m.user.name}</TableCell>
|
||||
<TableCell>{m.user.email}</TableCell>
|
||||
<TableCell>{m.role}</TableCell>
|
||||
{hasDeleteAccess && m.role !== "OWNER" ? (
|
||||
{hasDeleteAccess &&
|
||||
m.user.id !== session.data?.user?.id &&
|
||||
m.role !== "OWNER" ? (
|
||||
<TableCell>
|
||||
<Button
|
||||
variant="ghost"
|
||||
|
||||
@@ -10,10 +10,12 @@ const scopes = [
|
||||
"apiKeys:delete",
|
||||
|
||||
"traces:publish",
|
||||
"traces:delete",
|
||||
|
||||
"scores:CUD",
|
||||
|
||||
"project:delete",
|
||||
"project:transfer",
|
||||
|
||||
"datasets:CUD",
|
||||
] as const;
|
||||
@@ -30,8 +32,10 @@ export const roleAccessRights: Record<MembershipRole, Scope[]> = {
|
||||
"apiKeys:create",
|
||||
"apiKeys:delete",
|
||||
"traces:publish",
|
||||
"traces:delete",
|
||||
"scores:CUD",
|
||||
"project:delete",
|
||||
"project:transfer",
|
||||
"datasets:CUD",
|
||||
],
|
||||
ADMIN: [
|
||||
@@ -42,8 +46,8 @@ export const roleAccessRights: Record<MembershipRole, Scope[]> = {
|
||||
"apiKeys:create",
|
||||
"apiKeys:delete",
|
||||
"traces:publish",
|
||||
"traces:delete",
|
||||
"scores:CUD",
|
||||
"project:delete",
|
||||
"datasets:CUD",
|
||||
],
|
||||
MEMBER: ["members:read", "traces:publish", "scores:CUD", "datasets:CUD"],
|
||||
|
||||
@@ -34,13 +34,13 @@ function hasAccess(p: HasAccessParams): boolean {
|
||||
? // MembershipRole
|
||||
p.role
|
||||
: "data" in p.session
|
||||
? // SessionContextValue
|
||||
p.session.data?.user?.projects.find(
|
||||
(project) => project.id === p.projectId,
|
||||
)?.role
|
||||
: // Session
|
||||
p.session.user?.projects.find((project) => project.id === p.projectId)
|
||||
?.role;
|
||||
? // SessionContextValue
|
||||
p.session.data?.user?.projects.find(
|
||||
(project) => project.id === p.projectId,
|
||||
)?.role
|
||||
: // Session
|
||||
p.session.user?.projects.find((project) => project.id === p.projectId)
|
||||
?.role;
|
||||
if (role === undefined) return false;
|
||||
|
||||
return roleAccessRights[role].includes(p.scope);
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
# Telemetry service for Docker deployments
|
||||
|
||||
By default, Langfuse automatically reports basic usage statistics to a centralized server (PostHog).
|
||||
|
||||
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/features/telemetry/index.ts).
|
||||
|
||||
You can opt-out by setting `TELEMETRY_ENABLED=false`.
|
||||
@@ -0,0 +1,272 @@
|
||||
import { VERSION } from "@/src/constants/VERSION";
|
||||
import { prisma } from "@/src/server/db";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { PostHog } from "posthog-node";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
// Safe as it is intended to be public
|
||||
const POSTHOG_API_KEY = "phc_zkMwFajk8ehObUlMth0D7DtPItFnxETi3lmSvyQDrwB";
|
||||
|
||||
// Interval between jobs in milliseconds
|
||||
const JOB_INTERVAL_MINUTES = Prisma.raw("60");
|
||||
|
||||
// Timeout for job in minutes, if job is not finished in this time, it will be retried
|
||||
const JOB_TIMEOUT_MINUTES = Prisma.raw("10");
|
||||
|
||||
export async function telemetry() {
|
||||
try {
|
||||
// Only run in prod
|
||||
if (process.env.NODE_ENV !== "production") return;
|
||||
// Do not run in Lanfuse cloud, separate telemetry is used
|
||||
if (process.env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION !== undefined) return;
|
||||
// Check if telemetry is not disabled
|
||||
if (process.env.TELEMETRY_ENABLED === "false") return;
|
||||
// Do not run in CI
|
||||
if (process.env.CI) return;
|
||||
|
||||
// Check via db cron_jobs table if it is time to run job
|
||||
const job = await jobScheduler();
|
||||
|
||||
if (job.shouldRunJob) {
|
||||
const { jobStartedAt, lastRun, clientId } = job;
|
||||
|
||||
// Run telemetry job
|
||||
await posthogTelemetry({
|
||||
startTimeframe: lastRun,
|
||||
endTimeframe: jobStartedAt,
|
||||
clientId,
|
||||
});
|
||||
|
||||
// Update cron_jobs table
|
||||
await prisma.cronJobs.update({
|
||||
where: { name: "telemetry" },
|
||||
data: { lastRun: jobStartedAt, state: clientId, jobStartedAt: null },
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
// Catch all errors to be sure telemetry does not break the application
|
||||
console.error("Telemetry, unexpected error:", error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a job should be scheduled and returns the necessary information.
|
||||
* @returns A promise that resolves to an object with the following properties:
|
||||
* - shouldRunJob: A boolean indicating whether the job should be run.
|
||||
* - job_started_at: The timestamp when the job was started.
|
||||
* - last_run: The timestamp of the last run of the job, or null if it has never run.
|
||||
* - clientId: A unique clienId that identifies the host.
|
||||
*/
|
||||
async function jobScheduler(): Promise<
|
||||
| { shouldRunJob: false }
|
||||
| {
|
||||
shouldRunJob: true;
|
||||
jobStartedAt: Date;
|
||||
lastRun: Date | null;
|
||||
clientId: string;
|
||||
}
|
||||
> {
|
||||
// Check if job should run, without a lock to not impact performance
|
||||
// "not exists" triggers when this is run for the very first time in a container
|
||||
const checkNoLock = await prisma.$queryRaw<Array<{ status: boolean }>>`
|
||||
SELECT (
|
||||
EXISTS (
|
||||
SELECT 1
|
||||
FROM cron_jobs
|
||||
WHERE name = 'telemetry'
|
||||
AND (last_run IS NULL OR last_run <= (NOW() - INTERVAL '${JOB_INTERVAL_MINUTES} minute'))
|
||||
AND (job_started_at IS NULL OR job_started_at <= (NOW() - INTERVAL '${JOB_TIMEOUT_MINUTES} minute'))
|
||||
)
|
||||
OR NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM cron_jobs
|
||||
WHERE name = 'telemetry'
|
||||
)
|
||||
) AS status;`;
|
||||
// Return if job should not run
|
||||
if (checkNoLock.length !== 1) {
|
||||
console.error("Telemetry failed to check if job should run");
|
||||
return { shouldRunJob: false };
|
||||
}
|
||||
if (checkNoLock[0]!.status === false) return { shouldRunJob: false };
|
||||
|
||||
// Lock table and update job_started_at if no other job was created in the meantime
|
||||
const res = await prisma.$transaction([
|
||||
prisma.$executeRaw`LOCK TABLE cron_jobs IN SHARE ROW EXCLUSIVE MODE`,
|
||||
prisma.$queryRaw<
|
||||
Array<{
|
||||
name: string;
|
||||
last_run: Date | null;
|
||||
job_started_at: Date | null;
|
||||
state: string | null;
|
||||
}>
|
||||
>`INSERT INTO cron_jobs (name, last_run, job_started_at, state)
|
||||
VALUES ('telemetry', NULL, CURRENT_TIMESTAMP, NULL)
|
||||
ON CONFLICT (name)
|
||||
DO UPDATE
|
||||
SET job_started_at = CASE
|
||||
WHEN (cron_jobs.last_run IS NULL OR cron_jobs.last_run <= (NOW() - INTERVAL '${JOB_INTERVAL_MINUTES} minutes'))
|
||||
AND (cron_jobs.job_started_at IS NULL OR cron_jobs.job_started_at <= (NOW() - INTERVAL '${JOB_TIMEOUT_MINUTES} minutes'))
|
||||
THEN CURRENT_TIMESTAMP
|
||||
ELSE cron_jobs.job_started_at
|
||||
END
|
||||
WHERE cron_jobs.name = 'telemetry'
|
||||
AND (cron_jobs.last_run IS NULL OR cron_jobs.last_run <= (NOW() - INTERVAL '${JOB_INTERVAL_MINUTES} minutes'))
|
||||
AND (cron_jobs.job_started_at IS NULL OR cron_jobs.job_started_at <= (NOW() - INTERVAL '${JOB_TIMEOUT_MINUTES} minutes'))
|
||||
RETURNING *`,
|
||||
]);
|
||||
const createJobLocked = res[1];
|
||||
|
||||
// Other job was created in the meantime
|
||||
if (createJobLocked.length !== 1) {
|
||||
console.error("Telemetry job is locked");
|
||||
return { shouldRunJob: false };
|
||||
}
|
||||
|
||||
const jobStartedAt = createJobLocked[0]?.job_started_at ?? null;
|
||||
|
||||
// should not happen
|
||||
if (!jobStartedAt) {
|
||||
console.error("Telemetry failed to create job_started_at");
|
||||
return { shouldRunJob: false };
|
||||
}
|
||||
|
||||
// set vars
|
||||
const lastRun = createJobLocked[0]?.last_run ?? null;
|
||||
const clientId = createJobLocked[0]?.state ?? uuidv4();
|
||||
|
||||
return { shouldRunJob: true, jobStartedAt, lastRun, clientId };
|
||||
}
|
||||
|
||||
async function posthogTelemetry({
|
||||
startTimeframe,
|
||||
endTimeframe,
|
||||
clientId,
|
||||
}: {
|
||||
startTimeframe: Date | null;
|
||||
endTimeframe: Date;
|
||||
clientId: string;
|
||||
}) {
|
||||
try {
|
||||
const posthog = new PostHog(POSTHOG_API_KEY, {
|
||||
host: "https://eu.posthog.com",
|
||||
});
|
||||
if (process.env.NODE_ENV === "development") posthog.debug();
|
||||
|
||||
// Count projects
|
||||
const totalProjects = await prisma.project.count();
|
||||
|
||||
// Count traces
|
||||
const countTraces = await prisma.trace.count({
|
||||
where: {
|
||||
timestamp: {
|
||||
gte: startTimeframe?.toISOString(),
|
||||
lt: endTimeframe.toISOString(),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Count scores
|
||||
const countScores = await prisma.score.count({
|
||||
where: {
|
||||
timestamp: {
|
||||
gte: startTimeframe?.toISOString(),
|
||||
lt: endTimeframe.toISOString(),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Count observations
|
||||
const countObservations = await prisma.observation.count({
|
||||
where: {
|
||||
startTime: {
|
||||
gte: startTimeframe?.toISOString(),
|
||||
lt: endTimeframe.toISOString(),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Count datasets
|
||||
const countDatasets = await prisma.dataset.count({
|
||||
where: {
|
||||
createdAt: {
|
||||
gte: startTimeframe?.toISOString(),
|
||||
lt: endTimeframe.toISOString(),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Count dataset items
|
||||
const countDatasetItems = await prisma.datasetItem.count({
|
||||
where: {
|
||||
createdAt: {
|
||||
gte: startTimeframe?.toISOString(),
|
||||
lt: endTimeframe.toISOString(),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Count dataset runs
|
||||
const countDatasetRuns = await prisma.datasetRuns.count({
|
||||
where: {
|
||||
createdAt: {
|
||||
gte: startTimeframe?.toISOString(),
|
||||
lt: endTimeframe.toISOString(),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Count dataset run items
|
||||
const countDatasetRunItems = await prisma.datasetRunItems.count({
|
||||
where: {
|
||||
createdAt: {
|
||||
gte: startTimeframe?.toISOString(),
|
||||
lt: endTimeframe.toISOString(),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// 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,
|
||||
observations: countObservations,
|
||||
datasets: countDatasets,
|
||||
datasetItems: countDatasetItems,
|
||||
datasetRuns: countDatasetRuns,
|
||||
datasetRunItems: countDatasetRunItems,
|
||||
startTimeframe: startTimeframe?.toISOString(),
|
||||
endTimeframe: endTimeframe.toISOString(),
|
||||
},
|
||||
});
|
||||
|
||||
await posthog.shutdownAsync();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,10 @@ import { createUserEmailPassword } from "@/src/features/auth/lib/emailPassword";
|
||||
import { signupSchema } from "@/src/features/auth/lib/signupSchema";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
/*
|
||||
* Sign-up endpoint (email/password users), creates user in database.
|
||||
* SSO users are created by the NextAuth adapters.
|
||||
*/
|
||||
export default async function handler(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse,
|
||||
@@ -12,6 +16,13 @@ export default async function handler(
|
||||
res.status(422).json({ message: "Sign up is disabled." });
|
||||
return;
|
||||
}
|
||||
if (env.AUTH_DISABLE_USERNAME_PASSWORD === "true") {
|
||||
res.status(422).json({
|
||||
message:
|
||||
"Sign up with email and password is disabled for this instance. Please use SSO.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// parse and type check the request body with zod
|
||||
const validBody = signupSchema.safeParse(req.body);
|
||||
|
||||
@@ -1,137 +0,0 @@
|
||||
import { prisma } from "@/src/server/db";
|
||||
import { type NextApiRequest, type NextApiResponse } from "next";
|
||||
import { PostHog } from "posthog-node";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
// Safe as it is intended to be public
|
||||
const POSTHOG_API_KEY = "phc_zkMwFajk8ehObUlMth0D7DtPItFnxETi3lmSvyQDrwB";
|
||||
|
||||
export default async function handler(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse,
|
||||
) {
|
||||
try {
|
||||
const posthog = new PostHog(POSTHOG_API_KEY, {
|
||||
host: "https://eu.posthog.com",
|
||||
});
|
||||
if (process.env.NODE_ENV === "development") posthog.debug();
|
||||
|
||||
// Time frame is the last time this cron job ran until now
|
||||
const dbCronState = await prisma.cronJobs.findUnique({
|
||||
where: { name: "telemetry" },
|
||||
});
|
||||
const startTimeframe = dbCronState?.lastRun ?? undefined;
|
||||
const clientId = dbCronState?.state ?? uuidv4();
|
||||
const endTimeframe = new Date(Date.now());
|
||||
|
||||
// Count projects
|
||||
const totalProjects = await prisma.project.count();
|
||||
|
||||
// Count traces
|
||||
const countTraces = await prisma.trace.count({
|
||||
where: {
|
||||
timestamp: {
|
||||
gte: startTimeframe?.toISOString(),
|
||||
lt: endTimeframe.toISOString(),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Count scores
|
||||
const countScores = await prisma.score.count({
|
||||
where: {
|
||||
timestamp: {
|
||||
gte: startTimeframe?.toISOString(),
|
||||
lt: endTimeframe.toISOString(),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Count observations
|
||||
const countObservations = await prisma.observation.count({
|
||||
where: {
|
||||
startTime: {
|
||||
gte: startTimeframe?.toISOString(),
|
||||
lt: endTimeframe.toISOString(),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Count datasets
|
||||
const countDatasets = await prisma.dataset.count({
|
||||
where: {
|
||||
createdAt: {
|
||||
gte: startTimeframe?.toISOString(),
|
||||
lt: endTimeframe.toISOString(),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Count dataset items
|
||||
const countDatasetItems = await prisma.datasetItem.count({
|
||||
where: {
|
||||
createdAt: {
|
||||
gte: startTimeframe?.toISOString(),
|
||||
lt: endTimeframe.toISOString(),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Count dataset runs
|
||||
const countDatasetRuns = await prisma.datasetRuns.count({
|
||||
where: {
|
||||
createdAt: {
|
||||
gte: startTimeframe?.toISOString(),
|
||||
lt: endTimeframe.toISOString(),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Count dataset run items
|
||||
const countDatasetRunItems = await prisma.datasetRunItems.count({
|
||||
where: {
|
||||
createdAt: {
|
||||
gte: startTimeframe?.toISOString(),
|
||||
lt: endTimeframe.toISOString(),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
posthog.identify({
|
||||
distinctId: "docker:" + clientId,
|
||||
properties: {
|
||||
environment: process.env.NODE_ENV,
|
||||
docker: true,
|
||||
},
|
||||
});
|
||||
posthog.capture({
|
||||
distinctId: "docker:" + clientId,
|
||||
event: "telemetry",
|
||||
properties: {
|
||||
totalProjects: totalProjects,
|
||||
traces: countTraces,
|
||||
scores: countScores,
|
||||
observations: countObservations,
|
||||
datasets: countDatasets,
|
||||
datasetItems: countDatasetItems,
|
||||
datasetRuns: countDatasetRuns,
|
||||
datasetRunItems: countDatasetRunItems,
|
||||
startTimeframe: startTimeframe?.toISOString(),
|
||||
endTimeframe: endTimeframe.toISOString(),
|
||||
},
|
||||
});
|
||||
|
||||
await posthog.shutdownAsync();
|
||||
|
||||
await prisma.cronJobs.upsert({
|
||||
where: { name: "telemetry" },
|
||||
update: { lastRun: endTimeframe, state: clientId },
|
||||
create: { name: "telemetry", lastRun: endTimeframe, state: clientId },
|
||||
});
|
||||
|
||||
return res.status(200).json({ message: "OK" });
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return res.status(500).json({ message: "Internal server error" });
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { cors, runMiddleware } from "@/src/features/public-api/server/cors";
|
||||
import { telemetry } from "@/src/features/telemetry";
|
||||
import { prisma } from "@/src/server/db";
|
||||
import { type NextApiRequest, type NextApiResponse } from "next";
|
||||
|
||||
@@ -7,10 +8,11 @@ export default async function handler(
|
||||
res: NextApiResponse,
|
||||
) {
|
||||
await runMiddleware(req, res, cors);
|
||||
await telemetry();
|
||||
try {
|
||||
await prisma.$queryRaw`SELECT 1;`;
|
||||
} catch (e) {
|
||||
return res.status(500).json({ status: "Database not available" });
|
||||
return res.status(503).json({ status: "Database not available" });
|
||||
}
|
||||
return res.status(200).json({ status: "OK" });
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import { ObservationProcessor } from "../../../server/api/services/EventProcesso
|
||||
import { TraceProcessor } from "../../../server/api/services/EventProcessor";
|
||||
import { ScoreProcessor } from "../../../server/api/services/EventProcessor";
|
||||
import { isNotNullOrUndefined } from "@/src/utils/types";
|
||||
import { telemetry } from "@/src/features/telemetry";
|
||||
|
||||
export default async function handler(
|
||||
req: NextApiRequest,
|
||||
@@ -81,6 +82,8 @@ export default async function handler(
|
||||
const filteredBatch: z.infer<typeof singleEventSchema>[] =
|
||||
batch.filter(isNotNullOrUndefined);
|
||||
|
||||
await telemetry();
|
||||
|
||||
const sortedBatch = sortBatch(filteredBatch);
|
||||
const result = await handleBatch(sortedBatch, req, authCheck);
|
||||
|
||||
@@ -242,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" });
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
eventTypes,
|
||||
} from "@/src/features/public-api/server/ingestion-api-schema";
|
||||
import { v4 } from "uuid";
|
||||
import { telemetry } from "@/src/features/telemetry";
|
||||
|
||||
const GetTracesSchema = z.object({
|
||||
...paginationZod,
|
||||
@@ -52,6 +53,9 @@ export default async function handler(
|
||||
});
|
||||
|
||||
const body = TraceSchema.parse(req.body);
|
||||
|
||||
await telemetry();
|
||||
|
||||
const event = {
|
||||
id: v4(),
|
||||
type: eventTypes.TRACE_CREATE,
|
||||
|
||||
+75
-52
@@ -21,6 +21,9 @@ import { useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import * as z from "zod";
|
||||
import { usePostHog } from "posthog-js/react";
|
||||
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(),
|
||||
@@ -29,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;
|
||||
@@ -37,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 {
|
||||
@@ -48,13 +53,60 @@ export const getServerSideProps: GetServerSideProps<PageProps> = async () => {
|
||||
github:
|
||||
env.AUTH_GITHUB_CLIENT_ID !== undefined &&
|
||||
env.AUTH_GITHUB_CLIENT_SECRET !== undefined,
|
||||
credentials: true,
|
||||
credentials: env.AUTH_DISABLE_USERNAME_PASSWORD !== "true",
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
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);
|
||||
@@ -98,12 +150,13 @@ export default function SignIn(props: PageProps) {
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div className="mt-14 sm:mx-auto sm:w-full sm:max-w-[480px]">
|
||||
<div className="divide-y bg-white p-6 py-6 shadow sm:rounded-lg sm:px-12">
|
||||
{props.authProviders.credentials ? (
|
||||
<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="space-y-8">
|
||||
<CloudRegionSwitch />
|
||||
{authProviders.credentials ? (
|
||||
<Form {...credentialsForm}>
|
||||
<form
|
||||
className="space-y-6 py-6"
|
||||
className="space-y-6"
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||
onSubmit={credentialsForm.handleSubmit(onCredentialsSubmit)}
|
||||
>
|
||||
@@ -150,53 +203,23 @@ export default function SignIn(props: PageProps) {
|
||||
</form>
|
||||
</Form>
|
||||
) : null}
|
||||
|
||||
{
|
||||
// any authprovider from props is enanbles
|
||||
Object.values(props.authProviders).some((enabled) => enabled) ? (
|
||||
<div className="flex flex-row flex-wrap items-center justify-center gap-4 py-6">
|
||||
{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>
|
||||
) : null
|
||||
}
|
||||
<SSOButtons authProviders={authProviders} />
|
||||
</div>
|
||||
|
||||
{env.NEXT_PUBLIC_SIGN_UP_DISABLED !== "true" &&
|
||||
props.authProviders.credentials ? (
|
||||
<p className="mt-10 text-center text-sm text-gray-500">
|
||||
No account yet?{" "}
|
||||
<Link
|
||||
href="/auth/sign-up"
|
||||
className="font-semibold leading-6 text-indigo-600 hover:text-indigo-500"
|
||||
>
|
||||
Sign up
|
||||
</Link>
|
||||
</p>
|
||||
) : null}
|
||||
<CloudPrivacyNotice action="signing in" />
|
||||
</div>
|
||||
|
||||
{env.NEXT_PUBLIC_SIGN_UP_DISABLED !== "true" &&
|
||||
authProviders.credentials ? (
|
||||
<p className="mt-10 text-center text-sm text-gray-500">
|
||||
No account yet?{" "}
|
||||
<Link
|
||||
href="/auth/sign-up"
|
||||
className="font-semibold leading-6 text-indigo-600 hover:text-indigo-500"
|
||||
>
|
||||
Sign up
|
||||
</Link>
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
+98
-120
@@ -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,130 +90,102 @@ 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 sm:mx-auto sm:w-full sm:max-w-[480px]">
|
||||
<div className="bg-white px-6 py-12 shadow sm:rounded-lg sm:px-12">
|
||||
<Form {...form}>
|
||||
<form
|
||||
className="space-y-6"
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="Jane Doe" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="email"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Email</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="jsdoe@example.com" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="password"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Password</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="password" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION !== undefined ? (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="referralSource"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
Where did you hear about us?{" "}
|
||||
<span className="font-normal">(optional)</span>
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="referralSource" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
) : null}
|
||||
<Button
|
||||
type="submit"
|
||||
className="w-full"
|
||||
loading={form.formState.isSubmitting}
|
||||
>
|
||||
Sign up
|
||||
</Button>
|
||||
{formError ? (
|
||||
<div className="text-center text-sm font-medium text-destructive">
|
||||
{formError}
|
||||
</div>
|
||||
) : null}
|
||||
</form>
|
||||
</Form>
|
||||
{env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION !== undefined ? (
|
||||
<div className="-mb-4 mt-8 text-center text-xs text-gray-500">
|
||||
By creating an account you are agreeing to our{" "}
|
||||
<a
|
||||
href="https://app.termly.io/document/terms-of-service/baf80a2e-dc67-46de-9ca8-2f7457179c32"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="italic"
|
||||
>
|
||||
Terms of Service
|
||||
</a>
|
||||
,{" "}
|
||||
<a
|
||||
href="https://app.termly.io/document/privacy-policy/47905712-56e1-4ad0-9bb7-8958f3263f90"
|
||||
rel="noopener noreferrer"
|
||||
className="italic"
|
||||
>
|
||||
Privacy Policy
|
||||
</a>
|
||||
, and{" "}
|
||||
<a
|
||||
href="https://app.termly.io/document/cookie-policy/f97945a3-cb02-4db7-9370-c57023d92838"
|
||||
rel="noopener noreferrer"
|
||||
className="italic"
|
||||
>
|
||||
Cookie Policy
|
||||
</a>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<p className="mt-10 text-center text-sm text-gray-500">
|
||||
Already have an account?{" "}
|
||||
<Link
|
||||
href="/auth/sign-in"
|
||||
className="font-semibold leading-6 text-indigo-600 hover:text-indigo-500"
|
||||
<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"
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
>
|
||||
Sign in
|
||||
</Link>
|
||||
</p>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="Jane Doe" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="email"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Email</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="jsdoe@example.com" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="password"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Password</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="password" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION !== undefined ? (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="referralSource"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
Where did you hear about us?{" "}
|
||||
<span className="font-normal">(optional)</span>
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="referralSource" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
) : null}
|
||||
<Button
|
||||
type="submit"
|
||||
className="w-full"
|
||||
loading={form.formState.isSubmitting}
|
||||
>
|
||||
Sign up
|
||||
</Button>
|
||||
{formError ? (
|
||||
<div className="text-center text-sm font-medium text-destructive">
|
||||
{formError}
|
||||
</div>
|
||||
) : null}
|
||||
</form>
|
||||
</Form>
|
||||
<SSOButtons authProviders={authProviders} action="Sign up" />
|
||||
<CloudPrivacyNotice action="creating an account" />
|
||||
</div>
|
||||
|
||||
<p className="mt-10 text-center text-sm text-gray-500">
|
||||
Already have an account?{" "}
|
||||
<Link
|
||||
href="/auth/sign-in"
|
||||
className="font-semibold leading-6 text-indigo-600 hover:text-indigo-500"
|
||||
>
|
||||
Sign in
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
+16
-29
@@ -9,47 +9,34 @@ import {
|
||||
import Link from "next/link";
|
||||
import { NewProjectButton } from "@/src/features/projects/components/NewProjectButton";
|
||||
import Header from "@/src/components/layouts/header";
|
||||
import { api } from "@/src/utils/api";
|
||||
import { useRouter } from "next/router";
|
||||
import { useState, useEffect } from "react";
|
||||
import { env } from "@/src/env.mjs";
|
||||
import { cn } from "@/src/utils/tailwind";
|
||||
import { useSession } from "next-auth/react";
|
||||
import { Spinner } from "@/src/components/layouts/spinner";
|
||||
|
||||
export default function GetStartedPage() {
|
||||
const projects = api.projects.all.useQuery();
|
||||
const router = useRouter();
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
// check url query for welcome = 1
|
||||
const getStarted = router.query.getStarted === "1";
|
||||
|
||||
useEffect(() => {
|
||||
if (projects.data) {
|
||||
if (
|
||||
projects.data.filter((p) => p.id !== env.NEXT_PUBLIC_DEMO_PROJECT_ID)
|
||||
.length > 0 &&
|
||||
loading === true &&
|
||||
!getStarted
|
||||
)
|
||||
void router.push(
|
||||
`/project/${
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
projects.data.filter(
|
||||
(p) => p.id !== env.NEXT_PUBLIC_DEMO_PROJECT_ID,
|
||||
)[0]!.id
|
||||
}`,
|
||||
);
|
||||
else setLoading(false);
|
||||
}
|
||||
}, [projects.data, router, loading, getStarted]);
|
||||
const session = useSession();
|
||||
const projects = session.data?.user?.projects;
|
||||
const redirectProject = projects?.filter(
|
||||
(p) => p.id !== env.NEXT_PUBLIC_DEMO_PROJECT_ID,
|
||||
)[0];
|
||||
|
||||
if (loading || projects.status === "loading") {
|
||||
return <div>Loading...</div>;
|
||||
if (session.status === "authenticated" && redirectProject && !getStarted) {
|
||||
void router.push(`/project/${redirectProject.id}`);
|
||||
return <Spinner message="Redirecting" />;
|
||||
}
|
||||
|
||||
if (session.status === "loading") {
|
||||
return <Spinner message="Loading" />;
|
||||
}
|
||||
|
||||
const demoProject =
|
||||
env.NEXT_PUBLIC_DEMO_PROJECT_ID !== undefined && projects.data?.length
|
||||
? projects.data.find(
|
||||
env.NEXT_PUBLIC_DEMO_PROJECT_ID !== undefined
|
||||
? projects?.find(
|
||||
(project) => project.id === env.NEXT_PUBLIC_DEMO_PROJECT_ID,
|
||||
)
|
||||
: undefined;
|
||||
|
||||
@@ -8,6 +8,7 @@ import { Code, Bird, GraduationCap } from "lucide-react";
|
||||
import { ProjectMembersTable } from "@/src/features/rbac/components/ProjectMembersTable";
|
||||
import { DeleteProjectButton } from "@/src/features/projects/components/DeleteProjectButton";
|
||||
import { ProjectUsageChart } from "@/src/features/usage-metering/ProjectUsageChart";
|
||||
import { TransferOwnershipButton } from "@/src/features/projects/components/TransferOwnershipButton";
|
||||
|
||||
export default function SettingsPage() {
|
||||
const router = useRouter();
|
||||
@@ -20,7 +21,10 @@ export default function SettingsPage() {
|
||||
<ApiKeyList projectId={projectId} />
|
||||
<ProjectUsageChart projectId={projectId} />
|
||||
<Instructions />
|
||||
<DeleteProjectButton projectId={projectId} />
|
||||
<div className="space-y-3">
|
||||
<DeleteProjectButton projectId={projectId} />
|
||||
<TransferOwnershipButton projectId={projectId} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
datetimeFilterToPrismaSql,
|
||||
filterToPrismaSql,
|
||||
} from "@/src/features/filters/server/filterToPrisma";
|
||||
import { throwIfNoAccess } from "@/src/features/rbac/utils/checkAccess";
|
||||
|
||||
const TraceFilterOptions = z.object({
|
||||
projectId: z.string(), // Required for protectedProjectProcedure
|
||||
@@ -246,9 +247,9 @@ export const traceRouter = createTRPCRouter({
|
||||
? (obsEndTimes[obsEndTimes.length - 1] as Date).getTime() -
|
||||
obsStartTimes[0]!.getTime()
|
||||
: obsStartTimes.length > 1
|
||||
? obsStartTimes[obsStartTimes.length - 1]!.getTime() -
|
||||
obsStartTimes[0]!.getTime()
|
||||
: undefined
|
||||
? obsStartTimes[obsStartTimes.length - 1]!.getTime() -
|
||||
obsStartTimes[0]!.getTime()
|
||||
: undefined
|
||||
: undefined;
|
||||
|
||||
const enrichedObservations = observations.map(
|
||||
@@ -275,4 +276,37 @@ export const traceRouter = createTRPCRouter({
|
||||
observations: enrichedObservations as ObservationReturnType[],
|
||||
};
|
||||
}),
|
||||
delete: protectedProjectProcedure
|
||||
.input(z.object({ traceId: z.string(), projectId: z.string() }))
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
throwIfNoAccess({
|
||||
session: ctx.session,
|
||||
projectId: input.projectId,
|
||||
scope: "traces:delete",
|
||||
});
|
||||
|
||||
const trace = await ctx.prisma.trace.findFirst({
|
||||
where: {
|
||||
id: input.traceId,
|
||||
projectId: input.projectId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!trace) {
|
||||
throw new Error("Trace not found in project");
|
||||
}
|
||||
|
||||
return ctx.prisma.$transaction([
|
||||
ctx.prisma.trace.delete({
|
||||
where: {
|
||||
id: input.traceId,
|
||||
},
|
||||
}),
|
||||
ctx.prisma.observation.deleteMany({
|
||||
where: {
|
||||
traceId: input.traceId,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -52,6 +52,10 @@ const providers: Provider[] = [
|
||||
},
|
||||
async authorize(credentials, _req) {
|
||||
if (!credentials) throw new Error("No credentials");
|
||||
if (env.AUTH_DISABLE_USERNAME_PASSWORD === "true")
|
||||
throw new Error(
|
||||
"Sign in with email and password is disabled for this instance. Please use SSO.",
|
||||
);
|
||||
|
||||
const blockedDomains =
|
||||
env.AUTH_DOMAINS_WITH_SSO_ENFORCEMENT?.split(",") ?? [];
|
||||
|
||||
Reference in New Issue
Block a user