Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c360cf10d4 | ||
|
|
3e2f3f3d28 | ||
|
|
11e5da2cd8 | ||
|
|
8205595f2c | ||
|
|
101277cab8 | ||
|
|
954e31ca4a | ||
|
|
43c3258135 | ||
|
|
eb8ae94a5d | ||
|
|
33abe53acc | ||
|
|
5e8ec0bd13 | ||
|
|
177f370914 | ||
|
|
5538242649 | ||
|
|
8c8e58e2b8 | ||
|
|
a2c367c1b6 | ||
|
|
da0ac73603 | ||
|
|
e60e74478c | ||
|
|
329f83e844 | ||
|
|
28c035da76 | ||
|
|
4dfeceb9d6 | ||
|
|
df802184a6 | ||
|
|
407d169095 | ||
|
|
e1958e73d4 | ||
|
|
2a08c3395c | ||
|
|
5c405f4ed8 | ||
|
|
25cc29f353 |
@@ -18,16 +18,25 @@ types:
|
||||
public:
|
||||
type: optional<boolean>
|
||||
docs: Public traces are accessible via url without login
|
||||
TraceWithDetails:
|
||||
TraceWithDetails: # GET /traces
|
||||
extends: Trace
|
||||
properties:
|
||||
htmlPath:
|
||||
type: string
|
||||
docs: Path of trace in Langfuse UI
|
||||
latency:
|
||||
type: double
|
||||
docs: Latency of trace in seconds
|
||||
totalCost:
|
||||
type: double
|
||||
docs: Cost of trace in USD.
|
||||
observations:
|
||||
type: list<string>
|
||||
docs: List of observation ids
|
||||
scores:
|
||||
type: list<string>
|
||||
docs: List of score ids
|
||||
TraceWithFullDetails:
|
||||
TraceWithFullDetails: # GET traces/[traceID]
|
||||
extends: Trace
|
||||
properties:
|
||||
observations: list<ObservationsView>
|
||||
@@ -72,6 +81,7 @@ types:
|
||||
calculatedInputCost: optional<double>
|
||||
calculatedOutputCost: optional<double>
|
||||
calculatedTotalCost: optional<double>
|
||||
latency: optional<double>
|
||||
|
||||
Usage:
|
||||
properties:
|
||||
|
||||
@@ -128,6 +128,7 @@ types:
|
||||
TraceBody:
|
||||
properties:
|
||||
id: optional<string>
|
||||
timestamp: optional<datetime>
|
||||
name: optional<string>
|
||||
userId: optional<string>
|
||||
input: optional<unknown>
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
# yaml-language-server: $schema=https://raw.githubusercontent.com/fern-api/fern/main/fern.schema.json
|
||||
imports:
|
||||
pagination: ./utils/pagination.yml
|
||||
commons: ./commons.yml
|
||||
service:
|
||||
auth: true
|
||||
base-path: /api/public
|
||||
endpoints:
|
||||
daily:
|
||||
docs: Get daily metrics of the Langfuse project
|
||||
method: GET
|
||||
path: /metrics/daily
|
||||
request:
|
||||
name: GetDailyMetricsRequest
|
||||
query-parameters:
|
||||
page: optional<integer>
|
||||
limit: optional<integer>
|
||||
traceName:
|
||||
type: optional<string>
|
||||
docs: Optional filter by the name of the trace
|
||||
userId:
|
||||
type: optional<string>
|
||||
docs: Optional filter by the userId associated with the trace
|
||||
tags:
|
||||
type: optional<string>
|
||||
allow-multiple: true
|
||||
docs: Optional filter for metrics where traces include all of these tags
|
||||
response: DailyMetrics
|
||||
types:
|
||||
DailyMetrics:
|
||||
properties:
|
||||
data:
|
||||
type: list<DailyMetricsDetails>
|
||||
docs: A list of daily metrics, only days with ingested data are included.
|
||||
meta: pagination.MetaResponse
|
||||
DailyMetricsDetails:
|
||||
properties:
|
||||
date: date
|
||||
countTraces: integer
|
||||
totalCost: double
|
||||
usage: list<UsageByModel>
|
||||
UsageByModel:
|
||||
docs: Daily usage of a given model. Usage corresponds to the unit set for the specific model (e.g. tokens).
|
||||
properties:
|
||||
model: string
|
||||
inputUsage: integer
|
||||
outputUsage: integer
|
||||
totalUsage: integer
|
||||
@@ -24,6 +24,14 @@ service:
|
||||
userId: optional<string>
|
||||
name: optional<string>
|
||||
response: Scores
|
||||
delete:
|
||||
docs: Delete a score
|
||||
method: DELETE
|
||||
path: /scores/{scoreId}
|
||||
path-parameters:
|
||||
scoreId:
|
||||
type: string
|
||||
docs: The unique langfuse identifier of a score
|
||||
types:
|
||||
CreateScoreRequest:
|
||||
properties:
|
||||
|
||||
@@ -387,6 +387,81 @@ paths:
|
||||
$ref: '#/components/schemas/IngestionEvent'
|
||||
required:
|
||||
- batch
|
||||
/api/public/metrics/daily:
|
||||
get:
|
||||
description: Get daily metrics of the Langfuse project
|
||||
operationId: metrics_daily
|
||||
tags:
|
||||
- Metrics
|
||||
parameters:
|
||||
- name: page
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: integer
|
||||
nullable: true
|
||||
- name: limit
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: integer
|
||||
nullable: true
|
||||
- name: traceName
|
||||
in: query
|
||||
description: Optional filter by the name of the trace
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
nullable: true
|
||||
- name: userId
|
||||
in: query
|
||||
description: Optional filter by the userId associated with the trace
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
nullable: true
|
||||
- name: tags
|
||||
in: query
|
||||
description: Optional filter for metrics where traces include all of these tags
|
||||
required: false
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
nullable: true
|
||||
responses:
|
||||
'200':
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/DailyMetrics'
|
||||
'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/observations/{observationId}:
|
||||
get:
|
||||
description: Get a specific observation
|
||||
@@ -761,6 +836,48 @@ paths:
|
||||
application/json:
|
||||
schema: {}
|
||||
security: *ref_0
|
||||
/api/public/scores/{scoreId}:
|
||||
delete:
|
||||
description: Delete a score
|
||||
operationId: score_delete
|
||||
tags:
|
||||
- Score
|
||||
parameters:
|
||||
- name: scoreId
|
||||
in: path
|
||||
description: The unique langfuse identifier of a score
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
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: {}
|
||||
security: *ref_0
|
||||
/api/public/sessions/{sessionId}:
|
||||
get:
|
||||
description: Get a session
|
||||
@@ -983,6 +1100,17 @@ components:
|
||||
title: TraceWithDetails
|
||||
type: object
|
||||
properties:
|
||||
htmlPath:
|
||||
type: string
|
||||
description: Path of trace in Langfuse UI
|
||||
latency:
|
||||
type: number
|
||||
format: double
|
||||
description: Latency of trace in seconds
|
||||
totalCost:
|
||||
type: number
|
||||
format: double
|
||||
description: Cost of trace in USD.
|
||||
observations:
|
||||
type: array
|
||||
items:
|
||||
@@ -994,6 +1122,9 @@ components:
|
||||
type: string
|
||||
description: List of score ids
|
||||
required:
|
||||
- htmlPath
|
||||
- latency
|
||||
- totalCost
|
||||
- observations
|
||||
- scores
|
||||
allOf:
|
||||
@@ -1134,6 +1265,10 @@ components:
|
||||
type: number
|
||||
format: double
|
||||
nullable: true
|
||||
latency:
|
||||
type: number
|
||||
format: double
|
||||
nullable: true
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/Observation'
|
||||
Usage:
|
||||
@@ -1720,6 +1855,10 @@ components:
|
||||
id:
|
||||
type: string
|
||||
nullable: true
|
||||
timestamp:
|
||||
type: string
|
||||
format: date-time
|
||||
nullable: true
|
||||
name:
|
||||
type: string
|
||||
nullable: true
|
||||
@@ -1936,6 +2075,60 @@ components:
|
||||
required:
|
||||
- successes
|
||||
- errors
|
||||
DailyMetrics:
|
||||
title: DailyMetrics
|
||||
type: object
|
||||
properties:
|
||||
data:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/DailyMetricsDetails'
|
||||
description: A list of daily metrics, only days with ingested data are included.
|
||||
meta:
|
||||
$ref: '#/components/schemas/utilsMetaResponse'
|
||||
required:
|
||||
- data
|
||||
- meta
|
||||
DailyMetricsDetails:
|
||||
title: DailyMetricsDetails
|
||||
type: object
|
||||
properties:
|
||||
date:
|
||||
type: string
|
||||
countTraces:
|
||||
type: integer
|
||||
totalCost:
|
||||
type: number
|
||||
format: double
|
||||
usage:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/UsageByModel'
|
||||
required:
|
||||
- date
|
||||
- countTraces
|
||||
- totalCost
|
||||
- usage
|
||||
UsageByModel:
|
||||
title: UsageByModel
|
||||
type: object
|
||||
description: >-
|
||||
Daily usage of a given model. Usage corresponds to the unit set for the
|
||||
specific model (e.g. tokens).
|
||||
properties:
|
||||
model:
|
||||
type: string
|
||||
inputUsage:
|
||||
type: integer
|
||||
outputUsage:
|
||||
type: integer
|
||||
totalUsage:
|
||||
type: integer
|
||||
required:
|
||||
- model
|
||||
- inputUsage
|
||||
- outputUsage
|
||||
- totalUsage
|
||||
Observations:
|
||||
title: Observations
|
||||
type: object
|
||||
|
||||
@@ -320,7 +320,7 @@
|
||||
"auth": null,
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"batch\": [\n {\n \"type\": \"trace-create\",\n \"body\": {\n \"id\": \"example\",\n \"name\": \"example\",\n \"userId\": \"example\",\n \"input\": \"UNKNOWN\",\n \"output\": \"UNKNOWN\",\n \"sessionId\": \"example\",\n \"release\": \"example\",\n \"version\": \"example\",\n \"metadata\": \"UNKNOWN\",\n \"tags\": [\n \"example\"\n ],\n \"public\": true\n },\n \"id\": \"example\",\n \"timestamp\": \"example\",\n \"metadata\": \"UNKNOWN\"\n }\n ]\n}",
|
||||
"raw": "{\n \"batch\": [\n {\n \"type\": \"trace-create\",\n \"body\": {\n \"id\": \"example\",\n \"timestamp\": \"1994-11-05T13:15:30Z\",\n \"name\": \"example\",\n \"userId\": \"example\",\n \"input\": \"UNKNOWN\",\n \"output\": \"UNKNOWN\",\n \"sessionId\": \"example\",\n \"release\": \"example\",\n \"version\": \"example\",\n \"metadata\": \"UNKNOWN\",\n \"tags\": [\n \"example\"\n ],\n \"public\": true\n },\n \"id\": \"example\",\n \"timestamp\": \"example\",\n \"metadata\": \"UNKNOWN\"\n }\n ]\n}",
|
||||
"options": {
|
||||
"raw": {
|
||||
"language": "json"
|
||||
@@ -332,6 +332,65 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"_type": "container",
|
||||
"description": null,
|
||||
"name": "Metrics",
|
||||
"item": [
|
||||
{
|
||||
"_type": "endpoint",
|
||||
"name": "Daily",
|
||||
"request": {
|
||||
"description": "Get daily metrics of the Langfuse project",
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/public/metrics/daily?page=&limit=&traceName=&userId=&tags=",
|
||||
"host": [
|
||||
"{{baseUrl}}"
|
||||
],
|
||||
"path": [
|
||||
"api",
|
||||
"public",
|
||||
"metrics",
|
||||
"daily"
|
||||
],
|
||||
"query": [
|
||||
{
|
||||
"key": "page",
|
||||
"value": "",
|
||||
"description": null
|
||||
},
|
||||
{
|
||||
"key": "limit",
|
||||
"value": "",
|
||||
"description": null
|
||||
},
|
||||
{
|
||||
"key": "traceName",
|
||||
"value": "",
|
||||
"description": "Optional filter by the name of the trace"
|
||||
},
|
||||
{
|
||||
"key": "userId",
|
||||
"value": "",
|
||||
"description": "Optional filter by the userId associated with the trace"
|
||||
},
|
||||
{
|
||||
"key": "tags",
|
||||
"value": "",
|
||||
"description": "Optional filter for metrics where traces include all of these tags"
|
||||
}
|
||||
],
|
||||
"variable": []
|
||||
},
|
||||
"header": [],
|
||||
"method": "GET",
|
||||
"auth": null,
|
||||
"body": null
|
||||
},
|
||||
"response": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"_type": "container",
|
||||
"description": null,
|
||||
@@ -623,6 +682,38 @@
|
||||
"body": null
|
||||
},
|
||||
"response": []
|
||||
},
|
||||
{
|
||||
"_type": "endpoint",
|
||||
"name": "Delete",
|
||||
"request": {
|
||||
"description": "Delete a score",
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/public/scores/:scoreId",
|
||||
"host": [
|
||||
"{{baseUrl}}"
|
||||
],
|
||||
"path": [
|
||||
"api",
|
||||
"public",
|
||||
"scores",
|
||||
":scoreId"
|
||||
],
|
||||
"query": [],
|
||||
"variable": [
|
||||
{
|
||||
"key": "scoreId",
|
||||
"value": "",
|
||||
"description": "The unique langfuse identifier of a score"
|
||||
}
|
||||
]
|
||||
},
|
||||
"header": [],
|
||||
"method": "DELETE",
|
||||
"auth": null,
|
||||
"body": null
|
||||
},
|
||||
"response": []
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
Generated
+268
-318
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "langfuse-core",
|
||||
"version": "2.4.3",
|
||||
"version": "2.6.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "langfuse-core",
|
||||
"version": "2.4.3",
|
||||
"version": "2.6.0",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@anthropic-ai/tokenizer": "^0.0.4",
|
||||
@@ -40,8 +40,8 @@
|
||||
"@radix-ui/react-tooltip": "^1.0.7",
|
||||
"@react-email/components": "^0.0.14",
|
||||
"@react-email/render": "^0.0.12",
|
||||
"@sentry/nextjs": "^7.100.1",
|
||||
"@sentry/profiling-node": "^7.100.1",
|
||||
"@sentry/nextjs": "^7.101.1",
|
||||
"@sentry/profiling-node": "^7.101.1",
|
||||
"@sentry/types": "^7.88.0",
|
||||
"@t3-oss/env-nextjs": "^0.8.0",
|
||||
"@tailwindcss/forms": "^0.5.7",
|
||||
@@ -56,7 +56,7 @@
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.1.0",
|
||||
"cmdk": "^0.2.1",
|
||||
"core-js": "^3.35.1",
|
||||
"core-js": "^3.36.0",
|
||||
"cors": "^2.8.5",
|
||||
"date-fns": "^3.3.1",
|
||||
"decimal.js": "^10.4.3",
|
||||
@@ -65,11 +65,11 @@
|
||||
"lodash": "^4.17.21",
|
||||
"lucide-react": "^0.330.0",
|
||||
"next": "^14.1.0",
|
||||
"next-auth": "^4.24.5",
|
||||
"next-auth": "^4.24.6",
|
||||
"next-query-params": "^5.0.0",
|
||||
"nodemailer": "^6.9.9",
|
||||
"posthog-js": "^1.105.7",
|
||||
"posthog-node": "^3.6.2",
|
||||
"posthog-js": "^1.105.9",
|
||||
"posthog-node": "^3.6.3",
|
||||
"react": "18.2.0",
|
||||
"react-day-picker": "^8.10.0",
|
||||
"react-dom": "18.2.0",
|
||||
@@ -115,7 +115,7 @@
|
||||
"prettier-plugin-tailwindcss": "^0.5.11",
|
||||
"prisma": "^5.9.1",
|
||||
"prisma-erd-generator": "^1.11.2",
|
||||
"release-it": "^17.0.3",
|
||||
"release-it": "^17.0.5",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"ts-node": "^10.9.2",
|
||||
"tsconfig-paths": "^4.2.0",
|
||||
@@ -2898,12 +2898,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@ljharb/through": {
|
||||
"version": "2.3.11",
|
||||
"resolved": "https://registry.npmjs.org/@ljharb/through/-/through-2.3.11.tgz",
|
||||
"integrity": "sha512-ccfcIDlogiXNq5KcbAwbaO7lMh3Tm1i3khMPYpxlK8hH/W53zN81KM9coerRLOnTGu3nfXIniAmQbRI9OxbC0w==",
|
||||
"version": "2.3.12",
|
||||
"resolved": "https://registry.npmjs.org/@ljharb/through/-/through-2.3.12.tgz",
|
||||
"integrity": "sha512-ajo/heTlG3QgC8EGP6APIejksVAYt4ayz4tqoP3MolFELzcH1x1fzwEYRJTPO0IELutZ5HQ0c26/GqAYy79u3g==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"call-bind": "^1.0.2"
|
||||
"call-bind": "^1.0.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
@@ -4989,57 +4989,57 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry-internal/feedback": {
|
||||
"version": "7.100.1",
|
||||
"resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-7.100.1.tgz",
|
||||
"integrity": "sha512-yqcRVnjf+qS+tC4NxOKLJOaSJ+csHmh/dHUzvCTkf5rLsplwXYRnny2r0tqGTQ4tuXMxwgSMKPYwicg81P+xuw==",
|
||||
"version": "7.101.1",
|
||||
"resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-7.101.1.tgz",
|
||||
"integrity": "sha512-fOKDMVvLX+FuJHJszKBvRg1m7+fd4hchqRnZ9DDfitT6P5Ppl0gbEt/LStqu8Wq5M0tna+hpdwHlVEt7gZVKzw==",
|
||||
"dependencies": {
|
||||
"@sentry/core": "7.100.1",
|
||||
"@sentry/types": "7.100.1",
|
||||
"@sentry/utils": "7.100.1"
|
||||
"@sentry/core": "7.101.1",
|
||||
"@sentry/types": "7.101.1",
|
||||
"@sentry/utils": "7.101.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry-internal/replay-canvas": {
|
||||
"version": "7.100.1",
|
||||
"resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-7.100.1.tgz",
|
||||
"integrity": "sha512-TnqxqJGhbFhhYRhTG2WLFer+lVieV7mNGeIxFBiw1L4kuj8KGl+C0sknssKyZSRVJFSahhHIosHJGRMkkD//7g==",
|
||||
"version": "7.101.1",
|
||||
"resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-7.101.1.tgz",
|
||||
"integrity": "sha512-09l6nD+lxWvwkpXLlIZuzj/z79Llbo6mcH33TJvxrUTjAqSGF/i3Pd5bTLWro9atippOyQgIV/yTGG4Bc5FhyQ==",
|
||||
"dependencies": {
|
||||
"@sentry/core": "7.100.1",
|
||||
"@sentry/replay": "7.100.1",
|
||||
"@sentry/types": "7.100.1",
|
||||
"@sentry/utils": "7.100.1"
|
||||
"@sentry/core": "7.101.1",
|
||||
"@sentry/replay": "7.101.1",
|
||||
"@sentry/types": "7.101.1",
|
||||
"@sentry/utils": "7.101.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry-internal/tracing": {
|
||||
"version": "7.100.1",
|
||||
"resolved": "https://registry.npmjs.org/@sentry-internal/tracing/-/tracing-7.100.1.tgz",
|
||||
"integrity": "sha512-+u9RRf5eL3StiyiRyAHZmdkAR7GTSGx4Mt4Lmi5NEtCcWlTGZ1QgW2r8ZbhouVmTiJkjhQgYCyej3cojtazeJg==",
|
||||
"version": "7.101.1",
|
||||
"resolved": "https://registry.npmjs.org/@sentry-internal/tracing/-/tracing-7.101.1.tgz",
|
||||
"integrity": "sha512-ihjWG8x4x0ozx6t+EHoXLKbsPrgzYLCpeBLWyS+M6n3hn6cmHM76c8nZw3ldhUQi5UYL3LFC/JZ50b4oSxtlrg==",
|
||||
"dependencies": {
|
||||
"@sentry/core": "7.100.1",
|
||||
"@sentry/types": "7.100.1",
|
||||
"@sentry/utils": "7.100.1"
|
||||
"@sentry/core": "7.101.1",
|
||||
"@sentry/types": "7.101.1",
|
||||
"@sentry/utils": "7.101.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry/browser": {
|
||||
"version": "7.100.1",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-7.100.1.tgz",
|
||||
"integrity": "sha512-IxHQ08ixf0bmaWpe4yt1J4UUsOpg02fxax9z3tOQYXw5MSzz5pDXn8M8DFUVJB3wWuyXhHXTub9yD3VIP9fnoA==",
|
||||
"version": "7.101.1",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-7.101.1.tgz",
|
||||
"integrity": "sha512-+rIFoWPdO29AHVYsAwq8QEl2Ihv17Xh9Bt2aPFvLTGDA0caHjnx98g2jSOvLIOah6HI7Nwp3Njg2zBEzDtHkNw==",
|
||||
"dependencies": {
|
||||
"@sentry-internal/feedback": "7.100.1",
|
||||
"@sentry-internal/replay-canvas": "7.100.1",
|
||||
"@sentry-internal/tracing": "7.100.1",
|
||||
"@sentry/core": "7.100.1",
|
||||
"@sentry/replay": "7.100.1",
|
||||
"@sentry/types": "7.100.1",
|
||||
"@sentry/utils": "7.100.1"
|
||||
"@sentry-internal/feedback": "7.101.1",
|
||||
"@sentry-internal/replay-canvas": "7.101.1",
|
||||
"@sentry-internal/tracing": "7.101.1",
|
||||
"@sentry/core": "7.101.1",
|
||||
"@sentry/replay": "7.101.1",
|
||||
"@sentry/types": "7.101.1",
|
||||
"@sentry/utils": "7.101.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
@@ -5066,25 +5066,25 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry/core": {
|
||||
"version": "7.100.1",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.100.1.tgz",
|
||||
"integrity": "sha512-f+ItUge/o9AjlveQq0ZUbQauKlPH1FIJbC1TRaYLJ4KNfOdrsh8yZ29RmWv0cFJ/e+FGTr603gWpRPObF5rM8Q==",
|
||||
"version": "7.101.1",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.101.1.tgz",
|
||||
"integrity": "sha512-XSmXXeYT1d4O14eDF3OXPJFUgaN2qYEeIGUztqPX9nBs9/ij8y/kZOayFqlIMnfGvjOUM+63sy/2xDBOpFn6ug==",
|
||||
"dependencies": {
|
||||
"@sentry/types": "7.100.1",
|
||||
"@sentry/utils": "7.100.1"
|
||||
"@sentry/types": "7.101.1",
|
||||
"@sentry/utils": "7.101.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry/integrations": {
|
||||
"version": "7.100.1",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/integrations/-/integrations-7.100.1.tgz",
|
||||
"integrity": "sha512-RUyZHcsN3Plc8G4hJN3BCMdbwS8ljUY3E3iLjzucA4HroBsGk5AMc6n7Pp/QqFIRgxrPjKEgA52Wgy5Nq6dSvw==",
|
||||
"version": "7.101.1",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/integrations/-/integrations-7.101.1.tgz",
|
||||
"integrity": "sha512-0kk6773Lg2Pa1cUmK1VtxaLLAmpIXcT3qYPlYxRXZQJEUpGcjZZ704V7i8SFLhJSsHkXrL/sAGSyM1p+NDF+QA==",
|
||||
"dependencies": {
|
||||
"@sentry/core": "7.100.1",
|
||||
"@sentry/types": "7.100.1",
|
||||
"@sentry/utils": "7.100.1",
|
||||
"@sentry/core": "7.101.1",
|
||||
"@sentry/types": "7.101.1",
|
||||
"@sentry/utils": "7.101.1",
|
||||
"localforage": "^1.8.1"
|
||||
},
|
||||
"engines": {
|
||||
@@ -5092,18 +5092,18 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry/nextjs": {
|
||||
"version": "7.100.1",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/nextjs/-/nextjs-7.100.1.tgz",
|
||||
"integrity": "sha512-JIDS8oQrr/xrU5llXNVoLnFJCFdS7+k6FVrtW7JnP9B+0NmILEVS3jqbabhh+af9Ch67DPZ2G4KWDRIDZE5HSQ==",
|
||||
"version": "7.101.1",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/nextjs/-/nextjs-7.101.1.tgz",
|
||||
"integrity": "sha512-qIT0jByxaAbbmX5Gl//M9+d+Pi1znk9aWoIGZyakpK+seGQ3D+UjrznxxmwvjcqQgVpbo0SJSq+3yQv7eAh2Tg==",
|
||||
"dependencies": {
|
||||
"@rollup/plugin-commonjs": "24.0.0",
|
||||
"@sentry/core": "7.100.1",
|
||||
"@sentry/integrations": "7.100.1",
|
||||
"@sentry/node": "7.100.1",
|
||||
"@sentry/react": "7.100.1",
|
||||
"@sentry/types": "7.100.1",
|
||||
"@sentry/utils": "7.100.1",
|
||||
"@sentry/vercel-edge": "7.100.1",
|
||||
"@sentry/core": "7.101.1",
|
||||
"@sentry/integrations": "7.101.1",
|
||||
"@sentry/node": "7.101.1",
|
||||
"@sentry/react": "7.101.1",
|
||||
"@sentry/types": "7.101.1",
|
||||
"@sentry/utils": "7.101.1",
|
||||
"@sentry/vercel-edge": "7.101.1",
|
||||
"@sentry/webpack-plugin": "1.21.0",
|
||||
"chalk": "3.0.0",
|
||||
"resolve": "1.22.8",
|
||||
@@ -5125,23 +5125,23 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry/node": {
|
||||
"version": "7.100.1",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/node/-/node-7.100.1.tgz",
|
||||
"integrity": "sha512-jB6tBLr7BpgdE2SlYZu343vvpa5jMFnqyFlprr+jdDu/ayNF4idB0qFwQe8p4C6LI6M/MNDRLVOgPBiCjjZSpw==",
|
||||
"version": "7.101.1",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/node/-/node-7.101.1.tgz",
|
||||
"integrity": "sha512-iXSxUT6Zbt/KUY0+fRcW5II6Tgp2zdTfhBW+fQuDt/UUZt7Ypvb+6n4U2oom3LJfttmD7mdjQuT4+vsNImDjTQ==",
|
||||
"dependencies": {
|
||||
"@sentry-internal/tracing": "7.100.1",
|
||||
"@sentry/core": "7.100.1",
|
||||
"@sentry/types": "7.100.1",
|
||||
"@sentry/utils": "7.100.1"
|
||||
"@sentry-internal/tracing": "7.101.1",
|
||||
"@sentry/core": "7.101.1",
|
||||
"@sentry/types": "7.101.1",
|
||||
"@sentry/utils": "7.101.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry/profiling-node": {
|
||||
"version": "7.100.1",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/profiling-node/-/profiling-node-7.100.1.tgz",
|
||||
"integrity": "sha512-Q/B7SntzB/qt0Y/MZK8dBy8PIf6nCT/kdnn+wrTIFicxozqsF3hq5vmKvGyVUWobb3FrNjc2dSuDXTijN1xmkQ==",
|
||||
"version": "7.101.1",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/profiling-node/-/profiling-node-7.101.1.tgz",
|
||||
"integrity": "sha512-buM+HZZW7jf3mqJgByVGPwytvmHcbjxRT8CkjO5UDO7stRTsq3iT/hK7qt0bQEDQwhmKeVCvfqryg2nKkPq3RA==",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"detect-libc": "^2.0.2",
|
||||
@@ -5155,14 +5155,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry/react": {
|
||||
"version": "7.100.1",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/react/-/react-7.100.1.tgz",
|
||||
"integrity": "sha512-EdrBtrXVLK2LSx4Rvz/nQP7HZUZQmr+t3GHV8436RAhF6vs5mntACVMBoQJRWiUvtZ1iRo3rIsIdah7DLiFPgQ==",
|
||||
"version": "7.101.1",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/react/-/react-7.101.1.tgz",
|
||||
"integrity": "sha512-CwaBXntX2e3XHZQZVuv/tcfm5H+UHcS6aVChGfUiBHIBi2JpAqdnLdQIFGTkE8BSnKyolKgIsnvIU3BQ//QTig==",
|
||||
"dependencies": {
|
||||
"@sentry/browser": "7.100.1",
|
||||
"@sentry/core": "7.100.1",
|
||||
"@sentry/types": "7.100.1",
|
||||
"@sentry/utils": "7.100.1",
|
||||
"@sentry/browser": "7.101.1",
|
||||
"@sentry/core": "7.101.1",
|
||||
"@sentry/types": "7.101.1",
|
||||
"@sentry/utils": "7.101.1",
|
||||
"hoist-non-react-statics": "^3.3.2"
|
||||
},
|
||||
"engines": {
|
||||
@@ -5173,47 +5173,47 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry/replay": {
|
||||
"version": "7.100.1",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/replay/-/replay-7.100.1.tgz",
|
||||
"integrity": "sha512-B1NFjzGEFaqejxBRdUyEzH8ChXc2kfiqlA/W/Lg0aoWIl2/7nuMk+l4ld9gW5F5bIAXDTVd5vYltb1lWEbpr7w==",
|
||||
"version": "7.101.1",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/replay/-/replay-7.101.1.tgz",
|
||||
"integrity": "sha512-l4jmj2Rf/myzk3TA83PdMiomassG8okdBh1b2Hp1+ycBRVZFDmsR81gKPvnefSXwGwGNGKEmp6Q2bdGzekpp3Q==",
|
||||
"dependencies": {
|
||||
"@sentry-internal/tracing": "7.100.1",
|
||||
"@sentry/core": "7.100.1",
|
||||
"@sentry/types": "7.100.1",
|
||||
"@sentry/utils": "7.100.1"
|
||||
"@sentry-internal/tracing": "7.101.1",
|
||||
"@sentry/core": "7.101.1",
|
||||
"@sentry/types": "7.101.1",
|
||||
"@sentry/utils": "7.101.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry/types": {
|
||||
"version": "7.100.1",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.100.1.tgz",
|
||||
"integrity": "sha512-fLM+LedHuKzOd8IhXBqaQuym+AA519MGjeczBa5kGakes/BbAsUMwsNfjsKQedp7Kh44RgYF99jwoRPK2oDrXw==",
|
||||
"version": "7.101.1",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.101.1.tgz",
|
||||
"integrity": "sha512-bwtkQvrCZ6JGc7vqX7TEAKBgkbQFORt84FFS3JQQb8G3efTt9fZd2ReY4buteKQdlALl8h1QWVngTLmI+kyUuw==",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry/utils": {
|
||||
"version": "7.100.1",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.100.1.tgz",
|
||||
"integrity": "sha512-Ve6dXr1o6xiBe3VCoJgiutmBKrugryI65EZAbYto5XI+t+PjiLLf9wXtEMF24ZrwImo4Lv3E9Uqza+fWkEbw6A==",
|
||||
"version": "7.101.1",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.101.1.tgz",
|
||||
"integrity": "sha512-Nrg0nrEI3nrOCd9SLJ/WGzxS5KMQE4cryLOvrDcHJRWpsSyGBF1hLLerk84Nsw/0myMsn7zTYU+xoq7idNsX5A==",
|
||||
"dependencies": {
|
||||
"@sentry/types": "7.100.1"
|
||||
"@sentry/types": "7.101.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry/vercel-edge": {
|
||||
"version": "7.100.1",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/vercel-edge/-/vercel-edge-7.100.1.tgz",
|
||||
"integrity": "sha512-SEWX7KAQreAQREHv+AYm50f/yK8nkq0DQHBQhr+UNZFKbWtSvytbkSmt4HgvOO6nbx9jeAIcg6Z1IKPYNGLKfg==",
|
||||
"version": "7.101.1",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/vercel-edge/-/vercel-edge-7.101.1.tgz",
|
||||
"integrity": "sha512-xs6Xq910CR9rWHCYJEoEbfIfH60Oce+09NQTS5h3R/SOmrrAv4/mmZyblhunIS8TJqABeGRtEPKt1lOqO20O/A==",
|
||||
"dependencies": {
|
||||
"@sentry-internal/tracing": "7.100.1",
|
||||
"@sentry/core": "7.100.1",
|
||||
"@sentry/types": "7.100.1",
|
||||
"@sentry/utils": "7.100.1"
|
||||
"@sentry-internal/tracing": "7.101.1",
|
||||
"@sentry/core": "7.101.1",
|
||||
"@sentry/types": "7.101.1",
|
||||
"@sentry/utils": "7.101.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
@@ -5250,9 +5250,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@sindresorhus/merge-streams": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-1.0.0.tgz",
|
||||
"integrity": "sha512-rUV5WyJrJLoloD4NDN1V1+LDMDWOa4OTsT4yYJwQNpTU6FWxkxHpL7eu4w+DmiH8x/EAM1otkPE1+LaspIbplw==",
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-2.2.1.tgz",
|
||||
"integrity": "sha512-255V7MMIKw6aQ43Wbqp9HZ+VHn6acddERTLiiLnlcPLU9PdTq9Aijl12oklAgUEblLWye+vHLzmqBx6f2TGcZw==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
@@ -7520,9 +7520,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/basic-ftp": {
|
||||
"version": "5.0.3",
|
||||
"resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.0.3.tgz",
|
||||
"integrity": "sha512-QHX8HLlncOLpy54mh+k/sWIFd0ThmRqwe9ZjELybGZK+tZ8rUb9VO0saKJUROTbE+KhzDUT7xziGpGrW8Kmd+g==",
|
||||
"version": "5.0.4",
|
||||
"resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.0.4.tgz",
|
||||
"integrity": "sha512-8PzkB0arJFV4jJWSGOYR+OEic6aeKMu/osRhBULN6RY0ykby6LKhbmuQ5ublvaas5BOwboah5D87nrHyuh8PPA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
@@ -7547,6 +7547,17 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/bl": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
|
||||
"integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"buffer": "^5.5.0",
|
||||
"inherits": "^2.0.4",
|
||||
"readable-stream": "^3.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/bowser": {
|
||||
"version": "2.11.0",
|
||||
"resolved": "https://registry.npmjs.org/bowser/-/bowser-2.11.0.tgz",
|
||||
@@ -8474,9 +8485,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/core-js": {
|
||||
"version": "3.35.1",
|
||||
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.35.1.tgz",
|
||||
"integrity": "sha512-IgdsbxNyMskrTFxa9lWHyMwAJU5gXOPP+1yO+K59d50VLVAIDAbs7gIv705KzALModfK3ZrSZTPNpC0PQgIZuw==",
|
||||
"version": "3.36.0",
|
||||
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.36.0.tgz",
|
||||
"integrity": "sha512-mt7+TUBbTFg5+GngsAxeKBTl5/VS0guFeJacYge9OmHb+m058UwwIm41SE9T4Den7ClatV57B6TYTuJ0CX1MAw==",
|
||||
"hasInstallScript": true,
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
@@ -8821,9 +8832,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/data-uri-to-buffer": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.1.tgz",
|
||||
"integrity": "sha512-MZd3VlchQkp8rdend6vrx7MmVDJzSNTBvghvKjirLkD+WTChA3KUf0jkE68Q4UyctNqI11zZO9/x2Yx+ub5Cvg==",
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz",
|
||||
"integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 14"
|
||||
@@ -10406,31 +10417,27 @@
|
||||
"integrity": "sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA=="
|
||||
},
|
||||
"node_modules/figures": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/figures/-/figures-5.0.0.tgz",
|
||||
"integrity": "sha512-ej8ksPF4x6e5wvK9yevct0UCXh8TTFlWGVLlgjZuoBH1HwjIfKE/IdL5mq89sFA7zELi1VhKpmtDnrs7zWyeyg==",
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz",
|
||||
"integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"escape-string-regexp": "^5.0.0",
|
||||
"is-unicode-supported": "^1.2.0"
|
||||
"escape-string-regexp": "^1.0.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/figures/node_modules/escape-string-regexp": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz",
|
||||
"integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==",
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
|
||||
"integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
"node": ">=0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/file-entry-cache": {
|
||||
@@ -10594,17 +10601,17 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/fs-extra": {
|
||||
"version": "8.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz",
|
||||
"integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==",
|
||||
"version": "11.2.0",
|
||||
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz",
|
||||
"integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"graceful-fs": "^4.2.0",
|
||||
"jsonfile": "^4.0.0",
|
||||
"universalify": "^0.1.0"
|
||||
"jsonfile": "^6.0.1",
|
||||
"universalify": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6 <7 || >=8"
|
||||
"node": ">=14.14"
|
||||
}
|
||||
},
|
||||
"node_modules/fs.realpath": {
|
||||
@@ -10763,15 +10770,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/get-uri": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.2.tgz",
|
||||
"integrity": "sha512-5KLucCJobh8vBY1K07EFV4+cPZH3mrV9YeAruUseCQKHB58SGjjT2l9/eA9LD082IiuMjSlFJEcdJ27TXvbZNw==",
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.3.tgz",
|
||||
"integrity": "sha512-BzUrJBS9EcUb4cFol8r4W3v1cPsSyajLSthNkz5BxbpDcHN5tIrM10E2eNvfnvBn3DaT3DUgx0OpsBKkaOpanw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"basic-ftp": "^5.0.2",
|
||||
"data-uri-to-buffer": "^6.0.0",
|
||||
"data-uri-to-buffer": "^6.0.2",
|
||||
"debug": "^4.3.4",
|
||||
"fs-extra": "^8.1.0"
|
||||
"fs-extra": "^11.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14"
|
||||
@@ -11278,18 +11285,18 @@
|
||||
}
|
||||
},
|
||||
"node_modules/inquirer": {
|
||||
"version": "9.2.12",
|
||||
"resolved": "https://registry.npmjs.org/inquirer/-/inquirer-9.2.12.tgz",
|
||||
"integrity": "sha512-mg3Fh9g2zfuVWJn6lhST0O7x4n03k7G8Tx5nvikJkbq8/CK47WDVm+UznF0G6s5Zi0KcyUisr6DU8T67N5U+1Q==",
|
||||
"version": "9.2.14",
|
||||
"resolved": "https://registry.npmjs.org/inquirer/-/inquirer-9.2.14.tgz",
|
||||
"integrity": "sha512-4ByIMt677Iz5AvjyKrDpzaepIyMewNvDcvwpVVRZNmy9dLakVoVgdCHZXbK1SlVJra1db0JZ6XkJyHsanpdrdQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@ljharb/through": "^2.3.11",
|
||||
"@ljharb/through": "^2.3.12",
|
||||
"ansi-escapes": "^4.3.2",
|
||||
"chalk": "^5.3.0",
|
||||
"cli-cursor": "^3.1.0",
|
||||
"cli-width": "^4.1.0",
|
||||
"external-editor": "^3.1.0",
|
||||
"figures": "^5.0.0",
|
||||
"figures": "^3.2.0",
|
||||
"lodash": "^4.17.21",
|
||||
"mute-stream": "1.0.0",
|
||||
"ora": "^5.4.1",
|
||||
@@ -11300,42 +11307,7 @@
|
||||
"wrap-ansi": "^6.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.18.0"
|
||||
}
|
||||
},
|
||||
"node_modules/inquirer/node_modules/bl": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
|
||||
"integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"buffer": "^5.5.0",
|
||||
"inherits": "^2.0.4",
|
||||
"readable-stream": "^3.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/inquirer/node_modules/buffer": {
|
||||
"version": "5.7.1",
|
||||
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
|
||||
"integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"dependencies": {
|
||||
"base64-js": "^1.3.1",
|
||||
"ieee754": "^1.1.13"
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/inquirer/node_modules/chalk": {
|
||||
@@ -11481,10 +11453,23 @@
|
||||
"loose-envify": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ip": {
|
||||
"version": "1.1.8",
|
||||
"resolved": "https://registry.npmjs.org/ip/-/ip-1.1.8.tgz",
|
||||
"integrity": "sha512-PuExPYUiu6qMBQb4l06ecm6T6ujzhmh+MeJcW9wa89PoAz5pvd4zPgN5WJV104mb6S2T1AwNIAaB70JNrLQWhg==",
|
||||
"node_modules/ip-address": {
|
||||
"version": "9.0.5",
|
||||
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz",
|
||||
"integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"jsbn": "1.1.0",
|
||||
"sprintf-js": "^1.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 12"
|
||||
}
|
||||
},
|
||||
"node_modules/ip-address/node_modules/sprintf-js": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz",
|
||||
"integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/is-arguments": {
|
||||
@@ -13160,6 +13145,12 @@
|
||||
"js-yaml": "bin/js-yaml.js"
|
||||
}
|
||||
},
|
||||
"node_modules/jsbn": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz",
|
||||
"integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/jsdom": {
|
||||
"version": "20.0.3",
|
||||
"resolved": "https://registry.npmjs.org/jsdom/-/jsdom-20.0.3.tgz",
|
||||
@@ -13254,10 +13245,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/jsonfile": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz",
|
||||
"integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==",
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz",
|
||||
"integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"universalify": "^2.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"graceful-fs": "^4.1.6"
|
||||
}
|
||||
@@ -13846,9 +13840,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/next-auth": {
|
||||
"version": "4.24.5",
|
||||
"resolved": "https://registry.npmjs.org/next-auth/-/next-auth-4.24.5.tgz",
|
||||
"integrity": "sha512-3RafV3XbfIKk6rF6GlLE4/KxjTcuMCifqrmD+98ejFq73SRoj2rmzoca8u764977lH/Q7jo6Xu6yM+Re1Mz/Og==",
|
||||
"version": "4.24.6",
|
||||
"resolved": "https://registry.npmjs.org/next-auth/-/next-auth-4.24.6.tgz",
|
||||
"integrity": "sha512-djQt3ZEaWEIxcsuh3HTW2uuzLfXMRjHH+ugAsichlQSbH4iA5MRcgMA2HvTNvsDTDLh44tyU72+/gWsxgTbAKg==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.20.13",
|
||||
"@panva/hkdf": "^1.0.2",
|
||||
@@ -14530,9 +14524,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/pac-proxy-agent/node_modules/http-proxy-agent": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.0.tgz",
|
||||
"integrity": "sha512-+ZT+iBxVUQ1asugqnD6oWoRiS25AkjNfG085dKJGtGxkdwLQrMKU5wJr2bOOFAXzKcTuqq+7fZlTMgG3SRfIYQ==",
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.1.tgz",
|
||||
"integrity": "sha512-My1KCEPs6A0hb4qCVzYp8iEvA8j8YqcvXLZZH8C9OFuTYpYjHE7N2dtG3mRl1HMD4+VGXpF3XcDVcxGBT7yDZQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"agent-base": "^7.1.0",
|
||||
@@ -14543,9 +14537,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/pac-proxy-agent/node_modules/https-proxy-agent": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.2.tgz",
|
||||
"integrity": "sha512-NmLNjm6ucYwtcUmL7JQC1ZQ57LmHP4lT15FQ8D61nak1rO6DH+fz5qNK2Ap5UN4ZapYICE3/0KodcLYSPsPbaA==",
|
||||
"version": "7.0.3",
|
||||
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.3.tgz",
|
||||
"integrity": "sha512-kCnwztfX0KZJSLOBrcL0emLeFako55NWMovvyPP2AjsghNk9RB1yjSI+jVumPHYZsNXegNoqupSW9IY3afSH8w==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"agent-base": "^7.0.2",
|
||||
@@ -14555,28 +14549,13 @@
|
||||
"node": ">= 14"
|
||||
}
|
||||
},
|
||||
"node_modules/pac-proxy-agent/node_modules/socks-proxy-agent": {
|
||||
"version": "8.0.2",
|
||||
"resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.2.tgz",
|
||||
"integrity": "sha512-8zuqoLv1aP/66PHF5TqwJ7Czm3Yv32urJQHrVyhD7mmA6d61Zv8cIXQYPTWwmg6qlupnPvs/QKDmfa4P/qct2g==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"agent-base": "^7.0.2",
|
||||
"debug": "^4.3.4",
|
||||
"socks": "^2.7.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14"
|
||||
}
|
||||
},
|
||||
"node_modules/pac-resolver": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.0.tgz",
|
||||
"integrity": "sha512-Fd9lT9vJbHYRACT8OhCbZBbxr6KRSawSovFpy8nDGshaK99S/EBhVIHp9+crhxrsZOuvLpgL1n23iyPg6Rl2hg==",
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz",
|
||||
"integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"degenerator": "^5.0.0",
|
||||
"ip": "^1.1.8",
|
||||
"netmask": "^2.0.2"
|
||||
},
|
||||
"engines": {
|
||||
@@ -15038,18 +15017,18 @@
|
||||
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="
|
||||
},
|
||||
"node_modules/posthog-js": {
|
||||
"version": "1.105.7",
|
||||
"resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.105.7.tgz",
|
||||
"integrity": "sha512-skpVufQrYllZ4Hi5bdBfe1F9pzeym1rlXUuvKbEYbMhmA+FCz47ZZ0zDX6a72A5hqPW5h7ZBTEJZbwad7jYt1A==",
|
||||
"version": "1.105.9",
|
||||
"resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.105.9.tgz",
|
||||
"integrity": "sha512-i9DnfyXDktugF5E9x0ejihQ8Xdh1kdVnVh7GdL7+PvFtULGMoN/b4pDmWBCwmH+ciubmqTCImVuNrZYy8+eTbw==",
|
||||
"dependencies": {
|
||||
"fflate": "^0.4.8",
|
||||
"preact": "^10.19.3"
|
||||
}
|
||||
},
|
||||
"node_modules/posthog-node": {
|
||||
"version": "3.6.2",
|
||||
"resolved": "https://registry.npmjs.org/posthog-node/-/posthog-node-3.6.2.tgz",
|
||||
"integrity": "sha512-tVIaShR3SxBx17AlAUS86jQTweKuJIFRedBB504fCz7YPnXJTYSrVcUHn5IINE2wu4jUQimQK6ihQr90Djrdrg==",
|
||||
"version": "3.6.3",
|
||||
"resolved": "https://registry.npmjs.org/posthog-node/-/posthog-node-3.6.3.tgz",
|
||||
"integrity": "sha512-JB+ei0LkwE+rKHyW5z79Nd1jUaGxU6TvkfjFqY9vQaHxU5aU8dRl0UUaEmZdZbHwjp3WmXCBQQRNyimwbNQfCw==",
|
||||
"dependencies": {
|
||||
"axios": "^1.6.2",
|
||||
"rusha": "^0.8.14"
|
||||
@@ -15315,15 +15294,15 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/proxy-agent": {
|
||||
"version": "6.3.1",
|
||||
"resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.3.1.tgz",
|
||||
"integrity": "sha512-Rb5RVBy1iyqOtNl15Cw/llpeLH8bsb37gM1FUfKQ+Wck6xHlbAhWGUFiTRHtkjqGTA5pSHz6+0hrPW/oECihPQ==",
|
||||
"version": "6.4.0",
|
||||
"resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.4.0.tgz",
|
||||
"integrity": "sha512-u0piLU+nCOHMgGjRbimiXmA9kM/L9EHh3zL81xCdp7m+Y2pHIsnmbdDoEDoAz5geaonNR6q6+yOPQs6n4T6sBQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"agent-base": "^7.0.2",
|
||||
"debug": "^4.3.4",
|
||||
"http-proxy-agent": "^7.0.0",
|
||||
"https-proxy-agent": "^7.0.2",
|
||||
"http-proxy-agent": "^7.0.1",
|
||||
"https-proxy-agent": "^7.0.3",
|
||||
"lru-cache": "^7.14.1",
|
||||
"pac-proxy-agent": "^7.0.1",
|
||||
"proxy-from-env": "^1.1.0",
|
||||
@@ -15346,9 +15325,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/proxy-agent/node_modules/http-proxy-agent": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.0.tgz",
|
||||
"integrity": "sha512-+ZT+iBxVUQ1asugqnD6oWoRiS25AkjNfG085dKJGtGxkdwLQrMKU5wJr2bOOFAXzKcTuqq+7fZlTMgG3SRfIYQ==",
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.1.tgz",
|
||||
"integrity": "sha512-My1KCEPs6A0hb4qCVzYp8iEvA8j8YqcvXLZZH8C9OFuTYpYjHE7N2dtG3mRl1HMD4+VGXpF3XcDVcxGBT7yDZQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"agent-base": "^7.1.0",
|
||||
@@ -15359,9 +15338,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/proxy-agent/node_modules/https-proxy-agent": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.2.tgz",
|
||||
"integrity": "sha512-NmLNjm6ucYwtcUmL7JQC1ZQ57LmHP4lT15FQ8D61nak1rO6DH+fz5qNK2Ap5UN4ZapYICE3/0KodcLYSPsPbaA==",
|
||||
"version": "7.0.3",
|
||||
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.3.tgz",
|
||||
"integrity": "sha512-kCnwztfX0KZJSLOBrcL0emLeFako55NWMovvyPP2AjsghNk9RB1yjSI+jVumPHYZsNXegNoqupSW9IY3afSH8w==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"agent-base": "^7.0.2",
|
||||
@@ -15380,20 +15359,6 @@
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/proxy-agent/node_modules/socks-proxy-agent": {
|
||||
"version": "8.0.2",
|
||||
"resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.2.tgz",
|
||||
"integrity": "sha512-8zuqoLv1aP/66PHF5TqwJ7Czm3Yv32urJQHrVyhD7mmA6d61Zv8cIXQYPTWwmg6qlupnPvs/QKDmfa4P/qct2g==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"agent-base": "^7.0.2",
|
||||
"debug": "^4.3.4",
|
||||
"socks": "^2.7.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14"
|
||||
}
|
||||
},
|
||||
"node_modules/proxy-from-env": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||
@@ -15978,9 +15943,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/release-it": {
|
||||
"version": "17.0.3",
|
||||
"resolved": "https://registry.npmjs.org/release-it/-/release-it-17.0.3.tgz",
|
||||
"integrity": "sha512-QjTCmvQm91pwLEbvavEs9jofHNe8thsb9Uimin+8DNSwFRdUd73p0Owy2PP/Dzh/EegRkKq/o+4Pn1xp8pC1og==",
|
||||
"version": "17.0.5",
|
||||
"resolved": "https://registry.npmjs.org/release-it/-/release-it-17.0.5.tgz",
|
||||
"integrity": "sha512-97FcBu/2IjPd4qMvliZSnavpCO0+BWi8lZr+BgVJTXs6ihX8HLPucYR5RsgUUcZoGKj3beO8cunrsMuvEWTzGQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@@ -16000,9 +15965,9 @@
|
||||
"cosmiconfig": "9.0.0",
|
||||
"execa": "8.0.1",
|
||||
"git-url-parse": "14.0.0",
|
||||
"globby": "14.0.0",
|
||||
"globby": "14.0.1",
|
||||
"got": "13.0.0",
|
||||
"inquirer": "9.2.12",
|
||||
"inquirer": "9.2.14",
|
||||
"is-ci": "3.0.1",
|
||||
"issue-parser": "6.0.0",
|
||||
"lodash": "4.17.21",
|
||||
@@ -16013,8 +15978,8 @@
|
||||
"ora": "8.0.1",
|
||||
"os-name": "5.1.0",
|
||||
"promise.allsettled": "1.0.7",
|
||||
"proxy-agent": "6.3.1",
|
||||
"semver": "7.5.4",
|
||||
"proxy-agent": "6.4.0",
|
||||
"semver": "7.6.0",
|
||||
"shelljs": "0.8.5",
|
||||
"update-notifier": "7.0.0",
|
||||
"url-join": "5.0.0",
|
||||
@@ -16111,12 +16076,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/release-it/node_modules/globby": {
|
||||
"version": "14.0.0",
|
||||
"resolved": "https://registry.npmjs.org/globby/-/globby-14.0.0.tgz",
|
||||
"integrity": "sha512-/1WM/LNHRAOH9lZta77uGbq0dAEQM+XjNesWwhlERDVenqothRbnzTrL3/LrIoEPPjeUHC3vrS6TwoyxeHs7MQ==",
|
||||
"version": "14.0.1",
|
||||
"resolved": "https://registry.npmjs.org/globby/-/globby-14.0.1.tgz",
|
||||
"integrity": "sha512-jOMLD2Z7MAhyG8aJpNOpmziMOP4rPLcc95oQPKXBazW82z+CEgPFBQvEpRUa1KeIMUJo4Wsm+q6uzO/Q/4BksQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@sindresorhus/merge-streams": "^1.0.0",
|
||||
"@sindresorhus/merge-streams": "^2.1.0",
|
||||
"fast-glob": "^3.3.2",
|
||||
"ignore": "^5.2.4",
|
||||
"path-type": "^5.0.0",
|
||||
@@ -16130,18 +16095,6 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/release-it/node_modules/globby/node_modules/path-type": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/path-type/-/path-type-5.0.0.tgz",
|
||||
"integrity": "sha512-5HviZNaZcfqP95rwpv+1HDgUamezbqdSYTyzjTvwtJSnIH+3vnbmWsItli8OFEndS984VT55M3jduxZbX351gg==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/release-it/node_modules/human-signals": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz",
|
||||
@@ -16235,6 +16188,18 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/release-it/node_modules/path-type": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/path-type/-/path-type-5.0.0.tgz",
|
||||
"integrity": "sha512-5HviZNaZcfqP95rwpv+1HDgUamezbqdSYTyzjTvwtJSnIH+3vnbmWsItli8OFEndS984VT55M3jduxZbX351gg==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/release-it/node_modules/signal-exit": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
|
||||
@@ -16585,9 +16550,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/semver": {
|
||||
"version": "7.5.4",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
|
||||
"integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
|
||||
"version": "7.6.0",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz",
|
||||
"integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==",
|
||||
"dependencies": {
|
||||
"lru-cache": "^6.0.0"
|
||||
},
|
||||
@@ -16770,24 +16735,44 @@
|
||||
}
|
||||
},
|
||||
"node_modules/socks": {
|
||||
"version": "2.7.1",
|
||||
"resolved": "https://registry.npmjs.org/socks/-/socks-2.7.1.tgz",
|
||||
"integrity": "sha512-7maUZy1N7uo6+WVEX6psASxtNlKaNVMlGQKkG/63nEDdLOWNbiUMoLK7X4uYoLhQstau72mLgfEWcXcwsaHbYQ==",
|
||||
"version": "2.7.3",
|
||||
"resolved": "https://registry.npmjs.org/socks/-/socks-2.7.3.tgz",
|
||||
"integrity": "sha512-vfuYK48HXCTFD03G/1/zkIls3Ebr2YNa4qU9gHDZdblHLiqhJrJGkY3+0Nx0JpN9qBhJbVObc1CNciT1bIZJxw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"ip": "^2.0.0",
|
||||
"ip-address": "^9.0.5",
|
||||
"smart-buffer": "^4.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10.13.0",
|
||||
"node": ">= 10.0.0",
|
||||
"npm": ">= 3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/socks/node_modules/ip": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ip/-/ip-2.0.0.tgz",
|
||||
"integrity": "sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ==",
|
||||
"dev": true
|
||||
"node_modules/socks-proxy-agent": {
|
||||
"version": "8.0.2",
|
||||
"resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.2.tgz",
|
||||
"integrity": "sha512-8zuqoLv1aP/66PHF5TqwJ7Czm3Yv32urJQHrVyhD7mmA6d61Zv8cIXQYPTWwmg6qlupnPvs/QKDmfa4P/qct2g==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"agent-base": "^7.0.2",
|
||||
"debug": "^4.3.4",
|
||||
"socks": "^2.7.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14"
|
||||
}
|
||||
},
|
||||
"node_modules/socks-proxy-agent/node_modules/agent-base": {
|
||||
"version": "7.1.0",
|
||||
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.0.tgz",
|
||||
"integrity": "sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"debug": "^4.3.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14"
|
||||
}
|
||||
},
|
||||
"node_modules/sonner": {
|
||||
"version": "1.4.0",
|
||||
@@ -17320,41 +17305,6 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/tar-stream/node_modules/bl": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
|
||||
"integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"buffer": "^5.5.0",
|
||||
"inherits": "^2.0.4",
|
||||
"readable-stream": "^3.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tar-stream/node_modules/buffer": {
|
||||
"version": "5.7.1",
|
||||
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
|
||||
"integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"dependencies": {
|
||||
"base64-js": "^1.3.1",
|
||||
"ieee754": "^1.1.13"
|
||||
}
|
||||
},
|
||||
"node_modules/test-exclude": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz",
|
||||
@@ -17825,12 +17775,12 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/universalify": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz",
|
||||
"integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==",
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
|
||||
"integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 4.0.0"
|
||||
"node": ">= 10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/update-browserslist-db": {
|
||||
|
||||
+8
-8
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "langfuse-core",
|
||||
"version": "2.4.3",
|
||||
"version": "2.6.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",
|
||||
@@ -60,8 +60,8 @@
|
||||
"@radix-ui/react-tooltip": "^1.0.7",
|
||||
"@react-email/components": "^0.0.14",
|
||||
"@react-email/render": "^0.0.12",
|
||||
"@sentry/nextjs": "^7.100.1",
|
||||
"@sentry/profiling-node": "^7.100.1",
|
||||
"@sentry/nextjs": "^7.101.1",
|
||||
"@sentry/profiling-node": "^7.101.1",
|
||||
"@sentry/types": "^7.88.0",
|
||||
"@t3-oss/env-nextjs": "^0.8.0",
|
||||
"@tailwindcss/forms": "^0.5.7",
|
||||
@@ -76,7 +76,7 @@
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.1.0",
|
||||
"cmdk": "^0.2.1",
|
||||
"core-js": "^3.35.1",
|
||||
"core-js": "^3.36.0",
|
||||
"cors": "^2.8.5",
|
||||
"date-fns": "^3.3.1",
|
||||
"decimal.js": "^10.4.3",
|
||||
@@ -85,11 +85,11 @@
|
||||
"lodash": "^4.17.21",
|
||||
"lucide-react": "^0.330.0",
|
||||
"next": "^14.1.0",
|
||||
"next-auth": "^4.24.5",
|
||||
"next-auth": "^4.24.6",
|
||||
"next-query-params": "^5.0.0",
|
||||
"nodemailer": "^6.9.9",
|
||||
"posthog-js": "^1.105.7",
|
||||
"posthog-node": "^3.6.2",
|
||||
"posthog-js": "^1.105.9",
|
||||
"posthog-node": "^3.6.3",
|
||||
"react": "18.2.0",
|
||||
"react-day-picker": "^8.10.0",
|
||||
"react-dom": "18.2.0",
|
||||
@@ -135,7 +135,7 @@
|
||||
"prettier-plugin-tailwindcss": "^0.5.11",
|
||||
"prisma": "^5.9.1",
|
||||
"prisma-erd-generator": "^1.11.2",
|
||||
"release-it": "^17.0.3",
|
||||
"release-it": "^17.0.5",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"ts-node": "^10.9.2",
|
||||
"tsconfig-paths": "^4.2.0",
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
-- This is an empty migration.
|
||||
|
||||
INSERT INTO models (
|
||||
id,
|
||||
project_id,
|
||||
model_name,
|
||||
match_pattern,
|
||||
start_date,
|
||||
input_price,
|
||||
output_price,
|
||||
total_price,
|
||||
unit,
|
||||
tokenizer_id,
|
||||
tokenizer_config
|
||||
)
|
||||
VALUES
|
||||
('clsnq07bn000008l4e46v1ll8', NULL, 'gpt-4-turbo-preview', '(?i)^(gpt-4-turbo-preview)$', '2023-11-06', 0.00001, 0.00003, NULL, 'TOKENS', 'openai', '{ "tokensPerMessage": 3, "tokensPerName": 1, "tokenizerModel": "gpt-4" }')
|
||||
@@ -0,0 +1,50 @@
|
||||
CREATE OR REPLACE VIEW "observations_view" AS
|
||||
SELECT
|
||||
o.*,
|
||||
m.id AS "model_id",
|
||||
m.start_date AS "model_start_date",
|
||||
m.input_price,
|
||||
m.output_price,
|
||||
m.total_price,
|
||||
m.tokenizer_config AS "tokenizer_config",
|
||||
CASE
|
||||
WHEN o.input_cost IS NULL AND o.output_cost IS NULL AND o.total_cost IS NULL THEN
|
||||
o.prompt_tokens::decimal * m.input_price
|
||||
ELSE
|
||||
o.input_cost
|
||||
END AS "calculated_input_cost",
|
||||
CASE
|
||||
WHEN o.input_cost IS NULL AND o.output_cost IS NULL AND o.total_cost IS NULL THEN
|
||||
o.completion_tokens::decimal * m.output_price
|
||||
ELSE
|
||||
o.output_cost
|
||||
END AS "calculated_output_cost",
|
||||
CASE
|
||||
WHEN o.input_cost IS NULL AND o.output_cost IS NULL AND o.total_cost IS NULL THEN
|
||||
CASE
|
||||
WHEN m.total_price IS NOT NULL AND o.total_tokens IS NOT NULL THEN
|
||||
m.total_price * o.total_tokens
|
||||
ELSE
|
||||
o.prompt_tokens::decimal * m.input_price +
|
||||
o.completion_tokens::decimal * m.output_price
|
||||
END
|
||||
ELSE
|
||||
o.total_cost
|
||||
END AS "calculated_total_cost",
|
||||
CASE WHEN o.end_time IS NULL THEN NULL ELSE (EXTRACT(EPOCH FROM o."end_time") - EXTRACT(EPOCH FROM o."start_time"))::double precision END AS "latency"
|
||||
FROM
|
||||
observations o
|
||||
LEFT JOIN models m ON m.id = (
|
||||
SELECT
|
||||
id
|
||||
FROM
|
||||
models
|
||||
WHERE (project_id = o.project_id OR project_id IS NULL)
|
||||
AND model_name = o.internal_model
|
||||
AND (start_date < o.start_time OR start_date is NULL)
|
||||
AND o.unit::TEXT = unit
|
||||
ORDER BY
|
||||
project_id ASC, -- in postgres, NULLs are sorted last when ordering ASC
|
||||
start_date DESC NULLS LAST -- now, NULLs are sorted last when ordering DESC as well
|
||||
LIMIT 1
|
||||
)
|
||||
@@ -493,6 +493,7 @@ view ObservationView {
|
||||
calculatedInputCost Decimal? @map("calculated_input_cost")
|
||||
calculatedOutputCost Decimal? @map("calculated_output_cost")
|
||||
calculatedTotalCost Decimal? @map("calculated_total_cost")
|
||||
latency Decimal? @map("latency")
|
||||
|
||||
@@map("observations_view")
|
||||
}
|
||||
|
||||
@@ -239,8 +239,23 @@ describe("cost retrieval tests", () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it(`should prioritize latest models`, async () => {
|
||||
await pruneDatabase();
|
||||
await prisma.model.create({
|
||||
data: {
|
||||
id: "model-0",
|
||||
modelName: "gpt-3.5-turbo",
|
||||
inputPrice: "0.0000000",
|
||||
outputPrice: "0.0000000",
|
||||
totalPrice: "0.1",
|
||||
matchPattern: "(.*)(gpt-)(35|3.5)(-turbo)?(.*)",
|
||||
projectId: null,
|
||||
startDate: null,
|
||||
tokenizerConfig: { tokensPerMessage: 3, tokensPerName: 1 },
|
||||
unit: ModelUsageUnit.Tokens,
|
||||
},
|
||||
});
|
||||
|
||||
await prisma.model.create({
|
||||
data: {
|
||||
@@ -305,6 +320,70 @@ describe("cost retrieval tests", () => {
|
||||
expect(view?.calculatedTotalCost?.toString()).toBe("0.0124");
|
||||
});
|
||||
|
||||
it(`should prioritize own models`, async () => {
|
||||
await pruneDatabase();
|
||||
await prisma.model.create({
|
||||
data: {
|
||||
id: "model-0",
|
||||
modelName: "gpt-3.5-turbo",
|
||||
inputPrice: "0.0000000",
|
||||
outputPrice: "0.0000000",
|
||||
totalPrice: "0.1",
|
||||
matchPattern: "(.*)(gpt-)(35|3.5)(-turbo)?(.*)",
|
||||
projectId: null,
|
||||
startDate: null,
|
||||
tokenizerConfig: { tokensPerMessage: 3, tokensPerName: 1 },
|
||||
unit: ModelUsageUnit.Tokens,
|
||||
},
|
||||
});
|
||||
|
||||
await prisma.model.create({
|
||||
data: {
|
||||
id: "model-1",
|
||||
modelName: "gpt-3.5-turbo",
|
||||
inputPrice: "0.0000010",
|
||||
outputPrice: "0.0000020",
|
||||
totalPrice: "0.1",
|
||||
matchPattern: "(.*)(gpt-)(35|3.5)(-turbo)?(.*)",
|
||||
startDate: null,
|
||||
project: { connect: { id: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a" } },
|
||||
tokenizerConfig: { tokensPerMessage: 3, tokensPerName: 1 },
|
||||
unit: ModelUsageUnit.Tokens,
|
||||
},
|
||||
});
|
||||
|
||||
const dbTrace = await prisma.trace.create({
|
||||
data: {
|
||||
name: "trace-name",
|
||||
project: { connect: { id: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a" } },
|
||||
},
|
||||
});
|
||||
|
||||
await prisma.observation.create({
|
||||
data: {
|
||||
traceId: dbTrace.id,
|
||||
type: "GENERATION",
|
||||
project: { connect: { id: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a" } },
|
||||
model: "gpt-3.5-turbo",
|
||||
internalModel: "gpt-3.5-turbo",
|
||||
startTime: new Date("2024-01-01T00:00:00.000Z"),
|
||||
unit: ModelUsageUnit.Tokens,
|
||||
promptTokens: 200,
|
||||
completionTokens: 3000,
|
||||
totalTokens: undefined,
|
||||
},
|
||||
});
|
||||
|
||||
const view = await prisma.observationView.findFirst({
|
||||
where: { traceId: dbTrace.id },
|
||||
});
|
||||
|
||||
console.log(view);
|
||||
|
||||
// calculated cost fields
|
||||
expect(view?.modelId).toBe("model-1");
|
||||
});
|
||||
|
||||
it(`should prioritize old model if the latest model is not own one`, async () => {
|
||||
await pruneDatabase();
|
||||
|
||||
@@ -371,6 +450,72 @@ describe("cost retrieval tests", () => {
|
||||
expect(view?.calculatedTotalCost?.toString()).toBe("0.0124");
|
||||
});
|
||||
|
||||
it(`should prioritize new model if the latest model is own one`, async () => {
|
||||
await pruneDatabase();
|
||||
|
||||
await prisma.model.create({
|
||||
data: {
|
||||
id: "model-1",
|
||||
modelName: "gpt-3.5-turbo",
|
||||
inputPrice: "0.0010",
|
||||
outputPrice: "0.0020",
|
||||
totalPrice: "0.1",
|
||||
matchPattern: "(.*)(gpt-)(35|3.5)(-turbo)?(.*)",
|
||||
startDate: null,
|
||||
tokenizerConfig: { tokensPerMessage: 3, tokensPerName: 1 },
|
||||
unit: ModelUsageUnit.Tokens,
|
||||
},
|
||||
});
|
||||
await prisma.model.create({
|
||||
data: {
|
||||
id: "model-2",
|
||||
modelName: "gpt-3.5-turbo",
|
||||
inputPrice: "0.0000020",
|
||||
outputPrice: "0.0000040",
|
||||
totalPrice: undefined,
|
||||
matchPattern: "(.*)(gpt-)(35|3.5)(-turbo)?(.*)",
|
||||
startDate: new Date("2023-12-01"),
|
||||
tokenizerConfig: { tokensPerMessage: 3, tokensPerName: 1 },
|
||||
project: { connect: { id: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a" } },
|
||||
unit: ModelUsageUnit.Tokens,
|
||||
},
|
||||
});
|
||||
|
||||
const dbTrace = await prisma.trace.create({
|
||||
data: {
|
||||
name: "trace-name",
|
||||
project: { connect: { id: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a" } },
|
||||
},
|
||||
});
|
||||
|
||||
await prisma.observation.create({
|
||||
data: {
|
||||
traceId: dbTrace.id,
|
||||
type: "GENERATION",
|
||||
project: { connect: { id: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a" } },
|
||||
model: "gpt-3.5-turbo",
|
||||
internalModel: "gpt-3.5-turbo",
|
||||
startTime: new Date("2024-01-01T00:00:00.000Z"),
|
||||
unit: ModelUsageUnit.Tokens,
|
||||
promptTokens: 200,
|
||||
completionTokens: 3000,
|
||||
totalTokens: undefined,
|
||||
},
|
||||
});
|
||||
|
||||
const view = await prisma.observationView.findFirst({
|
||||
where: { traceId: dbTrace.id },
|
||||
});
|
||||
|
||||
console.log(view);
|
||||
|
||||
// calculated cost fields
|
||||
expect(view?.modelId).toBe("model-2");
|
||||
expect(view?.calculatedInputCost?.toString()).toBe("0.0004");
|
||||
expect(view?.calculatedOutputCost?.toString()).toBe("0.012");
|
||||
expect(view?.calculatedTotalCost?.toString()).toBe("0.0124");
|
||||
});
|
||||
|
||||
it(`should prioritize user provided cost`, async () => {
|
||||
await pruneDatabase();
|
||||
|
||||
|
||||
@@ -207,8 +207,6 @@ describe("/api/public/ingestion API Endpoint", () => {
|
||||
|
||||
expect(response.status).toBe(207);
|
||||
|
||||
console.log("response body", response.body);
|
||||
|
||||
const dbTrace = await prisma.trace.findMany({
|
||||
where: {
|
||||
name: "trace-name",
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
import { makeAPICall, pruneDatabase } from "@/src/__tests__/test-utils";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
describe("/api/public/metrics/daily API Endpoint", () => {
|
||||
beforeEach(async () => await pruneDatabase());
|
||||
afterEach(async () => await pruneDatabase());
|
||||
|
||||
it("should handle daily metrics correctly", async () => {
|
||||
await pruneDatabase();
|
||||
|
||||
// Create traces with observations on different days
|
||||
const traceId1 = uuidv4();
|
||||
const traceId2 = uuidv4();
|
||||
await makeAPICall("POST", "/api/public/traces", {
|
||||
id: traceId1,
|
||||
timestamp: "2021-01-01T00:00:00.000Z",
|
||||
name: "trace-day-1",
|
||||
userId: "user-daily-metrics",
|
||||
projectId: "project-daily-metrics",
|
||||
});
|
||||
await makeAPICall("POST", "/api/public/traces", {
|
||||
id: traceId2,
|
||||
timestamp: "2021-01-02T00:00:00.000Z",
|
||||
name: "trace-day-2",
|
||||
userId: "user-daily-metrics",
|
||||
projectId: "project-daily-metrics",
|
||||
});
|
||||
|
||||
// Simulate observations with usage metrics on different days
|
||||
await makeAPICall("POST", "/api/public/generations", {
|
||||
traceId: traceId1,
|
||||
model: "modelA",
|
||||
usage: { input: 100, output: 200, total: 300 },
|
||||
startTime: "2021-01-01T00:00:00.000Z",
|
||||
endTime: "2021-01-01T00:01:00.000Z",
|
||||
});
|
||||
await makeAPICall("POST", "/api/public/generations", {
|
||||
traceId: traceId2,
|
||||
model: "modelB",
|
||||
usage: { input: 333 },
|
||||
startTime: "2021-01-02T00:00:00.000Z",
|
||||
endTime: "2021-01-02T00:02:00.000Z",
|
||||
});
|
||||
await makeAPICall("POST", "/api/public/generations", {
|
||||
traceId: traceId2,
|
||||
model: "modelC",
|
||||
usage: { input: 666, output: 777, totalCost: 1024.22 },
|
||||
startTime: "2021-01-02T00:00:00.000Z",
|
||||
endTime: "2021-01-02T00:04:00.000Z",
|
||||
});
|
||||
|
||||
// Retrieve the daily metrics
|
||||
const dailyMetricsResponse = await makeAPICall<{
|
||||
data: Array<Record<string, unknown>>;
|
||||
}>("GET", `/api/public/metrics/daily`);
|
||||
const dailyMetricsData = dailyMetricsResponse.body.data;
|
||||
|
||||
// Check if the daily metrics are calculated correctly
|
||||
expect(dailyMetricsData).toHaveLength(2); // Two days of data
|
||||
if (!dailyMetricsData[0])
|
||||
throw new Error("dailyMetricsData[0] is undefined");
|
||||
expect(dailyMetricsData[0].date).toBe("2021-01-02"); // Latest date first
|
||||
expect(dailyMetricsData[0].countTraces).toBe(1);
|
||||
expect(dailyMetricsData[0].totalCost).toEqual(1024.22);
|
||||
expect(dailyMetricsData[0].usage).toEqual([
|
||||
{
|
||||
model: "modelB",
|
||||
inputUsage: 333,
|
||||
outputUsage: 0,
|
||||
totalUsage: 333,
|
||||
},
|
||||
{
|
||||
model: "modelC",
|
||||
inputUsage: 666,
|
||||
outputUsage: 777,
|
||||
totalUsage: 1443,
|
||||
},
|
||||
]);
|
||||
|
||||
if (!dailyMetricsData[1])
|
||||
throw new Error("dailyMetricsData[1] is undefined");
|
||||
expect(dailyMetricsData[1].date).toBe("2021-01-01");
|
||||
expect(dailyMetricsData[1].countTraces).toBe(1);
|
||||
expect(dailyMetricsData[1].totalCost).toEqual(0);
|
||||
expect(dailyMetricsData[1].usage).toEqual([
|
||||
{
|
||||
model: "modelA",
|
||||
inputUsage: 100,
|
||||
outputUsage: 200,
|
||||
totalUsage: 300,
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -16,28 +16,28 @@ describe("Build valid SQL queries", () => {
|
||||
{
|
||||
table: "traces",
|
||||
values: ["project-id"],
|
||||
strings: [' FROM traces t WHERE t."project_id" = ', " ;"],
|
||||
strings: [' FROM traces t WHERE t."project_id" = ', ";"],
|
||||
} as const,
|
||||
{
|
||||
table: "traces_observations",
|
||||
values: ["project-id", "project-id"],
|
||||
strings: [
|
||||
' FROM traces t LEFT JOIN observations_view o ON t.id = o.trace_id WHERE t."project_id" = ',
|
||||
' AND o."project_id" = ',
|
||||
" ;",
|
||||
' AND o."project_id" = ',
|
||||
";",
|
||||
],
|
||||
} as const,
|
||||
{
|
||||
table: "observations",
|
||||
values: ["project-id"],
|
||||
strings: [' FROM observations_view o WHERE o."project_id" = ', " ;"],
|
||||
strings: [' FROM observations_view o WHERE o."project_id" = ', ";"],
|
||||
} as const,
|
||||
{
|
||||
table: "traces_scores",
|
||||
values: ["project-id"],
|
||||
strings: [
|
||||
' FROM traces t JOIN scores s ON t.id = s.trace_id WHERE t."project_id" = ',
|
||||
" ;",
|
||||
";",
|
||||
],
|
||||
} as const,
|
||||
].forEach((prop) => {
|
||||
@@ -133,7 +133,7 @@ describe("Build valid SQL queries", () => {
|
||||
],
|
||||
select: [],
|
||||
}),
|
||||
).toThrow("Column unknown not found");
|
||||
).toThrow("Invalid filter column: unknown");
|
||||
});
|
||||
|
||||
it("should not select an unknown column", () => {
|
||||
|
||||
@@ -231,4 +231,43 @@ describe("/api/public/scores API Endpoint", () => {
|
||||
expect(dbScore?.comment).toBe("comment-updated");
|
||||
expect(dbScore?.observationId).toBe(dbGeneration[0]!.id);
|
||||
});
|
||||
|
||||
it("should delete a score", async () => {
|
||||
await pruneDatabase();
|
||||
|
||||
const traceId = uuidv4();
|
||||
|
||||
await makeAPICall("POST", "/api/public/traces", {
|
||||
id: traceId,
|
||||
});
|
||||
|
||||
const scoreId = uuidv4();
|
||||
const createScore = await makeAPICall("POST", "/api/public/scores", {
|
||||
id: scoreId,
|
||||
name: "score-name",
|
||||
value: 100.5,
|
||||
traceId: traceId,
|
||||
comment: "comment",
|
||||
});
|
||||
|
||||
expect(createScore.status).toBe(200);
|
||||
const dbScore = await prisma.score.findUnique({
|
||||
where: {
|
||||
id: scoreId,
|
||||
},
|
||||
});
|
||||
expect(dbScore?.id).toBe(scoreId);
|
||||
|
||||
const deleteScore = await makeAPICall(
|
||||
"DELETE",
|
||||
`/api/public/scores/${scoreId}`,
|
||||
);
|
||||
expect(deleteScore.status).toBe(200);
|
||||
const deletedScore = await prisma.score.findUnique({
|
||||
where: {
|
||||
id: scoreId,
|
||||
},
|
||||
});
|
||||
expect(deletedScore).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -44,12 +44,12 @@ export type ErrorIngestion = {
|
||||
error: string;
|
||||
};
|
||||
|
||||
export async function makeAPICall(
|
||||
export async function makeAPICall<T = IngestionAPIResponse>(
|
||||
method: "POST" | "GET" | "PUT" | "DELETE" | "PATCH",
|
||||
url: string,
|
||||
body?: unknown,
|
||||
auth?: string,
|
||||
) {
|
||||
): Promise<{ body: T; status: number }> {
|
||||
const finalUrl = `http://localhost:3000/${url}`;
|
||||
const authorization =
|
||||
auth || createBasicAuthHeader("pk-lf-1234567890", "sk-lf-1234567890");
|
||||
@@ -60,14 +60,12 @@ export async function makeAPICall(
|
||||
"Content-Type": "application/json;charset=UTF-8",
|
||||
Authorization: authorization,
|
||||
},
|
||||
// Conditionally include the body property if the method is not "GET"
|
||||
...(method !== "GET" &&
|
||||
body !== undefined && { body: JSON.stringify(body) }),
|
||||
};
|
||||
const a = await fetch(finalUrl, options);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
return { body: (await a.json()) as IngestionAPIResponse, status: a.status };
|
||||
const response = await fetch(finalUrl, options);
|
||||
const responseBody = (await response.json()) as T;
|
||||
return { body: responseBody, status: response.status };
|
||||
}
|
||||
|
||||
export const setupUserAndProject = async () => {
|
||||
|
||||
@@ -2,6 +2,14 @@
|
||||
|
||||
import { makeAPICall, pruneDatabase } from "@/src/__tests__/test-utils";
|
||||
import { prisma } from "@/src/server/db";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
interface TraceAPIResponse {
|
||||
data: Array<{
|
||||
id: string;
|
||||
[key: string]: unknown;
|
||||
}>;
|
||||
}
|
||||
|
||||
describe("/api/public/traces API Endpoint", () => {
|
||||
beforeEach(async () => await pruneDatabase());
|
||||
@@ -65,6 +73,7 @@ describe("/api/public/traces API Endpoint", () => {
|
||||
await makeAPICall("POST", "/api/public/traces", {
|
||||
id: "trace-id",
|
||||
metadata: { key: "value" },
|
||||
timestamp: "2021-01-01T00:00:00.000Z",
|
||||
release: "1.0.0",
|
||||
version: "5.0.0",
|
||||
public: false,
|
||||
@@ -84,6 +93,7 @@ describe("/api/public/traces API Endpoint", () => {
|
||||
version: "5.0.0",
|
||||
public: false,
|
||||
userId: "user-1",
|
||||
timestamp: new Date("2021-01-01T00:00:00.000Z"),
|
||||
});
|
||||
});
|
||||
|
||||
@@ -106,34 +116,82 @@ describe("/api/public/traces API Endpoint", () => {
|
||||
});
|
||||
|
||||
// multiple tags
|
||||
const traces = await makeAPICall(
|
||||
const traces = await makeAPICall<TraceAPIResponse>(
|
||||
"GET",
|
||||
"/api/public/traces?tags=tag-2&tags=tag-3",
|
||||
);
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
|
||||
const traceIds = traces.body.data.map((t: { id: string }) => t.id);
|
||||
const traceIds = traces.body.data.map((t) => t.id);
|
||||
// check for equality ok as ordered by timestamp
|
||||
expect(traceIds).toEqual(["trace-3", "trace-1"]);
|
||||
|
||||
// single tag
|
||||
const traces2 = await makeAPICall("GET", "/api/public/traces?tags=tag-1");
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
|
||||
const traceIds2 = traces2.body.data.map((t: { id: string }) => t.id);
|
||||
const traces2 = await makeAPICall<TraceAPIResponse>(
|
||||
"GET",
|
||||
"/api/public/traces?tags=tag-1",
|
||||
);
|
||||
const traceIds2 = traces2.body.data.map((t) => t.id);
|
||||
// check for equality ok as ordered by timestamp
|
||||
expect(traceIds2).toEqual(["trace-2", "trace-1"]);
|
||||
|
||||
// wrong tag
|
||||
const traces3 = await makeAPICall("GET", "/api/public/traces?tags=tag-10");
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
|
||||
const traceIds3 = traces3.body.data.map((t: { id: string }) => t.id);
|
||||
const traces3 = await makeAPICall<TraceAPIResponse>(
|
||||
"GET",
|
||||
"/api/public/traces?tags=tag-10",
|
||||
);
|
||||
const traceIds3 = traces3.body.data.map((t) => t.id);
|
||||
// check for equality ok as ordered by timestamp
|
||||
expect(traceIds3).toEqual([]);
|
||||
|
||||
// no tag
|
||||
const traces4 = await makeAPICall("GET", "/api/public/traces?tags=");
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
|
||||
const traceIds4 = traces4.body.data.map((t: { id: string }) => t.id);
|
||||
const traces4 = await makeAPICall<TraceAPIResponse>(
|
||||
"GET",
|
||||
"/api/public/traces?tags=",
|
||||
);
|
||||
const traceIds4 = traces4.body.data.map((t) => t.id);
|
||||
// check for equality ok as ordered by timestamp
|
||||
expect(traceIds4).toEqual(["trace-3", "trace-2", "trace-1"]);
|
||||
});
|
||||
|
||||
it("should handle totalCost and latency correctly", async () => {
|
||||
await pruneDatabase();
|
||||
|
||||
// Create a trace with some observations that have costs and latencies
|
||||
const traceId = uuidv4();
|
||||
await makeAPICall("POST", "/api/public/traces", {
|
||||
id: traceId,
|
||||
name: "trace-with-costs",
|
||||
userId: "user-costs",
|
||||
projectId: "project-costs",
|
||||
metadata: { key: "value" },
|
||||
release: "1.0.0",
|
||||
version: "2.0.0",
|
||||
});
|
||||
console.log(traceId);
|
||||
|
||||
// Simulate observations with costs and latencies
|
||||
await makeAPICall("POST", "/api/public/generations", {
|
||||
traceId: traceId,
|
||||
usage: { totalCost: 10.5 },
|
||||
startTime: "2021-01-01T00:00:00.000Z",
|
||||
endTime: "2021-01-01T00:10:00.000Z",
|
||||
});
|
||||
await makeAPICall("POST", "/api/public/generations", {
|
||||
traceId: traceId,
|
||||
usage: { totalCost: 5.25 },
|
||||
startTime: "2021-01-01T00:10:00.000Z",
|
||||
endTime: "2021-01-01T00:20:00.000Z",
|
||||
});
|
||||
|
||||
// Retrieve the trace with totalCost and latency
|
||||
const traces = await makeAPICall<TraceAPIResponse>(
|
||||
"GET",
|
||||
`/api/public/traces`,
|
||||
);
|
||||
const traceData = traces.body.data[0];
|
||||
if (!traceData) throw new Error("traceData is undefined");
|
||||
|
||||
// Check if the totalCost and latency are calculated correctly
|
||||
expect(traceData.totalCost).toBeCloseTo(15.75); // Sum of costs
|
||||
expect(traceData.latency).toBeCloseTo(1200); // Difference in seconds between min startTime and max endTime
|
||||
});
|
||||
});
|
||||
|
||||
@@ -31,7 +31,7 @@ import { type DashboardDateRange } from "@/src/pages/project/[projectId]";
|
||||
import { isValidOption } from "@/src/utils/types";
|
||||
import { setBeginningOfDay, setEndOfDay } from "@/src/utils/dates";
|
||||
|
||||
export const DEFAULT_DATE_RANGE_SELECTION = "Select date range" as const;
|
||||
export const DEFAULT_DATE_RANGE_SELECTION = "Date range" as const;
|
||||
export type AvailableDateRangeSelections =
|
||||
| typeof DEFAULT_DATE_RANGE_SELECTION
|
||||
| DateTimeAggregationOption;
|
||||
@@ -163,7 +163,7 @@ export function DatePickerWithRange({
|
||||
id="date"
|
||||
variant={"outline"}
|
||||
className={cn(
|
||||
"w-[350px] justify-start text-left font-normal",
|
||||
"w-[330px] justify-start text-left font-normal",
|
||||
!internalDateRange && "text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
@@ -171,8 +171,8 @@ export function DatePickerWithRange({
|
||||
{internalDateRange?.from ? (
|
||||
internalDateRange.to ? (
|
||||
<>
|
||||
{format(internalDateRange.from, "LLL dd, y : hh:mm")} -{" "}
|
||||
{format(internalDateRange.to, "LLL dd, y : hh:mm")}
|
||||
{format(internalDateRange.from, "LLL dd, yy : hh:mm")} -{" "}
|
||||
{format(internalDateRange.to, "LLL dd, yy : hh:mm")}
|
||||
</>
|
||||
) : (
|
||||
format(internalDateRange.from, "LLL dd, y")
|
||||
@@ -194,7 +194,7 @@ export function DatePickerWithRange({
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<Select value={selectedOption} onValueChange={onDropDownSelection}>
|
||||
<SelectTrigger className="w-40 hover:bg-accent hover:text-accent-foreground focus:ring-0 focus:ring-offset-0">
|
||||
<SelectTrigger className="w-[120px] hover:bg-accent hover:text-accent-foreground focus:ring-0 focus:ring-offset-0">
|
||||
<SelectValue placeholder="Select" />
|
||||
</SelectTrigger>
|
||||
<SelectContent position="popper" defaultValue={60}>
|
||||
|
||||
@@ -77,7 +77,12 @@ export default function Layout(props: PropsWithChildren) {
|
||||
// RBAC
|
||||
if (
|
||||
route.rbacScope !== undefined &&
|
||||
(!projectId || !hasAccess({ projectId, scope: route.rbacScope, session }))
|
||||
(!projectId ||
|
||||
!hasAccess({
|
||||
projectId,
|
||||
scope: route.rbacScope,
|
||||
session: session.data,
|
||||
}))
|
||||
)
|
||||
return null;
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import { ManualScoreButton } from "@/src/features/manual-scoring/components/Manu
|
||||
import { DetailPageNav } from "@/src/features/navigate-detail-pages/DetailPageNav";
|
||||
import { useDetailPageLists } from "@/src/features/navigate-detail-pages/context";
|
||||
import { api } from "@/src/utils/api";
|
||||
import { usdFormatter } from "@/src/utils/numbers";
|
||||
import Link from "next/link";
|
||||
import { useEffect } from "react";
|
||||
|
||||
@@ -88,6 +89,11 @@ export const SessionPage: React.FC<{
|
||||
</Link>
|
||||
))}
|
||||
<Badge variant="outline">Traces: {session.data?.traces.length}</Badge>
|
||||
{session.data && (
|
||||
<Badge variant="outline">
|
||||
Total cost: {usdFormatter(session.data.totalCost, 2, 2)}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-5 flex flex-col gap-2 border-t pt-5">
|
||||
{session.data?.traces.map((trace) => (
|
||||
|
||||
@@ -17,7 +17,7 @@ export default function TableLink({
|
||||
href={path}
|
||||
title={value}
|
||||
>
|
||||
{value.length > truncateAt
|
||||
{value.length - truncateAt > 3
|
||||
? `...${value.substring(value.length - truncateAt)}`
|
||||
: value}
|
||||
</Link>
|
||||
|
||||
@@ -4,6 +4,7 @@ import TableLink from "@/src/components/table/table-link";
|
||||
import { type LangfuseColumnDef } from "@/src/components/table/types";
|
||||
import useColumnVisibility from "@/src/features/column-visibility/hooks/useColumnVisibility";
|
||||
import { useQueryFilterState } from "@/src/features/filters/hooks/useFilterState";
|
||||
import { useOrderByState } from "@/src/features/orderBy/hooks/useOrderByState";
|
||||
import { scoresTableColsWithOptions } from "@/src/server/api/definitions/scoresTable";
|
||||
import { api } from "@/src/utils/api";
|
||||
import { type RouterInput } from "@/src/utils/types";
|
||||
@@ -49,11 +50,17 @@ export default function ScoresTable({
|
||||
])
|
||||
: userFilterState;
|
||||
|
||||
const [orderByState, setOrderByState] = useOrderByState({
|
||||
column: "timestamp",
|
||||
order: "DESC",
|
||||
});
|
||||
|
||||
const scores = api.scores.all.useQuery({
|
||||
page: paginationState.pageIndex,
|
||||
limit: paginationState.pageSize,
|
||||
projectId,
|
||||
filter: filterState,
|
||||
orderBy: orderByState,
|
||||
});
|
||||
const totalCount = scores.data?.slice(1)[0]?.totalCount ?? 0;
|
||||
|
||||
@@ -162,6 +169,8 @@ export default function ScoresTable({
|
||||
onChange: setPaginationState,
|
||||
state: paginationState,
|
||||
}}
|
||||
orderBy={orderByState}
|
||||
setOrderBy={setOrderByState}
|
||||
columnVisibility={columnVisibility}
|
||||
onColumnVisibilityChange={setColumnVisibility}
|
||||
/>
|
||||
|
||||
@@ -11,6 +11,7 @@ import { useOrderByState } from "@/src/features/orderBy/hooks/useOrderByState";
|
||||
import { sessionsViewCols } from "@/src/server/api/definitions/sessionsView";
|
||||
import { api } from "@/src/utils/api";
|
||||
import { formatInterval, utcDateOffsetByDays } from "@/src/utils/dates";
|
||||
import { usdFormatter } from "@/src/utils/numbers";
|
||||
import { type RouterOutput } from "@/src/utils/types";
|
||||
import { useEffect } from "react";
|
||||
import { NumberParam, useQueryParams, withDefault } from "use-query-params";
|
||||
@@ -22,6 +23,7 @@ export type SessionTableRow = {
|
||||
countTraces: number;
|
||||
bookmarked: boolean;
|
||||
sessionDuration: number | null;
|
||||
totalCost: number;
|
||||
};
|
||||
|
||||
export type SessionTableProps = {
|
||||
@@ -98,6 +100,7 @@ export default function SessionsTable({
|
||||
countTraces: session.countTraces,
|
||||
bookmarked: session.bookmarked,
|
||||
sessionDuration: session.sessionDuration,
|
||||
totalCost: session.totalCost,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -132,6 +135,7 @@ export default function SessionsTable({
|
||||
<TableLink
|
||||
path={`/project/${projectId}/sessions/${encodeURIComponent(value)}`}
|
||||
value={value}
|
||||
truncateAt={40}
|
||||
/>
|
||||
) : undefined;
|
||||
},
|
||||
@@ -185,6 +189,20 @@ export default function SessionsTable({
|
||||
enableHiding: true,
|
||||
enableSorting: true,
|
||||
},
|
||||
{
|
||||
accessorKey: "totalCost",
|
||||
id: "totalCost",
|
||||
header: "Total Cost",
|
||||
enableHiding: true,
|
||||
enableSorting: true,
|
||||
cell: ({ row }) => {
|
||||
const value: number | undefined = row.getValue("totalCost");
|
||||
|
||||
return value !== undefined ? (
|
||||
<span>{usdFormatter(value, 2, 2)}</span>
|
||||
) : undefined;
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const [columnVisibility, setColumnVisibility] =
|
||||
|
||||
@@ -185,6 +185,7 @@ export default function TracesTable({
|
||||
}
|
||||
}}
|
||||
aria-label="Select all"
|
||||
className="opacity-60"
|
||||
/>
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
@@ -192,6 +193,7 @@ export default function TracesTable({
|
||||
checked={row.getIsSelected()}
|
||||
onCheckedChange={(value) => row.toggleSelected(!!value)}
|
||||
aria-label="Select row"
|
||||
className="opacity-60"
|
||||
/>
|
||||
),
|
||||
},
|
||||
@@ -439,9 +441,8 @@ export default function TracesTable({
|
||||
actionButtons={
|
||||
<TraceTableMultiSelectAction
|
||||
// Exclude traces that are not in the current page
|
||||
selectedTraceIds={Object.keys(selectedRows).filter(
|
||||
(traceId) =>
|
||||
traces.data?.traces.map((t) => t.id).includes(traceId),
|
||||
selectedTraceIds={Object.keys(selectedRows).filter((traceId) =>
|
||||
traces.data?.traces.map((t) => t.id).includes(traceId),
|
||||
)}
|
||||
projectId={projectId}
|
||||
onDeleteSuccess={() => {
|
||||
|
||||
@@ -35,11 +35,27 @@ export const IOPreview: React.FC<{
|
||||
if (!inOpenAiMessageArray.success) {
|
||||
// check if input is an array of length 1 including an array of OpenAiMessageSchema
|
||||
// this is the case for some integrations
|
||||
// e.g. [[OpenAiMessageSchema, ...]]
|
||||
const inputArray = z.array(OpenAiMessageArraySchema).safeParse(input);
|
||||
if (inputArray.success && inputArray.data.length === 1) {
|
||||
inOpenAiMessageArray = OpenAiMessageArraySchema.safeParse(
|
||||
inputArray.data[0],
|
||||
);
|
||||
} else {
|
||||
// check if input is an object with a messages key
|
||||
// this is the case for some integrations
|
||||
// e.g. { messages: [OpenAiMessageSchema, ...] }
|
||||
const inputObject = z
|
||||
.object({
|
||||
messages: OpenAiMessageArraySchema,
|
||||
})
|
||||
.safeParse(input);
|
||||
|
||||
if (inputObject.success) {
|
||||
inOpenAiMessageArray = OpenAiMessageArraySchema.safeParse(
|
||||
inputObject.data.messages,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
const outOpenAiMessage = OpenAiMessageSchema.safeParse(output);
|
||||
@@ -102,9 +118,15 @@ export const IOPreview: React.FC<{
|
||||
|
||||
const OpenAiMessageSchema = z
|
||||
.object({
|
||||
role: z.enum(["system", "user", "assistant"]).optional(),
|
||||
role: z.enum(["system", "user", "assistant", "function"]).optional(),
|
||||
name: z.string().optional(),
|
||||
content: z.union([z.record(z.any()), z.string()]).nullable(),
|
||||
content: z.union([z.record(z.any()).array(), z.string()]).nullable(),
|
||||
function_call: z
|
||||
.object({
|
||||
name: z.string(),
|
||||
arguments: z.record(z.any()),
|
||||
})
|
||||
.optional(),
|
||||
})
|
||||
.strict() // no additional properties
|
||||
.refine((value) => value.content !== null || value.role !== undefined);
|
||||
@@ -131,7 +153,7 @@ const OpenAiMessageView: React.FC<{
|
||||
<Fragment key={index}>
|
||||
<JSONView
|
||||
title={message.name ?? message.role}
|
||||
json={message.content}
|
||||
json={message.function_call ?? message.content}
|
||||
className={cn(
|
||||
message.role === "system" && "bg-gray-100",
|
||||
message.role === "assistant" && "bg-green-50",
|
||||
|
||||
@@ -1 +1 @@
|
||||
export const VERSION = "v2.4.3";
|
||||
export const VERSION = "v2.6.0";
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
} from "@/src/features/dashboard/components/hooks";
|
||||
import { NoData } from "@/src/features/dashboard/components/NoData";
|
||||
import DocPopup from "@/src/components/layouts/doc-popup";
|
||||
import { createTracesTimeFilter } from "@/src/features/dashboard/lib/dashboard-utils";
|
||||
|
||||
export function ChartScores(props: {
|
||||
className?: string;
|
||||
@@ -25,9 +26,7 @@ export function ChartScores(props: {
|
||||
projectId: props.projectId,
|
||||
from: "traces_scores",
|
||||
select: [{ column: "scoreName" }, { column: "value", agg: "AVG" }],
|
||||
filter: props.globalFilterState.map((f) =>
|
||||
f.type === "datetime" ? { ...f, column: "timestamp" } : f,
|
||||
),
|
||||
filter: createTracesTimeFilter(props.globalFilterState),
|
||||
groupBy: [
|
||||
{
|
||||
type: "datetime",
|
||||
|
||||
@@ -30,9 +30,10 @@ export const LatencyChart = ({
|
||||
const latencies = api.dashboard.chart.useQuery(
|
||||
{
|
||||
projectId,
|
||||
from: "observations",
|
||||
from: "traces_observations",
|
||||
select: [
|
||||
{ column: "duration", agg: "50thPercentile" },
|
||||
{ column: "duration", agg: "75thPercentile" },
|
||||
{ column: "duration", agg: "90thPercentile" },
|
||||
{ column: "duration", agg: "95thPercentile" },
|
||||
{ column: "duration", agg: "99thPercentile" },
|
||||
@@ -83,6 +84,10 @@ export const LatencyChart = ({
|
||||
tabTitle: "50th Percentile",
|
||||
data: getData("percentile50Duration"),
|
||||
},
|
||||
{
|
||||
tabTitle: "75th Percentile",
|
||||
data: getData("percentile75Duration"),
|
||||
},
|
||||
{
|
||||
tabTitle: "90th Percentile",
|
||||
data: getData("percentile90Duration"),
|
||||
|
||||
@@ -19,7 +19,7 @@ export const MetricTable = ({
|
||||
const metrics = api.dashboard.chart.useQuery(
|
||||
{
|
||||
projectId,
|
||||
from: "observations",
|
||||
from: "traces_observations",
|
||||
select: [
|
||||
{ column: "calculatedTotalCost", agg: "SUM" },
|
||||
{ column: "totalTokens", agg: "SUM" },
|
||||
|
||||
@@ -33,7 +33,7 @@ export const ModelUsageChart = ({
|
||||
const tokens = api.dashboard.chart.useQuery(
|
||||
{
|
||||
projectId,
|
||||
from: "observations",
|
||||
from: "traces_observations",
|
||||
select: [
|
||||
{ column: "totalTokens", agg: "SUM" },
|
||||
{ column: "calculatedTotalCost", agg: "SUM" },
|
||||
|
||||
@@ -7,6 +7,7 @@ import { api } from "@/src/utils/api";
|
||||
import { compactNumberFormatter } from "@/src/utils/numbers";
|
||||
import { RightAlignedCell } from "./RightAlignedCell";
|
||||
import { TotalMetric } from "./TotalMetric";
|
||||
import { createTracesTimeFilter } from "@/src/features/dashboard/lib/dashboard-utils";
|
||||
|
||||
export const ScoresTable = ({
|
||||
className,
|
||||
@@ -17,11 +18,7 @@ export const ScoresTable = ({
|
||||
projectId: string;
|
||||
globalFilterState: FilterState;
|
||||
}) => {
|
||||
const localFilters = globalFilterState.map((f) => ({
|
||||
...f,
|
||||
column: "timestamp",
|
||||
}));
|
||||
|
||||
const localFilters = createTracesTimeFilter(globalFilterState);
|
||||
const metrics = api.dashboard.chart.useQuery(
|
||||
{
|
||||
projectId,
|
||||
|
||||
@@ -10,6 +10,7 @@ import { ExpandListButton } from "@/src/features/dashboard/components/cards/Chev
|
||||
import { useState } from "react";
|
||||
import DocPopup from "@/src/components/layouts/doc-popup";
|
||||
import { NoData } from "@/src/features/dashboard/components/NoData";
|
||||
import { createTracesTimeFilter } from "@/src/features/dashboard/lib/dashboard-utils";
|
||||
|
||||
type BarChartDataPoint = {
|
||||
name: string;
|
||||
@@ -61,10 +62,7 @@ export const UserChart = ({
|
||||
projectId,
|
||||
from: "traces",
|
||||
select: [{ column: "user" }, { column: "traceId", agg: "COUNT" }],
|
||||
filter: globalFilterState.map((f) => ({
|
||||
...f,
|
||||
column: "timestamp",
|
||||
})),
|
||||
filter: createTracesTimeFilter(globalFilterState),
|
||||
groupBy: [
|
||||
{
|
||||
type: "string",
|
||||
|
||||
@@ -10,7 +10,7 @@ export const getAllModels = (
|
||||
const allModels = api.dashboard.chart.useQuery(
|
||||
{
|
||||
projectId,
|
||||
from: "observations",
|
||||
from: "traces_observations",
|
||||
select: [{ column: "model" }],
|
||||
filter: [
|
||||
...globalFilterState,
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import { type FilterState } from "@/src/features/filters/types";
|
||||
|
||||
// traces do not have a startTime or endTime column, so we need to map these to the timestamp column
|
||||
export const createTracesTimeFilter = (filters: FilterState) => {
|
||||
return filters.map((f) => {
|
||||
if (f.column === "startTime" || f.column === "endTime") {
|
||||
return {
|
||||
...f,
|
||||
column: "timestamp",
|
||||
};
|
||||
} else {
|
||||
return f;
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -9,11 +9,19 @@ import {
|
||||
protectedProjectProcedure,
|
||||
} from "@/src/server/api/trpc";
|
||||
import { executeQuery } from "@/src/server/api/services/query-builder";
|
||||
import { sqlInterface } from "@/src/server/api/services/sqlInterface";
|
||||
import {
|
||||
filterInterface,
|
||||
sqlInterface,
|
||||
} from "@/src/server/api/services/sqlInterface";
|
||||
|
||||
export const dashboardRouter = createTRPCRouter({
|
||||
chart: protectedProjectProcedure
|
||||
.input(sqlInterface.extend({ projectId: z.string() }))
|
||||
.input(
|
||||
sqlInterface.extend({
|
||||
projectId: z.string(),
|
||||
filter: filterInterface.optional(),
|
||||
}),
|
||||
)
|
||||
.query(async ({ input, ctx }) => {
|
||||
return await executeQuery(ctx.prisma, input.projectId, input);
|
||||
}),
|
||||
|
||||
@@ -97,7 +97,9 @@ export function FilterBuilder({
|
||||
? new Date(filter.value).toLocaleDateString()
|
||||
: filter.type === "stringOptions" ||
|
||||
filter.type === "arrayOptions"
|
||||
? filter.value.join(", ")
|
||||
? filter.value.length > 2
|
||||
? `${filter.value.length} selected`
|
||||
: filter.value.join(", ")
|
||||
: filter.type === "number" ||
|
||||
filter.type === "numberObject"
|
||||
? filter.value
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { type FilterState } from "@/src/features/filters/types";
|
||||
import { filterOperators } from "@/src/server/api/interfaces/filters";
|
||||
import { type ColumnDefinition } from "@/src/server/api/interfaces/tableDefinition";
|
||||
import {
|
||||
type TableNames as TableName,
|
||||
type ColumnDefinition,
|
||||
} from "@/src/server/api/interfaces/tableDefinition";
|
||||
import { Prisma } from "@prisma/client";
|
||||
|
||||
const operatorReplacements = {
|
||||
@@ -18,11 +21,24 @@ const arrayOperatorReplacements = {
|
||||
"none of": "&&",
|
||||
};
|
||||
|
||||
export function filterToPrismaSql(
|
||||
export function tableColumnsToSqlFilterAndPrefix(
|
||||
filters: FilterState,
|
||||
tableColumns: ColumnDefinition[],
|
||||
table: TableName,
|
||||
): Prisma.Sql {
|
||||
const statements = filters.map((filter) => {
|
||||
const sql = tableColumnsToSqlFilter(filters, tableColumns, table);
|
||||
if (sql === Prisma.empty) {
|
||||
return Prisma.empty;
|
||||
}
|
||||
return Prisma.join([Prisma.raw("AND "), sql], "");
|
||||
}
|
||||
|
||||
export function tableColumnsToSqlFilter(
|
||||
filters: FilterState,
|
||||
tableColumns: ColumnDefinition[],
|
||||
table: TableName,
|
||||
): Prisma.Sql {
|
||||
const internalFilters = filters.map((filter) => {
|
||||
// Get column definition to map column to internal name, e.g. "t.id"
|
||||
const col = tableColumns.find(
|
||||
(c) =>
|
||||
@@ -33,8 +49,17 @@ export function filterToPrismaSql(
|
||||
console.error("Invalid filter column", filter.column);
|
||||
throw new Error("Invalid filter column: " + filter.column);
|
||||
}
|
||||
|
||||
const colPrisma = Prisma.raw(col.internal);
|
||||
return {
|
||||
condition: filter,
|
||||
internalColumn: colPrisma,
|
||||
column: col,
|
||||
table: table,
|
||||
};
|
||||
});
|
||||
|
||||
const statements = internalFilters.map((filterAndColumn) => {
|
||||
const filter = filterAndColumn.condition;
|
||||
const operatorPrisma =
|
||||
filter.type === "arrayOptions"
|
||||
? Prisma.raw(
|
||||
@@ -108,18 +133,27 @@ export function filterToPrismaSql(
|
||||
? [Prisma.raw("NOT ("), Prisma.raw(")")]
|
||||
: [Prisma.empty, Prisma.empty];
|
||||
|
||||
return Prisma.sql`${funcPrisma1}${cast1}${colPrisma}${jsonKeyPrisma}${cast2} ${operatorPrisma} ${valuePrefix}${valuePrisma}${valueSuffix}${funcPrisma2}`;
|
||||
return Prisma.sql`${funcPrisma1}${cast1}${filterAndColumn.internalColumn}${jsonKeyPrisma}${cast2} ${operatorPrisma} ${valuePrefix}${valuePrisma}${castValueToPostgresTypes(filterAndColumn.column, filterAndColumn.table)}${valueSuffix}${funcPrisma2}`;
|
||||
});
|
||||
if (statements.length === 0) {
|
||||
return Prisma.empty;
|
||||
}
|
||||
|
||||
return Prisma.join(
|
||||
[Prisma.raw("AND "), Prisma.join(statements, " AND ")],
|
||||
"",
|
||||
);
|
||||
return Prisma.join(statements, " AND ");
|
||||
}
|
||||
|
||||
const castValueToPostgresTypes = (
|
||||
column: ColumnDefinition,
|
||||
table: TableName,
|
||||
) => {
|
||||
return column.name === "type" &&
|
||||
(table === "observations" ||
|
||||
table === "traces_observations" ||
|
||||
table === "traces_parent_observation_scores")
|
||||
? Prisma.sql`::"ObservationType"`
|
||||
: Prisma.empty;
|
||||
};
|
||||
|
||||
const dateOperators = filterOperators["datetime"];
|
||||
|
||||
export const datetimeFilterToPrismaSql = (
|
||||
|
||||
@@ -14,7 +14,7 @@ export function orderByToPrismaSql(
|
||||
tableColumns: ColumnDefinition[],
|
||||
): Prisma.Sql {
|
||||
if (!orderBy) {
|
||||
return Prisma.sql`ORDER BY t.timestamp DESC`;
|
||||
return Prisma.sql`ORDER BY t.timestamp DESC NULLS LAST`;
|
||||
}
|
||||
// Get column definition to map column to internal name, e.g. "t.id"
|
||||
const col = tableColumns.find(
|
||||
@@ -37,5 +37,7 @@ export function orderByToPrismaSql(
|
||||
}
|
||||
|
||||
// Both column and order are safe, can use raw SQL
|
||||
return Prisma.raw(`ORDER BY ${col.internal} ${order.data}`);
|
||||
return Prisma.raw(
|
||||
`ORDER BY ${col.internal} ${order.data} ${orderBy.order === "DESC" ? "NULLS LAST" : "NULLS FIRST"}`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -45,22 +45,20 @@ export const usage = MixedUsage.nullish()
|
||||
unit: ModelUsageUnit.Tokens,
|
||||
};
|
||||
}
|
||||
// if we get the new generic format, we do not set a default
|
||||
if ("input" in v || "output" in v || "total" in v || "unit" in v) {
|
||||
const unit = v.unit;
|
||||
return { ...v, unit };
|
||||
}
|
||||
|
||||
// if the object is empty, we return undefined
|
||||
if (lodash.isEmpty(v)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return v;
|
||||
})
|
||||
// ensure output is always of new usage model
|
||||
.pipe(Usage.nullish());
|
||||
|
||||
export const TraceBody = z.object({
|
||||
id: z.string().nullish(),
|
||||
timestamp: stringDate,
|
||||
name: z.string().nullish(),
|
||||
externalId: z.string().nullish(),
|
||||
input: jsonSchema.nullish(),
|
||||
|
||||
@@ -5,20 +5,24 @@ import {
|
||||
import { type MembershipRole } from "@prisma/client";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { type Session } from "next-auth";
|
||||
import { useSession, type SessionContextValue } from "next-auth/react";
|
||||
import { useSession } from "next-auth/react";
|
||||
|
||||
type HasAccessParams =
|
||||
| {
|
||||
role: MembershipRole;
|
||||
scope: Scope;
|
||||
admin?: boolean; // prop user.admin
|
||||
}
|
||||
| {
|
||||
session: SessionContextValue | Session;
|
||||
session: null | Session;
|
||||
projectId: string;
|
||||
scope: Scope;
|
||||
};
|
||||
|
||||
// For use in TRPC routes
|
||||
/**
|
||||
* Check if user has access to the given scope, for use in TRPC resolvers
|
||||
* @throws TRPCError("UNAUTHORIZED") if user does not have access
|
||||
*/
|
||||
export const throwIfNoAccess = (p: HasAccessParams) => {
|
||||
if (!hasAccess(p))
|
||||
throw new TRPCError({
|
||||
@@ -28,27 +32,26 @@ export const throwIfNoAccess = (p: HasAccessParams) => {
|
||||
});
|
||||
};
|
||||
|
||||
// For use in UI components as react hook
|
||||
/**
|
||||
* React hook to check if user has access to the given scope
|
||||
* @returns true if user has access, false otherwise or while loading
|
||||
*/
|
||||
export const useHasAccess = (p: { projectId: string; scope: Scope }) => {
|
||||
const session = useSession();
|
||||
return hasAccess({ session, ...p });
|
||||
return hasAccess({ session: session.data, ...p });
|
||||
};
|
||||
|
||||
// For use in UI components as function, if session is already available
|
||||
export function hasAccess(p: HasAccessParams): boolean {
|
||||
const role: MembershipRole | undefined =
|
||||
"role" in p
|
||||
? // 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;
|
||||
if (role === undefined) return false;
|
||||
const isAdmin = "role" in p ? p.admin : p.session?.user?.admin;
|
||||
if (isAdmin && p.scope.endsWith(":read")) return true;
|
||||
|
||||
return roleAccessRights[role].includes(p.scope);
|
||||
const projectRole: MembershipRole | undefined =
|
||||
"role" in p
|
||||
? p.role
|
||||
: p.session?.user?.projects.find((project) => project.id === p.projectId)
|
||||
?.role;
|
||||
if (projectRole === undefined) return false;
|
||||
|
||||
return roleAccessRights[projectRole].includes(p.scope);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
import { type NextApiRequest, type NextApiResponse } from "next";
|
||||
import { z } from "zod";
|
||||
import { cors, runMiddleware } from "@/src/features/public-api/server/cors";
|
||||
import { prisma } from "@/src/server/db";
|
||||
import { verifyAuthHeaderAndReturnScope } from "@/src/features/public-api/server/apiAuth";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { paginationZod } from "@/src/utils/zod";
|
||||
|
||||
const GetUsageSchema = z.object({
|
||||
...paginationZod,
|
||||
traceName: z.string().nullish(),
|
||||
userId: z.string().nullish(),
|
||||
tags: z.union([z.array(z.string()), z.string()]).nullish(),
|
||||
});
|
||||
|
||||
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
|
||||
|
||||
try {
|
||||
if (req.method === "GET") {
|
||||
if (authCheck.scope.accessLevel !== "all") {
|
||||
return res.status(401).json({
|
||||
message:
|
||||
"Access denied - need to use basic auth with secret key to GET scores",
|
||||
});
|
||||
}
|
||||
const obj = GetUsageSchema.parse(req.query); // uses query and not body
|
||||
|
||||
const traceNameCondition = obj.traceName
|
||||
? Prisma.sql`AND t.name = ${obj.traceName}`
|
||||
: Prisma.empty;
|
||||
const userCondition = obj.userId
|
||||
? Prisma.sql`AND t."user_id" = ${obj.userId}`
|
||||
: Prisma.empty;
|
||||
const tagsCondition = obj.tags
|
||||
? Prisma.sql`AND ARRAY[${Prisma.join(
|
||||
(Array.isArray(obj.tags) ? obj.tags : [obj.tags]).map(
|
||||
(v) => Prisma.sql`${v}`,
|
||||
),
|
||||
", ",
|
||||
)}] <@ t."tags"`
|
||||
: Prisma.empty;
|
||||
|
||||
const usage = await prisma.$queryRaw`
|
||||
WITH model_usage AS (
|
||||
SELECT
|
||||
DATE_TRUNC('DAY',
|
||||
o.start_time) "date",
|
||||
o.model,
|
||||
SUM(o.prompt_tokens) inputUsage,
|
||||
SUM(o.completion_tokens) outputUsage,
|
||||
SUM(o.total_tokens) totalUsage
|
||||
FROM
|
||||
traces t
|
||||
LEFT JOIN observations o ON o.trace_id = t.id AND o.project_id = t.project_id
|
||||
WHERE o.start_time IS NOT NULL
|
||||
AND t.project_id = ${authCheck.scope.projectId}
|
||||
${traceNameCondition}
|
||||
${userCondition}
|
||||
${tagsCondition}
|
||||
GROUP BY
|
||||
1,
|
||||
2
|
||||
ORDER BY
|
||||
1,
|
||||
2
|
||||
),
|
||||
daily_model_usage AS (
|
||||
SELECT
|
||||
"date",
|
||||
json_agg(json_build_object('model',
|
||||
model,
|
||||
'inputUsage',
|
||||
inputUsage,
|
||||
'outputUsage',
|
||||
outputUsage,
|
||||
'totalUsage',
|
||||
totalUsage)) daily_usage_json
|
||||
FROM
|
||||
model_usage
|
||||
GROUP BY
|
||||
1
|
||||
),
|
||||
daily_stats AS (
|
||||
SELECT
|
||||
DATE_TRUNC('DAY', t.timestamp) "date",
|
||||
count(distinct t.id)::integer count_traces,
|
||||
SUM(o.calculated_total_cost)::DOUBLE PRECISION total_cost
|
||||
FROM traces t
|
||||
LEFT JOIN observations_view o ON o.project_id = t.project_id AND t.id = o.trace_id
|
||||
WHERE t.project_id = ${authCheck.scope.projectId}
|
||||
${traceNameCondition}
|
||||
${userCondition}
|
||||
${tagsCondition}
|
||||
GROUP BY 1
|
||||
)
|
||||
SELECT
|
||||
TO_CHAR(COALESCE(ds.date, daily_model_usage.date), 'YYYY-MM-DD') AS "date",
|
||||
COALESCE(count_traces, 0) "countTraces",
|
||||
COALESCE(total_cost, 0) "totalCost",
|
||||
COALESCE(daily_usage_json, '[]'::JSON) usage
|
||||
FROM
|
||||
daily_stats ds
|
||||
FULL OUTER JOIN
|
||||
daily_model_usage ON daily_model_usage.date = ds.date
|
||||
ORDER BY
|
||||
1 DESC
|
||||
LIMIT ${obj.limit} OFFSET ${(obj.page - 1) * obj.limit}
|
||||
`;
|
||||
|
||||
const totalItemsRes = await prisma.$queryRaw<{ count: number }[]>`
|
||||
SELECT
|
||||
COUNT(DISTINCT DATE_TRUNC('DAY', t.timestamp))::integer
|
||||
FROM traces t
|
||||
WHERE t.project_id = ${authCheck.scope.projectId}
|
||||
${traceNameCondition}
|
||||
${userCondition}
|
||||
${tagsCondition}
|
||||
`;
|
||||
|
||||
const totalItems =
|
||||
totalItemsRes[0] !== undefined ? totalItemsRes[0].count : 0;
|
||||
|
||||
return res.status(200).json({
|
||||
data: usage,
|
||||
meta: {
|
||||
page: obj.page,
|
||||
limit: obj.limit,
|
||||
totalItems,
|
||||
totalPages: Math.ceil(totalItems / obj.limit),
|
||||
},
|
||||
});
|
||||
} else {
|
||||
console.error(req.method, req.body);
|
||||
return res.status(405).json({ message: "Method not allowed" });
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
console.error(error);
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : "An unknown error occurred";
|
||||
res.status(400).json({
|
||||
message: "Invalid request data",
|
||||
error: errorMessage,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,206 +0,0 @@
|
||||
import { type NextApiRequest, type NextApiResponse } from "next";
|
||||
import { z } from "zod";
|
||||
import { cors, runMiddleware } from "@/src/features/public-api/server/cors";
|
||||
import { prisma } from "@/src/server/db";
|
||||
import { verifyAuthHeaderAndReturnScope } from "@/src/features/public-api/server/apiAuth";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { paginationZod } from "@/src/utils/zod";
|
||||
|
||||
const GetUsageSchema = z.object({
|
||||
...paginationZod,
|
||||
group_by: z.enum(["trace_name"]).nullish(),
|
||||
trace_name: z.string().nullish(),
|
||||
});
|
||||
|
||||
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
|
||||
|
||||
try {
|
||||
if (req.method === "GET") {
|
||||
if (authCheck.scope.accessLevel !== "all") {
|
||||
return res.status(401).json({
|
||||
message:
|
||||
"Access denied - need to use basic auth with secret key to GET scores",
|
||||
});
|
||||
}
|
||||
const obj = GetUsageSchema.parse(req.query); // uses query and not body
|
||||
|
||||
const traceNameCondition = obj.trace_name
|
||||
? Prisma.sql`AND t.name = ${obj.trace_name}`
|
||||
: Prisma.empty;
|
||||
|
||||
if (obj.group_by === undefined) {
|
||||
const usage = await prisma.$queryRaw`
|
||||
WITH model_usage AS (
|
||||
SELECT
|
||||
DATE_TRUNC('DAY',
|
||||
o.start_time) observation_day,
|
||||
o.model,
|
||||
SUM(o.prompt_tokens) prompt_tokens,
|
||||
SUM(o.completion_tokens) completion_tokens,
|
||||
SUM(o.total_tokens) total_tokens
|
||||
FROM
|
||||
traces t
|
||||
LEFT JOIN observations o ON o.trace_id = t.id
|
||||
WHERE o.start_time IS NOT NULL
|
||||
AND o.project_id = ${authCheck.scope.projectId}
|
||||
AND t.project_id = ${authCheck.scope.projectId}
|
||||
${traceNameCondition}
|
||||
GROUP BY 1,2
|
||||
order by 1,2
|
||||
),
|
||||
daily_usage AS (
|
||||
SELECT
|
||||
observation_day,
|
||||
json_agg(json_build_object('model',
|
||||
model,
|
||||
'prompt_tokens',
|
||||
prompt_tokens,
|
||||
'completion_tokens',
|
||||
completion_tokens,
|
||||
'total_tokens',
|
||||
total_tokens)) daily_usage_json
|
||||
FROM model_usage
|
||||
group by 1
|
||||
)
|
||||
SELECT
|
||||
observation_day "date",
|
||||
daily_usage_json usage
|
||||
FROM daily_usage
|
||||
ORDER BY 1 desc
|
||||
LIMIT ${obj.limit} OFFSET ${(obj.page - 1) * obj.limit}
|
||||
`;
|
||||
const totalItemsRes = await prisma.$queryRaw<{ count: bigint }[]>`
|
||||
SELECT
|
||||
count(DISTINCT DATE_TRUNC('DAY', observations.start_time))
|
||||
FROM
|
||||
observations
|
||||
JOIN traces ON observations.trace_id = traces.id
|
||||
WHERE traces.project_id = ${authCheck.scope.projectId}
|
||||
`;
|
||||
|
||||
const totalItems =
|
||||
totalItemsRes[0] !== undefined ? Number(totalItemsRes[0].count) : 0;
|
||||
|
||||
return res.status(200).json({
|
||||
data: usage,
|
||||
meta: {
|
||||
page: obj.page,
|
||||
limit: obj.limit,
|
||||
totalItems,
|
||||
totalPages: Math.ceil(totalItems / obj.limit),
|
||||
},
|
||||
});
|
||||
} else if (obj.group_by === "trace_name") {
|
||||
const usage = await prisma.$queryRaw`
|
||||
WITH model_usage AS (
|
||||
SELECT
|
||||
t."name" trace_name,
|
||||
DATE_TRUNC('DAY',
|
||||
o.start_time) observation_day,
|
||||
o.model,
|
||||
SUM(o.prompt_tokens) prompt_tokens,
|
||||
SUM(o.completion_tokens) completion_tokens,
|
||||
SUM(o.total_tokens) total_tokens
|
||||
FROM
|
||||
traces t
|
||||
LEFT JOIN observations o ON o.trace_id = t.id
|
||||
WHERE o.start_time IS NOT NULL
|
||||
AND t.project_id = ${authCheck.scope.projectId}
|
||||
AND o.project_id = ${authCheck.scope.projectId}
|
||||
${traceNameCondition}
|
||||
GROUP BY 1,2,3
|
||||
order by 1,2,3
|
||||
),
|
||||
daily_usage AS (
|
||||
SELECT
|
||||
trace_name,
|
||||
observation_day,
|
||||
json_agg(json_build_object('model',
|
||||
model,
|
||||
'prompt_tokens',
|
||||
prompt_tokens,
|
||||
'completion_tokens',
|
||||
completion_tokens,
|
||||
'total_tokens',
|
||||
total_tokens)) daily_usage_json
|
||||
FROM model_usage
|
||||
WHERE prompt_tokens > 0
|
||||
OR completion_tokens > 0
|
||||
OR total_tokens > 0
|
||||
group by 1,2
|
||||
order by 1,2 desc
|
||||
),
|
||||
all_trace_names AS (
|
||||
SELECT t."name" trace_name
|
||||
FROM traces t
|
||||
WHERE t.project_id = ${authCheck.scope.projectId}
|
||||
${traceNameCondition}
|
||||
GROUP BY 1
|
||||
)
|
||||
SELECT
|
||||
all_trace_names.trace_name,
|
||||
json_agg(json_build_object(
|
||||
'date',
|
||||
observation_day,
|
||||
'usage',
|
||||
daily_usage_json
|
||||
)) metrics
|
||||
FROM all_trace_names
|
||||
LEFT JOIN daily_usage ON all_trace_names.trace_name = daily_usage.trace_name
|
||||
group by 1
|
||||
ORDER BY 1
|
||||
LIMIT ${obj.limit} OFFSET ${(obj.page - 1) * obj.limit}
|
||||
`;
|
||||
const totalItemsRes = await prisma.$queryRaw<{ count: bigint }[]>`
|
||||
SELECT
|
||||
count(DISTINCT CASE WHEN "name" IS NULL THEN 'COUNT_NULL' ELSE "name" END)
|
||||
FROM
|
||||
traces
|
||||
WHERE project_id = ${authCheck.scope.projectId}
|
||||
`;
|
||||
|
||||
const totalItems =
|
||||
totalItemsRes[0] !== undefined ? Number(totalItemsRes[0].count) : 0;
|
||||
|
||||
return res.status(200).json({
|
||||
data: usage,
|
||||
meta: {
|
||||
page: obj.page,
|
||||
limit: obj.limit,
|
||||
totalItems,
|
||||
totalPages: Math.ceil(totalItems / obj.limit),
|
||||
},
|
||||
});
|
||||
} else {
|
||||
return res.status(400).json({
|
||||
message: "Invalid group_by value",
|
||||
});
|
||||
}
|
||||
} else {
|
||||
console.error(req.method, req.body);
|
||||
return res.status(405).json({ message: "Method not allowed" });
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
console.error(error);
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : "An unknown error occurred";
|
||||
res.status(400).json({
|
||||
message: "Invalid request data",
|
||||
error: errorMessage,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -138,8 +138,9 @@ const getObservation = async (
|
||||
o."total_price" as "totalPrice",
|
||||
o."calculated_input_cost" as "calculatedInputCost",
|
||||
o."calculated_output_cost" as "calculatedOutputCost",
|
||||
o."calculated_total_cost" as "calculatedTotalCost"
|
||||
FROM observations_view o LEFT JOIN traces ON o."trace_id" = traces."id"
|
||||
o."calculated_total_cost" as "calculatedTotalCost",
|
||||
o."latency"
|
||||
FROM observations_view o LEFT JOIN traces ON o."trace_id" = traces."id" AND traces."project_id" = o."project_id"
|
||||
WHERE o."project_id" = ${authenticatedProjectId}
|
||||
${nameCondition}
|
||||
${userIdCondition}
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
import { prisma } from "@/src/server/db";
|
||||
import { type NextApiRequest, type NextApiResponse } from "next";
|
||||
import { z } from "zod";
|
||||
import { cors, runMiddleware } from "@/src/features/public-api/server/cors";
|
||||
import { verifyAuthHeaderAndReturnScope } from "@/src/features/public-api/server/apiAuth";
|
||||
|
||||
const ScoreDeleteSchema = z.object({
|
||||
scoreId: z.string(),
|
||||
});
|
||||
|
||||
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 === "DELETE") {
|
||||
try {
|
||||
if (authCheck.scope.accessLevel !== "all") {
|
||||
return res.status(401).json({
|
||||
message:
|
||||
"Access denied - need to use basic auth with secret key to DELETE scores",
|
||||
});
|
||||
}
|
||||
|
||||
const { scoreId } = ScoreDeleteSchema.parse(req.query); // uses query and not body
|
||||
|
||||
const score = await prisma.score.findUnique({
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
where: {
|
||||
id: scoreId,
|
||||
trace: {
|
||||
projectId: authCheck.scope.projectId,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!score) {
|
||||
return res.status(404).json({
|
||||
message: "Score not found within authorized project",
|
||||
});
|
||||
}
|
||||
|
||||
await prisma.score.delete({
|
||||
where: {
|
||||
id: scoreId,
|
||||
trace: {
|
||||
projectId: authCheck.scope.projectId,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return res.status(200).json({ message: "Score deleted successfully" });
|
||||
} catch (error: unknown) {
|
||||
console.error(error);
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : "An unknown error occurred";
|
||||
res.status(400).json({
|
||||
message: "Invalid request data",
|
||||
error: errorMessage,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
return res.status(405).json({ message: "Method not allowed" });
|
||||
}
|
||||
}
|
||||
@@ -114,6 +114,7 @@ export default async function handler(
|
||||
>(Prisma.sql`
|
||||
SELECT
|
||||
t.id,
|
||||
CONCAT('/project/', t.project_id,'/traces/',t.id) as "htmlPath",
|
||||
t.timestamp,
|
||||
t.name,
|
||||
t.project_id as "projectId",
|
||||
@@ -123,10 +124,12 @@ export default async function handler(
|
||||
t.release,
|
||||
t.version,
|
||||
t.tags,
|
||||
COALESCE(SUM(o.calculated_total_cost), 0)::DOUBLE PRECISION AS "totalCost",
|
||||
COALESCE(EXTRACT(EPOCH FROM COALESCE(MAX(o."end_time"), MAX(o."start_time"))) - EXTRACT(EPOCH FROM MIN(o."start_time")), 0)::double precision AS "latency",
|
||||
array_remove(array_agg(o.id), NULL) AS "observations",
|
||||
array_remove(array_agg(s.id), NULL) AS "scores"
|
||||
FROM "traces" AS t
|
||||
LEFT JOIN "observations" AS o ON t.id = o.trace_id AND o.project_id = ${authCheck.scope.projectId}
|
||||
LEFT JOIN "observations_view" AS o ON t.id = o.trace_id AND o.project_id = ${authCheck.scope.projectId}
|
||||
LEFT JOIN "scores" AS s ON t.id = s.trace_id
|
||||
WHERE t.project_id = ${authCheck.scope.projectId}
|
||||
${userCondition}
|
||||
|
||||
@@ -28,6 +28,10 @@ import { usePostHog } from "posthog-js/react";
|
||||
import { FeedbackButtonWrapper } from "@/src/features/feedback/component/FeedbackButton";
|
||||
import { BarChart2 } from "lucide-react";
|
||||
import { Button } from "@/src/components/ui/button";
|
||||
import { FilterBuilder } from "@/src/features/filters/components/filter-builder";
|
||||
import { type FilterState } from "@/src/features/filters/types";
|
||||
import { type ColumnDefinition } from "@/src/server/api/interfaces/tableDefinition";
|
||||
import { useQueryFilterState } from "@/src/features/filters/hooks/useFilterState";
|
||||
|
||||
export type DashboardDateRange = {
|
||||
from: Date;
|
||||
@@ -75,7 +79,32 @@ export default function Start() {
|
||||
});
|
||||
};
|
||||
|
||||
const globalFilterState = dateRange
|
||||
const traceFilterOptions = api.traces.filterOptions.useQuery(
|
||||
{
|
||||
projectId,
|
||||
},
|
||||
{
|
||||
trpc: {
|
||||
context: {
|
||||
skipBatch: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
const values = traceFilterOptions.data?.name || [];
|
||||
|
||||
const traceName: ColumnDefinition[] = [
|
||||
{
|
||||
name: "traceName",
|
||||
type: "stringOptions" as const,
|
||||
options: values,
|
||||
internal: "internalValue",
|
||||
},
|
||||
];
|
||||
|
||||
const [userFilterState, setUserFilterState] = useQueryFilterState([]);
|
||||
|
||||
const timeFilter = dateRange
|
||||
? [
|
||||
{
|
||||
type: "datetime" as const,
|
||||
@@ -92,22 +121,31 @@ export default function Start() {
|
||||
]
|
||||
: [];
|
||||
|
||||
const mergedFilterState: FilterState = [...userFilterState, ...timeFilter];
|
||||
|
||||
return (
|
||||
<div className="md:container">
|
||||
<Header title={project?.name ?? "Dashboard"} />
|
||||
<div className="flex flex-wrap items-center justify-between">
|
||||
<DatePickerWithRange
|
||||
dateRange={dateRange}
|
||||
setAgg={setAgg}
|
||||
setDateRangeAndOption={setDateRangeAndOption}
|
||||
selectedOption={selectedOption}
|
||||
className="max-w-full overflow-x-auto"
|
||||
/>
|
||||
<div className="my-3 flex flex-wrap items-center justify-between gap-2">
|
||||
<div className=" flex flex-col gap-2 lg:flex-row">
|
||||
<DatePickerWithRange
|
||||
dateRange={dateRange}
|
||||
setAgg={setAgg}
|
||||
setDateRangeAndOption={setDateRangeAndOption}
|
||||
selectedOption={selectedOption}
|
||||
className="my-0 max-w-full overflow-x-auto"
|
||||
/>
|
||||
<FilterBuilder
|
||||
columns={traceName}
|
||||
filterState={userFilterState}
|
||||
onChange={setUserFilterState}
|
||||
/>
|
||||
</div>
|
||||
<FeedbackButtonWrapper
|
||||
title="Request Chart"
|
||||
description="Your feedback matters! Let the Langfuse team know what additional data or metrics you'd like to see in your dashboard."
|
||||
type="dashboard"
|
||||
className="hidden md:flex"
|
||||
className="hidden lg:flex"
|
||||
>
|
||||
<Button
|
||||
id="date"
|
||||
@@ -128,48 +166,47 @@ export default function Start() {
|
||||
<TracesBarListChart
|
||||
className="col-span-1 xl:col-span-2 "
|
||||
projectId={projectId}
|
||||
globalFilterState={globalFilterState}
|
||||
globalFilterState={mergedFilterState}
|
||||
/>
|
||||
|
||||
<MetricTable
|
||||
className="col-span-1 xl:col-span-2"
|
||||
projectId={projectId}
|
||||
globalFilterState={globalFilterState}
|
||||
globalFilterState={mergedFilterState}
|
||||
/>
|
||||
<ScoresTable
|
||||
className="col-span-1 xl:col-span-2"
|
||||
projectId={projectId}
|
||||
globalFilterState={globalFilterState}
|
||||
globalFilterState={mergedFilterState}
|
||||
/>
|
||||
<TracesTimeSeriesChart
|
||||
className="col-span-1 xl:col-span-3"
|
||||
projectId={projectId}
|
||||
globalFilterState={globalFilterState}
|
||||
globalFilterState={mergedFilterState}
|
||||
agg={agg}
|
||||
/>
|
||||
<ModelUsageChart
|
||||
className="col-span-1 min-h-24 xl:col-span-3"
|
||||
projectId={projectId}
|
||||
globalFilterState={globalFilterState}
|
||||
globalFilterState={mergedFilterState}
|
||||
agg={agg}
|
||||
/>
|
||||
<UserChart
|
||||
className="col-span-1 xl:col-span-3"
|
||||
projectId={projectId}
|
||||
globalFilterState={globalFilterState}
|
||||
globalFilterState={mergedFilterState}
|
||||
agg={agg}
|
||||
/>
|
||||
<ChartScores
|
||||
className="col-span-1 xl:col-span-3"
|
||||
agg={agg}
|
||||
projectId={projectId}
|
||||
globalFilterState={globalFilterState}
|
||||
globalFilterState={mergedFilterState}
|
||||
/>
|
||||
<LatencyChart
|
||||
className="col-span-1 flex-auto justify-between xl:col-span-full"
|
||||
projectId={projectId}
|
||||
agg={agg}
|
||||
globalFilterState={globalFilterState}
|
||||
globalFilterState={mergedFilterState}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -6,7 +6,7 @@ import { useEffect, useState } from "react";
|
||||
import TableLink from "@/src/components/table/table-link";
|
||||
import { DataTable } from "@/src/components/table/data-table";
|
||||
import { useRouter } from "next/router";
|
||||
import { compactNumberFormatter } from "@/src/utils/numbers";
|
||||
import { compactNumberFormatter, usdFormatter } from "@/src/utils/numbers";
|
||||
import { GroupedScoreBadges } from "@/src/components/grouped-score-badge";
|
||||
import { type Score } from "@prisma/client";
|
||||
import { useQueryParams, withDefault, NumberParam } from "use-query-params";
|
||||
@@ -75,6 +75,10 @@ export default function UsersPage() {
|
||||
accessorKey: "firstEvent",
|
||||
header: "First Event",
|
||||
},
|
||||
{
|
||||
accessorKey: "totalCost",
|
||||
header: "Total Cost",
|
||||
},
|
||||
{
|
||||
accessorKey: "lastEvent",
|
||||
header: "Last Event",
|
||||
@@ -151,6 +155,7 @@ export default function UsersPage() {
|
||||
),
|
||||
totalTokens: compactNumberFormatter(t.totalTokens),
|
||||
lastScore: t.lastScore,
|
||||
totalCost: usdFormatter(t.sumCalculatedTotalCost, 2, 2),
|
||||
};
|
||||
}),
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import Header from "@/src/components/layouts/header";
|
||||
import { api } from "@/src/utils/api";
|
||||
import TracesTable from "@/src/components/table/use-cases/traces";
|
||||
import ScoresTable from "@/src/components/table/use-cases/scores";
|
||||
import { compactNumberFormatter } from "@/src/utils/numbers";
|
||||
import { compactNumberFormatter, usdFormatter } from "@/src/utils/numbers";
|
||||
import { GroupedScoreBadges } from "@/src/components/grouped-score-badge";
|
||||
import TableLink from "@/src/components/table/table-link";
|
||||
import { StringParam, useQueryParam, withDefault } from "use-query-params";
|
||||
@@ -137,6 +137,10 @@ function DetailsTab({ userId, projectId }: TabProps) {
|
||||
label: "Total Tokens",
|
||||
value: compactNumberFormatter(user.data.totalTokens),
|
||||
},
|
||||
{
|
||||
label: "Total Cost",
|
||||
value: usdFormatter(user.data.sumCalculatedTotalCost, 2, 2),
|
||||
},
|
||||
]
|
||||
: [];
|
||||
|
||||
@@ -174,6 +178,7 @@ function DetailsTab({ userId, projectId }: TabProps) {
|
||||
: `/project/${projectId}/traces/${user.data.lastScore.traceId}`
|
||||
}
|
||||
value={user.data.lastScore.traceId}
|
||||
truncateAt={40}
|
||||
/>
|
||||
<GroupedScoreBadges scores={[user.data.lastScore]} />
|
||||
</div>
|
||||
|
||||
@@ -32,6 +32,17 @@ export type ColumnDefinition =
|
||||
keyOptions?: Array<string>;
|
||||
};
|
||||
|
||||
export const tableNames = [
|
||||
"traces",
|
||||
"traces_observations",
|
||||
"observations",
|
||||
"traces_scores",
|
||||
"traces_parent_observation_scores",
|
||||
"sessions",
|
||||
] as const;
|
||||
|
||||
export type TableNames = (typeof tableNames)[number];
|
||||
|
||||
export type TableDefinitions = {
|
||||
[tableName: string]: {
|
||||
table: string;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {
|
||||
datetimeFilterToPrismaSql,
|
||||
filterToPrismaSql,
|
||||
tableColumnsToSqlFilterAndPrefix,
|
||||
} from "@/src/features/filters/server/filterToPrisma";
|
||||
import { orderByToPrismaSql } from "@/src/features/orderBy/server/orderByToPrisma";
|
||||
import { observationsTableCols } from "@/src/server/api/definitions/observationsTable";
|
||||
@@ -29,9 +29,10 @@ export function getAllGenerationsSqlQuery({
|
||||
)`
|
||||
: Prisma.empty;
|
||||
|
||||
const filterCondition = filterToPrismaSql(
|
||||
const filterCondition = tableColumnsToSqlFilterAndPrefix(
|
||||
input.filter,
|
||||
observationsTableCols,
|
||||
"observations",
|
||||
);
|
||||
|
||||
const orderByCondition = orderByToPrismaSql(
|
||||
@@ -52,7 +53,7 @@ export function getAllGenerationsSqlQuery({
|
||||
)
|
||||
: Prisma.empty;
|
||||
|
||||
// For exports: use a date cutoff filter to ignore ingested rows
|
||||
// For exports: use a date cutoff filter to ignore newly ingested rows
|
||||
const dateCutoffFilter =
|
||||
type === "export"
|
||||
? datetimeFilterToPrismaSql("start_time", "<", new Date())
|
||||
@@ -65,18 +66,7 @@ export function getAllGenerationsSqlQuery({
|
||||
: Prisma.empty;
|
||||
|
||||
const rawSqlQuery = Prisma.sql`
|
||||
WITH observations_with_latency AS (
|
||||
SELECT
|
||||
o.*,
|
||||
CASE WHEN o.end_time IS NULL THEN NULL ELSE (EXTRACT(EPOCH FROM o."end_time") - EXTRACT(EPOCH FROM o."start_time"))::double precision END AS "latency"
|
||||
FROM observations_view o
|
||||
WHERE o.type = 'GENERATION'
|
||||
AND o.project_id = ${input.projectId}
|
||||
${datetimeFilter}
|
||||
${dateCutoffFilter}
|
||||
),
|
||||
-- used for filtering
|
||||
scores_avg AS (
|
||||
WITH scores_avg AS (
|
||||
SELECT
|
||||
trace_id,
|
||||
observation_id,
|
||||
@@ -123,12 +113,17 @@ export function getAllGenerationsSqlQuery({
|
||||
o.total_price as "totalPrice",
|
||||
o.calculated_input_cost as "calculatedInputCost",
|
||||
o.calculated_output_cost as "calculatedOutputCost",
|
||||
o.calculated_total_cost as "calculatedTotalCost"
|
||||
FROM observations_with_latency o
|
||||
JOIN traces t ON t.id = o.trace_id
|
||||
o.calculated_total_cost as "calculatedTotalCost",
|
||||
o."latency"
|
||||
FROM observations_view o
|
||||
JOIN traces t ON t.id = o.trace_id AND t.project_id = o.project_id
|
||||
LEFT JOIN scores_avg AS s_avg ON s_avg.trace_id = t.id and s_avg.observation_id = o.id
|
||||
WHERE
|
||||
t.project_id = ${input.projectId}
|
||||
o.project_id = ${input.projectId}
|
||||
AND t.project_id = ${input.projectId}
|
||||
AND o.type = 'GENERATION'
|
||||
${datetimeFilter}
|
||||
${dateCutoffFilter}
|
||||
${searchCondition}
|
||||
${filterCondition}
|
||||
${orderByCondition}
|
||||
|
||||
@@ -40,6 +40,7 @@ export const generationsExportQuery = protectedProjectProcedure
|
||||
input,
|
||||
type: "export",
|
||||
});
|
||||
|
||||
const queryPageSize = env.DB_EXPORT_PAGE_SIZE ?? 1000;
|
||||
const dbReadStream = new DatabaseReadStream<ObservationView>(
|
||||
ctx.prisma,
|
||||
|
||||
@@ -30,17 +30,7 @@ export const getAllQuery = protectedProjectProcedure
|
||||
Array<{ count: bigint }>
|
||||
>(
|
||||
Prisma.sql`
|
||||
WITH observations_with_latency AS (
|
||||
SELECT
|
||||
o.*,
|
||||
CASE WHEN o.end_time IS NULL THEN NULL ELSE (EXTRACT(EPOCH FROM o."end_time") - EXTRACT(EPOCH FROM o."start_time"))::double precision END AS "latency"
|
||||
FROM observations_view o
|
||||
WHERE o.type = 'GENERATION'
|
||||
AND o.project_id = ${input.projectId}
|
||||
${datetimeFilter}
|
||||
),
|
||||
-- used for filtering
|
||||
scores_avg AS (
|
||||
WITH scores_avg AS (
|
||||
SELECT
|
||||
trace_id,
|
||||
observation_id,
|
||||
@@ -64,11 +54,14 @@ export const getAllQuery = protectedProjectProcedure
|
||||
)
|
||||
SELECT
|
||||
count(*)
|
||||
FROM observations_with_latency o
|
||||
JOIN traces t ON t.id = o.trace_id
|
||||
FROM observations_view o
|
||||
JOIN traces t ON t.id = o.trace_id AND t.project_id = o.project_id
|
||||
LEFT JOIN scores_avg AS s_avg ON s_avg.trace_id = t.id and s_avg.observation_id = o.id
|
||||
WHERE
|
||||
t.project_id = ${input.projectId}
|
||||
AND o.type = 'GENERATION'
|
||||
AND o.project_id = ${input.projectId}
|
||||
${datetimeFilter}
|
||||
${searchCondition}
|
||||
${filterCondition}
|
||||
`,
|
||||
|
||||
@@ -9,16 +9,19 @@ import { throwIfNoAccess } from "@/src/features/rbac/utils/checkAccess";
|
||||
import { type MembershipRole, Prisma, type Score } from "@prisma/client";
|
||||
import { paginationZod } from "@/src/utils/zod";
|
||||
import { singleFilter } from "@/src/server/api/interfaces/filters";
|
||||
import { filterToPrismaSql } from "@/src/features/filters/server/filterToPrisma";
|
||||
import { tableColumnsToSqlFilterAndPrefix } from "@/src/features/filters/server/filterToPrisma";
|
||||
import {
|
||||
type ScoreOptions,
|
||||
scoresTableCols,
|
||||
} from "@/src/server/api/definitions/scoresTable";
|
||||
import { orderBy } from "@/src/server/api/interfaces/orderBy";
|
||||
import { orderByToPrismaSql } from "@/src/features/orderBy/server/orderByToPrisma";
|
||||
import { auditLog } from "@/src/features/audit-logs/auditLog";
|
||||
|
||||
const ScoreFilterOptions = z.object({
|
||||
projectId: z.string(), // Required for protectedProjectProcedure
|
||||
filter: z.array(singleFilter),
|
||||
orderBy: orderBy,
|
||||
});
|
||||
|
||||
const ScoreAllOptions = ScoreFilterOptions.extend({
|
||||
@@ -29,8 +32,16 @@ export const scoresRouter = createTRPCRouter({
|
||||
all: protectedProjectProcedure
|
||||
.input(ScoreAllOptions)
|
||||
.query(async ({ input, ctx }) => {
|
||||
const filterCondition = filterToPrismaSql(input.filter, scoresTableCols);
|
||||
console.log("filters: ", filterCondition);
|
||||
const filterCondition = tableColumnsToSqlFilterAndPrefix(
|
||||
input.filter,
|
||||
scoresTableCols,
|
||||
"traces_scores",
|
||||
);
|
||||
|
||||
const orderByCondition = orderByToPrismaSql(
|
||||
input.orderBy,
|
||||
scoresTableCols,
|
||||
);
|
||||
|
||||
const scores = await ctx.prisma.$queryRaw<
|
||||
Array<Score & { traceName: string; totalCount: number }>
|
||||
@@ -49,7 +60,7 @@ export const scoresRouter = createTRPCRouter({
|
||||
JOIN traces t ON t.id = s.trace_id
|
||||
WHERE t.project_id = ${input.projectId}
|
||||
${filterCondition}
|
||||
ORDER BY s.timestamp DESC
|
||||
${orderByCondition}
|
||||
LIMIT ${input.limit}
|
||||
OFFSET ${input.page * input.limit}
|
||||
`);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { sessionsViewCols } from "@/src/server/api/definitions/sessionsView";
|
||||
import { filterToPrismaSql } from "@/src/features/filters/server/filterToPrisma";
|
||||
import { tableColumnsToSqlFilterAndPrefix } from "@/src/features/filters/server/filterToPrisma";
|
||||
import {
|
||||
createTRPCRouter,
|
||||
protectedProjectProcedure,
|
||||
@@ -28,10 +28,12 @@ export const sessionRouter = createTRPCRouter({
|
||||
.input(SessionFilterOptions)
|
||||
.query(async ({ input, ctx }) => {
|
||||
try {
|
||||
const filterCondition = filterToPrismaSql(
|
||||
const filterCondition = tableColumnsToSqlFilterAndPrefix(
|
||||
input.filter ?? [],
|
||||
sessionsViewCols,
|
||||
"sessions",
|
||||
);
|
||||
|
||||
const orderByCondition = orderByToPrismaSql(
|
||||
input.orderBy,
|
||||
sessionsViewCols,
|
||||
@@ -47,17 +49,19 @@ export const sessionRouter = createTRPCRouter({
|
||||
userIds: (string | null)[] | null;
|
||||
totalCount: number;
|
||||
sessionDuration: number | null;
|
||||
totalCost: number;
|
||||
}>
|
||||
>(Prisma.sql`
|
||||
WITH observation_metrics AS (
|
||||
SELECT
|
||||
t.session_id,
|
||||
EXTRACT(EPOCH FROM COALESCE(MAX(o."end_time"), MAX(o."start_time"), MAX(t.timestamp))) - EXTRACT(EPOCH FROM COALESCE(MIN(o."start_time"), MIN(t.timestamp)))::double precision AS "sessionDuration"
|
||||
EXTRACT(EPOCH FROM COALESCE(MAX(o."end_time"), MAX(o."start_time"), MAX(t.timestamp))) - EXTRACT(EPOCH FROM COALESCE(MIN(o."start_time"), MIN(t.timestamp)))::double precision AS "sessionDuration",
|
||||
SUM(COALESCE(o."calculated_total_cost", 0)) AS "totalCost"
|
||||
FROM traces t
|
||||
LEFT JOIN observations o ON o.trace_id = t.id
|
||||
LEFT JOIN observations_view o ON o.trace_id = t.id
|
||||
WHERE
|
||||
t."project_id" = ${input.projectId}
|
||||
AND session_id IS NOT NULL
|
||||
AND t.session_id IS NOT NULL
|
||||
GROUP BY 1
|
||||
),
|
||||
trace_metrics AS (
|
||||
@@ -68,7 +72,7 @@ export const sessionRouter = createTRPCRouter({
|
||||
FROM traces t
|
||||
WHERE
|
||||
t."project_id" = ${input.projectId}
|
||||
AND session_id IS NOT NULL
|
||||
AND t.session_id IS NOT NULL
|
||||
GROUP BY 1
|
||||
)
|
||||
|
||||
@@ -80,6 +84,7 @@ export const sessionRouter = createTRPCRouter({
|
||||
t."userIds",
|
||||
t."countTraces",
|
||||
o."sessionDuration",
|
||||
o."totalCost",
|
||||
(count(*) OVER ())::int AS "totalCount"
|
||||
FROM trace_sessions s
|
||||
LEFT JOIN trace_metrics t ON t.session_id = s.id
|
||||
@@ -130,8 +135,24 @@ export const sessionRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
|
||||
const totalCostQuery = Prisma.sql`
|
||||
SELECT
|
||||
SUM(COALESCE(o."calculated_total_cost", 0)) AS "totalCost"
|
||||
FROM observations_view o
|
||||
JOIN traces t ON t.id = o.trace_id
|
||||
WHERE
|
||||
t."session_id" = ${input.sessionId}
|
||||
AND t."project_id" = ${input.projectId}
|
||||
`;
|
||||
|
||||
const [costData] =
|
||||
await ctx.prisma.$queryRaw<Array<{ totalCost: number }>>(
|
||||
totalCostQuery,
|
||||
);
|
||||
|
||||
return {
|
||||
...session,
|
||||
totalCost: costData?.totalCost ?? 0,
|
||||
users: [
|
||||
...new Set(
|
||||
session.traces.map((t) => t.userId).filter((t) => t !== null),
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
} from "@/src/server/api/definitions/tracesTable";
|
||||
import {
|
||||
datetimeFilterToPrismaSql,
|
||||
filterToPrismaSql,
|
||||
tableColumnsToSqlFilterAndPrefix,
|
||||
} from "@/src/features/filters/server/filterToPrisma";
|
||||
import { throwIfNoAccess } from "@/src/features/rbac/utils/checkAccess";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
@@ -44,9 +44,10 @@ export const traceRouter = createTRPCRouter({
|
||||
all: protectedProjectProcedure
|
||||
.input(TraceFilterOptions)
|
||||
.query(async ({ input, ctx }) => {
|
||||
const filterCondition = filterToPrismaSql(
|
||||
const filterCondition = tableColumnsToSqlFilterAndPrefix(
|
||||
input.filter ?? [],
|
||||
tracesTableCols,
|
||||
"traces",
|
||||
);
|
||||
const orderByCondition = orderByToPrismaSql(
|
||||
input.orderBy,
|
||||
|
||||
@@ -32,6 +32,7 @@ export const userRouter = createTRPCRouter({
|
||||
lastObservation: Date | null;
|
||||
totalObservations: number;
|
||||
totalCount: number;
|
||||
sumCalculatedTotalCost: number;
|
||||
}[]
|
||||
>`
|
||||
SELECT
|
||||
@@ -45,9 +46,10 @@ export const userRouter = createTRPCRouter({
|
||||
MIN(o.start_time) "firstObservation",
|
||||
MAX(o.start_time) "lastObservation",
|
||||
COUNT(distinct o.id)::int "totalObservations",
|
||||
(count(*) OVER())::int AS "totalCount"
|
||||
(count(*) OVER())::int AS "totalCount",
|
||||
SUM(COALESCE(o.calculated_total_cost, 0)) AS "sumCalculatedTotalCost"
|
||||
FROM traces t
|
||||
LEFT JOIN observations o on o.trace_id = t.id
|
||||
LEFT JOIN observations_view o on o.trace_id = t.id
|
||||
WHERE t.user_id is not null
|
||||
AND t.project_id = ${input.projectId}
|
||||
AND o.project_id = ${input.projectId}
|
||||
@@ -56,7 +58,6 @@ export const userRouter = createTRPCRouter({
|
||||
LIMIT ${input.limit}
|
||||
OFFSET ${input.page * input.limit}
|
||||
`;
|
||||
|
||||
if (users.length === 0) {
|
||||
return [];
|
||||
}
|
||||
@@ -124,6 +125,7 @@ export const userRouter = createTRPCRouter({
|
||||
firstObservation: Date;
|
||||
lastObservation: Date;
|
||||
totalObservations: number;
|
||||
sumCalculatedTotalCost: number;
|
||||
}[]
|
||||
>`
|
||||
SELECT
|
||||
@@ -136,9 +138,10 @@ export const userRouter = createTRPCRouter({
|
||||
COALESCE(SUM(o.total_tokens),0)::int "totalTokens",
|
||||
MIN(o.start_time) "firstObservation",
|
||||
MAX(o.start_time) "lastObservation",
|
||||
COUNT(distinct o.id)::int "totalObservations"
|
||||
COUNT(distinct o.id)::int "totalObservations",
|
||||
SUM(COALESCE(o.calculated_total_cost, 0)) AS "sumCalculatedTotalCost"
|
||||
FROM traces t
|
||||
LEFT JOIN observations o on o.trace_id = t.id
|
||||
LEFT JOIN observations_view o on o.trace_id = t.id
|
||||
WHERE t.user_id is not null
|
||||
AND t.project_id = ${input.projectId}
|
||||
AND o.project_id = ${input.projectId}
|
||||
@@ -194,6 +197,7 @@ export const userRouter = createTRPCRouter({
|
||||
lastObservation: agg[0]?.lastObservation,
|
||||
totalObservations: agg[0]?.totalObservations ?? 0,
|
||||
lastScore: lastScoresOfUsers[0],
|
||||
sumCalculatedTotalCost: agg[0]?.sumCalculatedTotalCost ?? 0,
|
||||
};
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -469,6 +469,9 @@ export class TraceProcessor implements EventProcessor {
|
||||
},
|
||||
create: {
|
||||
id: internalId,
|
||||
timestamp: this.event.body.timestamp
|
||||
? new Date(this.event.body.timestamp)
|
||||
: undefined,
|
||||
name: body.name ?? undefined,
|
||||
userId: body.userId ?? undefined,
|
||||
input: body.input ?? undefined,
|
||||
@@ -483,6 +486,9 @@ export class TraceProcessor implements EventProcessor {
|
||||
},
|
||||
update: {
|
||||
name: body.name ?? undefined,
|
||||
timestamp: this.event.body.timestamp
|
||||
? new Date(this.event.body.timestamp)
|
||||
: undefined,
|
||||
userId: body.userId ?? undefined,
|
||||
input: body.input ?? undefined,
|
||||
output: body.output ?? undefined,
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
filterInterface,
|
||||
} from "./sqlInterface";
|
||||
import { tableDefinitions } from "./tableDefinitions";
|
||||
import { tableColumnsToSqlFilter } from "@/src/features/filters/server/filterToPrisma";
|
||||
|
||||
export type InternalDatabaseRow = {
|
||||
[key: string]: bigint | number | Decimal | string | Date;
|
||||
@@ -29,9 +30,7 @@ export const executeQuery = async (
|
||||
unsafeQuery: z.TypeOf<typeof sqlInterface>,
|
||||
) => {
|
||||
const query = sqlInterface.parse(unsafeQuery);
|
||||
|
||||
const sql = enrichAndCreateQuery(projectId, query);
|
||||
|
||||
const response = await prisma.$queryRaw<InternalDatabaseRow[]>(sql);
|
||||
|
||||
const parsedResult = outputParser(response);
|
||||
@@ -111,11 +110,7 @@ export const createQuery = (queryUnsafe: z.TypeOf<typeof sqlInterface>) => {
|
||||
query.filter && query.filter.length > 0
|
||||
? Prisma.sql` ${
|
||||
cte ? Prisma.sql` AND ` : Prisma.sql` WHERE `
|
||||
} ${prepareFilterString(
|
||||
query.from,
|
||||
query.filter,
|
||||
tableDefinitions[query.from]!.columns,
|
||||
)}`
|
||||
} ${tableColumnsToSqlFilter(query.filter, tableDefinitions[query.from]!.columns, query.from)}`
|
||||
: Prisma.empty;
|
||||
|
||||
const limitString = query.limit
|
||||
@@ -143,6 +138,11 @@ const createOutputColumnName = (
|
||||
capitalizeFirstLetter(columnDefinition.name),
|
||||
)}`;
|
||||
}
|
||||
if (safeAgg === "75thPercentile") {
|
||||
return Prisma.sql`percentile75${Prisma.raw(
|
||||
capitalizeFirstLetter(columnDefinition.name),
|
||||
)}`;
|
||||
}
|
||||
if (safeAgg === "90thPercentile") {
|
||||
return Prisma.sql`percentile90${Prisma.raw(
|
||||
capitalizeFirstLetter(columnDefinition.name),
|
||||
@@ -182,6 +182,10 @@ const createAggregatedColumn = (
|
||||
return Prisma.sql`percentile_disc(0.5) within group (order by ${getInternalSql(
|
||||
columnDefinition,
|
||||
)})`;
|
||||
case "75thPercentile":
|
||||
return Prisma.sql`percentile_disc(0.75) within group (order by ${getInternalSql(
|
||||
columnDefinition,
|
||||
)})`;
|
||||
case "90thPercentile":
|
||||
return Prisma.sql`percentile_disc(0.9) within group (order by ${getInternalSql(
|
||||
columnDefinition,
|
||||
@@ -211,7 +215,7 @@ const prepareOrderByString = (
|
||||
return Prisma.sql`${createAggregatedColumn(
|
||||
safeColumn,
|
||||
safeAgg,
|
||||
)} ${Prisma.raw(orderBy.direction)}`;
|
||||
)} ${Prisma.raw(orderBy.direction)} ${Prisma.raw(orderBy.direction === "DESC" ? "NULLS LAST" : "NULLS FIRST")}`;
|
||||
});
|
||||
const addedCte = hasCte
|
||||
? [Prisma.sql`date_series."date" ASC`, ...orderBys]
|
||||
@@ -222,36 +226,6 @@ const prepareOrderByString = (
|
||||
: Prisma.empty;
|
||||
};
|
||||
|
||||
const prepareFilterString = (
|
||||
table: z.infer<typeof sqlInterface>["from"],
|
||||
filter: z.infer<typeof filterInterface>,
|
||||
columnDefinitions: ColumnDefinition[],
|
||||
): Prisma.Sql => {
|
||||
const filters = filter.map((filter) => {
|
||||
const column = columnDefinitions.find((x) => x.name === filter.column);
|
||||
if (!column) {
|
||||
console.error(`Column ${filter.column} not found`);
|
||||
throw new Error(`Column ${filter.column} not found`);
|
||||
}
|
||||
// raw manfatory for column defs and operator
|
||||
// non raw for value, which will go into parameterised string
|
||||
if (filter.type === "datetime") {
|
||||
return Prisma.sql`${getInternalSql(column)} ${Prisma.raw(
|
||||
filter.operator,
|
||||
)} ${filter.value}::timestamp with time zone at time zone 'UTC'`;
|
||||
} else {
|
||||
return Prisma.sql`${getInternalSql(column)} ${Prisma.raw(
|
||||
filter.operator,
|
||||
)} ${filter.value} ${
|
||||
column.name === "type" && table === "observations"
|
||||
? Prisma.sql`::"ObservationType"`
|
||||
: Prisma.empty
|
||||
}`;
|
||||
}
|
||||
});
|
||||
return Prisma.join(filters, " AND ");
|
||||
};
|
||||
|
||||
const prepareGroupBy = (
|
||||
table: z.infer<typeof sqlInterface>["from"],
|
||||
groupBy: z.infer<typeof groupByInterface>[number],
|
||||
|
||||
@@ -18,6 +18,7 @@ export const aggregations = z
|
||||
"MAX",
|
||||
"MIN",
|
||||
"50thPercentile",
|
||||
"75thPercentile",
|
||||
"90thPercentile",
|
||||
"95thPercentile",
|
||||
"99thPercentile",
|
||||
|
||||
@@ -121,6 +121,7 @@ export const tableDefinitions: TableDefinitions = {
|
||||
traceTimestamp,
|
||||
traceUser,
|
||||
startTime,
|
||||
traceName,
|
||||
],
|
||||
},
|
||||
observations: {
|
||||
@@ -161,6 +162,7 @@ export const tableDefinitions: TableDefinitions = {
|
||||
scoreName,
|
||||
traceUser,
|
||||
tracesProjectId,
|
||||
traceName,
|
||||
],
|
||||
},
|
||||
traces_parent_observation_scores: {
|
||||
|
||||
Reference in New Issue
Block a user