Compare commits

...
67 Commits
Author SHA1 Message Date
Max Deichmann ba197523d2 chore: release v1.7.0
CI/CD / push-docker-image (push) Blocked by required conditions
CI/CD / lint (push) Successful in 1m2s
CI/CD / tests (push) Failing after 36s
CI/CD / e2e-tests (push) Failing after 37s
CI/CD / all-ci-passed (push) Waiting to run
2023-11-25 13:43:38 +01:00
Max DeichmannandGitHub d8902d0d96 feature(api): add projects api (#569) 2023-11-25 12:44:16 +01:00
Marc Klingen 784860fd9d chore: release v1.6.0
CI/CD / push-docker-image (push) Blocked by required conditions
CI/CD / lint (push) Successful in 1m0s
CI/CD / tests (push) Failing after 51s
CI/CD / e2e-tests (push) Failing after 34s
CI/CD / all-ci-passed (push) Waiting to run
2023-11-24 02:02:33 +01:00
Marc KlingenandGitHub 9552618f33 feat(telemetry): include version number (#567) 2023-11-24 01:00:00 +00:00
Max DeichmannandGitHub f27c8a0629 fix(core): for failed events return correct errors on ingestion (#566) 2023-11-23 23:51:49 +01:00
Marc KlingenandGitHub e43e89be67 feat(ui): add comment on signup re demo account (#565) 2023-11-23 21:46:31 +00:00
Marc KlingenandGitHub 578f55f444 feat(auth): show SSO options on /auth/sign-up (#564)
* ui

* feat(auth): SSO on signup
2023-11-23 21:06:45 +00:00
Marc KlingenandGitHub 033dd6654b feat(telemetry): count users (#563) 2023-11-23 19:30:26 +00:00
Marc KlingenandGitHub 341dc1cb6a feat(UI): "does not contain" filter on string fields (#562) 2023-11-23 19:13:18 +00:00
Marc KlingenandGitHub 160b4d528c feat(cloud): add data region switch (#558)
* refactor: CloudPrivacyNotice in feature folder

* feat(cloud): add data region switch

* feat(UI): display hostname on API key form
2023-11-23 19:06:17 +00:00
Marc KlingenandGitHub 0081e73721 chore(deps): upgrade trpc and posthog (#560) 2023-11-23 19:03:01 +00:00
Marc Klingen 79eb4ed6fe chore: do not upgrade trpc via dependabot
Dependabot fails to upgrade all trpc packages simultaneously
2023-11-23 19:57:12 +01:00
Marc Klingen 3d50a9ee32 chore: release v1.5.1
CI/CD / push-docker-image (push) Blocked by required conditions
CI/CD / tests (push) Failing after 38s
CI/CD / lint (push) Successful in 1m18s
CI/CD / e2e-tests (push) Failing after 40s
CI/CD / all-ci-passed (push) Waiting to run
2023-11-23 17:14:09 +01:00
Marc KlingenandGitHub 5e5c853df1 fix(ui): fix usage by user chart on dashboard (#557)
* fix(ui): fix usage by user chart on dashboard

* fix lint
2023-11-23 14:41:54 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
c47bf6436d chore(deps-dev): bump the patches group with 2 updates (#551)
Bumps the patches group with 2 updates: [@types/jest](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/jest) and [@types/react-dom](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react-dom).


Updates `@types/jest` from 29.5.9 to 29.5.10
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/jest)

Updates `@types/react-dom` from 18.2.16 to 18.2.17
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react-dom)

---
updated-dependencies:
- dependency-name: "@types/jest"
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: patches
- dependency-name: "@types/react-dom"
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: patches
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-11-22 22:56:54 +00:00
Marc KlingenandGitHub 86cfe5b940 chore: terms/privacy on sign-in for cloud (#549) 2023-11-22 19:04:02 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
81723667cb chore(deps): bump the patches group with 7 updates (#547)
Bumps the patches group with 7 updates:

| Package | From | To |
| --- | --- | --- |
| [@sentry/nextjs](https://github.com/getsentry/sentry-javascript) | `7.81.0` | `7.81.1` |
| [posthog-js](https://github.com/PostHog/posthog-js) | `1.92.0` | `1.92.1` |
| [@types/cors](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/cors) | `2.8.16` | `2.8.17` |
| [@types/jest](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/jest) | `29.5.8` | `29.5.9` |
| [@types/lodash](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/lodash) | `4.14.201` | `4.14.202` |
| [@types/react](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react) | `18.2.37` | `18.2.38` |
| [@types/react-dom](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react-dom) | `18.2.15` | `18.2.16` |


Updates `@sentry/nextjs` from 7.81.0 to 7.81.1
- [Release notes](https://github.com/getsentry/sentry-javascript/releases)
- [Changelog](https://github.com/getsentry/sentry-javascript/blob/develop/CHANGELOG.md)
- [Commits](https://github.com/getsentry/sentry-javascript/compare/7.81.0...7.81.1)

Updates `posthog-js` from 1.92.0 to 1.92.1
- [Release notes](https://github.com/PostHog/posthog-js/releases)
- [Changelog](https://github.com/PostHog/posthog-js/blob/master/CHANGELOG.md)
- [Commits](https://github.com/PostHog/posthog-js/compare/v1.92.0...v1.92.1)

Updates `@types/cors` from 2.8.16 to 2.8.17
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/cors)

Updates `@types/jest` from 29.5.8 to 29.5.9
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/jest)

Updates `@types/lodash` from 4.14.201 to 4.14.202
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/lodash)

Updates `@types/react` from 18.2.37 to 18.2.38
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react)

Updates `@types/react-dom` from 18.2.15 to 18.2.16
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react-dom)

---
updated-dependencies:
- dependency-name: "@sentry/nextjs"
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: patches
- dependency-name: posthog-js
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: patches
- dependency-name: "@types/cors"
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: patches
- dependency-name: "@types/jest"
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: patches
- dependency-name: "@types/lodash"
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: patches
- dependency-name: "@types/react"
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: patches
- dependency-name: "@types/react-dom"
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: patches
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-11-21 22:39:48 +00:00
Max DeichmannandGitHub 73f7dd26bd fix(fern): fix API error types in fern definitions (#546) 2023-11-21 22:32:46 +00:00
Marc KlingenandGitHub cb6c834253 chore: trpc useUtils instead of useContext (deprecated) (#544) 2023-11-21 22:19:04 +00:00
Max DeichmannandGitHub 85f7616707 fix: filter signup body for sentry (#543) 2023-11-21 21:42:00 +00:00
Marc Klingen 7bc5719f94 docs: add comment to /api/auth/signup 2023-11-21 14:54:32 +01:00
Marc Klingen dfbbc1d297 chore: release v1.5.0
CI/CD / push-docker-image (push) Blocked by required conditions
CI/CD / lint (push) Successful in 59s
CI/CD / tests (push) Failing after 41s
CI/CD / e2e-tests (push) Failing after 35s
CI/CD / all-ci-passed (push) Waiting to run
2023-11-21 14:41:13 +01:00
3c70c6e40b feat(auth): add env AUTH_DISABLE_USERNAME_PASSWORD (#510)
---------

Co-authored-by: Marc Klingen <git@marcklingen.com>
2023-11-21 12:55:06 +00:00
Marc Klingen ff9500c133 chore: release v1.4.1
CI/CD / push-docker-image (push) Blocked by required conditions
CI/CD / tests (push) Failing after 36s
CI/CD / e2e-tests (push) Failing after 41s
CI/CD / lint (push) Successful in 58s
CI/CD / all-ci-passed (push) Waiting to run
2023-11-21 13:26:55 +01:00
Marc KlingenandGitHub 5e72daf3dc perf: get project list from session in layout.tsx (#542) 2023-11-21 12:16:37 +00:00
Marc KlingenandGitHub 5c5cca0d9e fix: redirect to project on user sign in (#541) 2023-11-21 12:45:59 +01:00
Marc KlingenandGitHub 840cad55be fix: redirect to project on sign in (#540) 2023-11-21 11:26:42 +01:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
523b6f5c46 chore(deps-dev): bump typescript from 5.2.2 to 5.3.2 (#535)
Bumps [typescript](https://github.com/Microsoft/TypeScript) from 5.2.2 to 5.3.2.
- [Release notes](https://github.com/Microsoft/TypeScript/releases)
- [Commits](https://github.com/Microsoft/TypeScript/compare/v5.2.2...v5.3.2)

---
updated-dependencies:
- dependency-name: typescript
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-11-21 01:12:51 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
46ff1b60eb chore(deps): bump posthog-js from 1.91.1 to 1.92.0 (#536)
Bumps [posthog-js](https://github.com/PostHog/posthog-js) from 1.91.1 to 1.92.0.
- [Release notes](https://github.com/PostHog/posthog-js/releases)
- [Changelog](https://github.com/PostHog/posthog-js/blob/master/CHANGELOG.md)
- [Commits](https://github.com/PostHog/posthog-js/compare/v1.91.1...v1.92.0)

---
updated-dependencies:
- dependency-name: posthog-js
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-11-21 01:07:46 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
935508f355 chore(deps-dev): bump @typescript-eslint/parser from 6.11.0 to 6.12.0 (#537)
Bumps [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) from 6.11.0 to 6.12.0.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/parser/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v6.12.0/packages/parser)

---
updated-dependencies:
- dependency-name: "@typescript-eslint/parser"
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-11-21 01:04:09 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
7c1122fc10 chore(deps): bump @sentry/nextjs from 7.80.1 to 7.81.0 (#538)
Bumps [@sentry/nextjs](https://github.com/getsentry/sentry-javascript) from 7.80.1 to 7.81.0.
- [Release notes](https://github.com/getsentry/sentry-javascript/releases)
- [Changelog](https://github.com/getsentry/sentry-javascript/blob/develop/CHANGELOG.md)
- [Commits](https://github.com/getsentry/sentry-javascript/compare/7.80.1...7.81.0)

---
updated-dependencies:
- dependency-name: "@sentry/nextjs"
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-11-21 01:00:23 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
bf3f82b76e chore(deps-dev): bump @typescript-eslint/eslint-plugin from 6.11.0 to 6.12.0 (#539)
chore(deps-dev): bump @typescript-eslint/eslint-plugin

Bumps [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) from 6.11.0 to 6.12.0.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v6.12.0/packages/eslint-plugin)

---
updated-dependencies:
- dependency-name: "@typescript-eslint/eslint-plugin"
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-11-21 01:56:24 +01:00
Marc Klingen 41e7d9d419 chore: release v1.4.0
CI/CD / push-docker-image (push) Blocked by required conditions
CI/CD / tests (push) Failing after 38s
CI/CD / lint (push) Successful in 54s
CI/CD / e2e-tests (push) Failing after 40s
CI/CD / all-ci-passed (push) Waiting to run
2023-11-20 22:04:53 +01:00
6088025a81 fix(telemetry): initial run & safeguard against exceptions (#534)
---------

Co-authored-by: Max Deichmann <m.deichmann@tum.de>
2023-11-20 22:04:00 +01:00
Marc Klingen fdec832934 chore(deps): pin @types/node version 2023-11-20 15:35:52 +01:00
a3a52681c8 feat(rbac): owners can transfer project to new owner (#473)
---------

Co-authored-by: Max Deichmann <m.deichmann@tum.de>
Co-authored-by: Marc Klingen <git@marcklingen.com>
2023-11-20 01:18:49 +01:00
Marc KlingenandGitHub 4dc1245437 feat(rbac): restrict project:delete to owner 2023-11-20 01:03:48 +01:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
ddb30f0bb8 chore(deps-dev): bump eslint from 8.53.0 to 8.54.0 (#523)
Bumps [eslint](https://github.com/eslint/eslint) from 8.53.0 to 8.54.0.
- [Release notes](https://github.com/eslint/eslint/releases)
- [Changelog](https://github.com/eslint/eslint/blob/main/CHANGELOG.md)
- [Commits](https://github.com/eslint/eslint/compare/v8.53.0...v8.54.0)

---
updated-dependencies:
- dependency-name: eslint
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-11-19 23:32:12 +00:00
Marc Klingen 067e89cf47 chore(deps): upgrade trpc 2023-11-20 00:27:50 +01:00
Marc KlingenandGitHub 34b4909640 ci: dependabot ignore @types/node
Should not be upgraded continuously but only when node version of the project changes
2023-11-20 00:25:44 +01:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2df2b8aa4b chore(deps): bump the trpc group with 2 updates (#531)
Bumps the trpc group with 2 updates: [@trpc/client](https://github.com/trpc/trpc/tree/HEAD/packages/client) and [@trpc/next](https://github.com/trpc/trpc/tree/HEAD/packages/next).


Updates `@trpc/client` from 10.43.5 to 10.43.6
- [Release notes](https://github.com/trpc/trpc/releases)
- [Commits](https://github.com/trpc/trpc/commits/v10.43.6/packages/client)

Updates `@trpc/next` from 10.43.5 to 10.43.6
- [Release notes](https://github.com/trpc/trpc/releases)
- [Commits](https://github.com/trpc/trpc/commits/v10.43.6/packages/next)

---
updated-dependencies:
- dependency-name: "@trpc/client"
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: trpc
- dependency-name: "@trpc/next"
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: trpc
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-11-19 23:22:25 +00:00
Marc KlingenandGitHub b40dec2cb6 ci: improve patterns of dependabot groups 2023-11-20 00:18:30 +01:00
Marc KlingenandGitHub ba2d9ee8c2 fix: hide delete role button for own role (#530) 2023-11-19 23:07:33 +00:00
Marc Klingen dc35096db7 chore: release v1.3.0
CI/CD / push-docker-image (push) Blocked by required conditions
CI/CD / lint (push) Successful in 1m19s
CI/CD / tests (push) Failing after 1m4s
CI/CD / e2e-tests (push) Failing after 1m12s
CI/CD / all-ci-passed (push) Waiting to run
2023-11-19 23:54:30 +01:00
Marc KlingenandGitHub 509c838608 feat(docker): remove cron process used for telemetry (#528)
The separate cron process that called the telemetry endpoint of the application ({host}/api/cron/telemetry) caused issues in certain environments. This process needed to scan the network interfaces of the container instance to determine its own IP. This change removes the need for a separate process.
2023-11-19 23:46:16 +01:00
Marc Klingen 75f2ae5f10 fix(api): health returns 503 instead of 500
More specific http status code (Service Unavailable)
2023-11-19 17:29:43 +01:00
Marc KlingenandGitHub 19778b2b94 docs(openapi): add api/public/health (#527) 2023-11-19 17:17:11 +01:00
be6cdd580f chore: add Github PR template (#524)
---------

Co-authored-by: Marc Klingen <git@marcklingen.com>
2023-11-19 16:46:30 +01:00
5b4523bd71 feat: delete single traces (admin and owner only) (#514)
* feat: delete-single-trace

* improvements: async, access checks, UI

* fix: delete observations as they do not cascade

---------

Co-authored-by: Marc Klingen <git@marcklingen.com>
2023-11-19 16:32:13 +01:00
c54b829250 feat(ui): display project name in header (#520)
Most of the app is on project-level, reduce confusions when having multiple projects by adding the project name to the header.

---------

Co-authored-by: Marc Klingen <git@marcklingen.com>
2023-11-19 14:33:41 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
484b7e06ca chore(deps): bump the patches group with 2 updates (#521)
Bumps the patches group with 2 updates: [@trpc/client](https://github.com/trpc/trpc/tree/HEAD/packages/client) and [@trpc/next](https://github.com/trpc/trpc/tree/HEAD/packages/next).


Updates `@trpc/client` from 10.43.3 to 10.43.5
- [Release notes](https://github.com/trpc/trpc/releases)
- [Commits](https://github.com/trpc/trpc/commits/v10.43.5/packages/client)

Updates `@trpc/next` from 10.43.3 to 10.43.5
- [Release notes](https://github.com/trpc/trpc/releases)
- [Commits](https://github.com/trpc/trpc/commits/v10.43.5/packages/next)

---
updated-dependencies:
- dependency-name: "@trpc/client"
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: patches
- dependency-name: "@trpc/next"
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: patches
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-11-17 22:32:58 +00:00
Marc KlingenandGitHub af3e645580 chore: upgrade dependencies (#516) 2023-11-17 04:01:53 +01:00
Marc Klingen cfde86f057 chore: release v1.2.0
CI/CD / push-docker-image (push) Blocked by required conditions
CI/CD / lint (push) Successful in 1m18s
CI/CD / tests (push) Failing after 31s
CI/CD / e2e-tests (push) Failing after 38s
CI/CD / all-ci-passed (push) Waiting to run
2023-11-16 23:49:21 +01:00
Marc KlingenandGitHub 8f02925844 feat(ui): add more string filters (#511)
Types: contains, starts with, ends with
2023-11-16 23:47:45 +01:00
Marc Klingen d20dff9ad9 refactor: .env.prod.example instead of cloud 2023-11-16 19:07:51 +01:00
Robert SturlaandGitHub 1b376bdff8 fix: update example environment variable AUTH_DOMAINS_WITH_SSO_ENFORCEMENT (#508)
Update example environment variable AUTH_DOMAINS_WITH_OAUTH_ENFORCEMENT to AUTH_DOMAINS_WITH_SSO_ENFORCEMENT
2023-11-16 19:05:20 +01:00
Marc KlingenandGitHub b42239095d docs: add current release to README.md 2023-11-16 02:37:10 +01:00
Marc Klingen 13e7fba4a4 chore: release v1.1.0
CI/CD / push-docker-image (push) Blocked by required conditions
CI/CD / lint (push) Successful in 1m19s
CI/CD / tests (push) Failing after 1m14s
CI/CD / e2e-tests (push) Failing after 35s
CI/CD / all-ci-passed (push) Waiting to run
2023-11-16 02:28:17 +01:00
Marc KlingenandGitHub e6d54458a8 feat(api): upsert scores (#500) 2023-11-16 02:26:16 +01:00
Marc KlingenandGitHub 802476e851 feat: trim user email in sharing dialog (#499) 2023-11-16 00:45:30 +00:00
Marc Klingen dc16c5b99d chore: release v1.0.1
CI/CD / push-docker-image (push) Blocked by required conditions
CI/CD / tests (push) Failing after 1m6s
CI/CD / lint (push) Successful in 1m25s
CI/CD / e2e-tests (push) Failing after 1m4s
CI/CD / all-ci-passed (push) Waiting to run
2023-11-15 22:29:33 +01:00
Marc Klingen 7f23d3c3e8 fix: add tag prefix to release-it config 2023-11-15 22:06:02 +01:00
Marc Klingen 4dfb5ab7d0 chore: release v1.0.0 2023-11-15 21:55:09 +01:00
Marc Klingen 6ace2e78f1 chore: change release-it config 2023-11-15 21:11:23 +01:00
Marc Klingen 338049a09d Release 1.0.0-rc.2 2023-11-15 21:07:12 +01:00
Marc KlingenandGitHub cc7b293b92 chore: add release-it (#497) 2023-11-15 21:04:25 +01:00
Marc KlingenandGitHub 48b59d96fb ci: tag docker images with semver (#496) 2023-11-15 20:53:24 +01:00
73 changed files with 6130 additions and 1964 deletions
+2 -1
View File
@@ -42,4 +42,5 @@ AUTH_GOOGLE_CLIENT_ID=
AUTH_GOOGLE_CLIENT_SECRET=
AUTH_GITHUB_CLIENT_ID=
AUTH_GITHUB_CLIENT_SECRET=
# AUTH_DOMAINS_WITH_OAUTH_ENFORCEMENT=domain1.com,domain2.com
# AUTH_DOMAINS_WITH_SSO_ENFORCEMENT=domain1.com,domain2.com
# AUTH_DISABLE_USERNAME_PASSWORD=true
+36
View File
@@ -0,0 +1,36 @@
## What does this PR do?
<!-- Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change. -->
Fixes # (issue)
<!-- Please provide a loom video for visual changes to speed up reviews
Loom Video: https://www.loom.com/
-->
## Type of change
<!-- Please delete bullets that are not relevant. -->
- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] Chore (refactoring code, technical debt, workflow improvements)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
- [ ] Refactor (does not change functionality, e.g. code style improvements, linting)
- [ ] This change requires a documentation update
## Mandatory Tasks
- [ ] Make sure you have self-reviewed the code. A decent size PR without self-review might be rejected.
## Checklist
<!-- Remove bullet points below that don't apply to you -->
- I haven't read the [contributing guide](https://github.com/calcom/cal.com/blob/main/CONTRIBUTING.md)
- My code doesn't follow the style guidelines of this project (`npm run prettier`)
- I haven't commented my code, particularly in hard-to-understand areas
- I haven't checked if my PR needs changes to the documentation
- I haven't checked if my changes generate no new warnings (`npm run lint`)
- I haven't added tests that prove my fix is effective or that my feature works
- I haven't checked if new and existing unit tests pass locally with my changes
+5 -5
View File
@@ -14,14 +14,14 @@ updates:
prefix: chore
prefix-development: chore
include: scope
ignore:
- dependency-name: "@types/node"
- dependency-name: "@trpc/*"
groups:
trpc:
patterns:
- ^@trpc/.*
prisma:
patterns:
- ^prisma
- ^@prisma/.*
- "prisma"
- "@prisma/*"
next:
patterns:
- "eslint-config-next"
+5 -2
View File
@@ -5,6 +5,8 @@ on:
push:
branches:
- "main"
tags:
- "v*"
pull_request:
branches:
- "main"
@@ -104,7 +106,7 @@ jobs:
push-docker-image:
needs: all-ci-passed
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/'))
runs-on: ubuntu-latest
env:
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/postgres
@@ -143,7 +145,8 @@ jobs:
type=ref,event=branch
type=ref,event=pr
type=sha
type=raw,value=latest
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
- name: Build and push Docker image
uses: docker/build-push-action@v4
+1 -1
View File
@@ -1 +1 @@
v18.16.0
v18.18
-2
View File
@@ -70,7 +70,6 @@ ENV NEXT_TELEMETRY_DISABLED 1
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
RUN npm install --no-package-lock --no-save cron
RUN npm install -g --no-package-lock --no-save prisma
COPY --from=builder /app/public ./public
@@ -81,7 +80,6 @@ COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
COPY --from=builder --chown=nextjs:nodejs /app/prisma ./prisma
COPY --chown=nextjs:nodejs cron.js ./cron.js
COPY --chown=nextjs:nodejs entrypoint.sh ./entrypoint.sh
RUN chmod +x ./entrypoint.sh
+2 -1
View File
@@ -35,6 +35,7 @@
<img src="https://img.shields.io/badge/License-MIT-red.svg?style=flat-square" alt="MIT License">
<a href="https://discord.gg/7NXusRtqYU"><img src="https://img.shields.io/discord/1111061815649124414?style=flat-square&logo=Discord&logoColor=white&label=Discord&color=%23434EE4" alt="Discord"></a>
<a href="https://github.com/langfuse/langfuse"><img src="https://img.shields.io/github/stars/langfuse/langfuse?style=flat-square&logo=GitHub&label=langfuse%2Flangfuse" alt="Github Repo Stars"></a>
<a href="https://github.com/langfuse/langfuse/releases"><img src="https://img.shields.io/github/v/release/langfuse/langfuse?include_prereleases&style=flat-square" alt="langfuse releases"></a>
<a href="https://github.com/langfuse/langfuse/actions/workflows/pipeline.yml?query=branch:main"><img src="https://img.shields.io/github/actions/workflow/status/langfuse/langfuse/pipeline.yml?style=flat-square&label=All%20tests" alt="CI test status"></a>
<a href="https://status.langfuse.com"><img src="https://uptime.betterstack.com/status-badges/v1/monitor/udlc.svg" alt="Uptime Status"/></a>
<a href="https://www.ycombinator.com/companies/langfuse"><img src="https://img.shields.io/badge/Y%20Combinator-W23-orange?style=flat-square" alt="Y Combinator W23"></a>
@@ -279,6 +280,6 @@ This helps us to:
1. Understand how Langfuse is used and improve the most relevant features.
2. Track overall usage for internal and external (e.g. fundraising) reporting.
None of the data is shared with third parties and does not include any sensitive information. We want to be super transparent about this and you can find the exact data we collect [here](/src/pages/api/cron/telemetry.ts).
None of the data is shared with third parties and does not include any sensitive information. We want to be super transparent about this and you can find the exact data we collect [here](/src/features/telemetry/index.ts).
You can opt-out by setting `TELEMETRY_ENABLED=false`.
-40
View File
@@ -1,40 +0,0 @@
var CronJob = require("cron").CronJob;
var os = require("os");
if (
process.env.TELEMETRY_ENABLED === undefined ||
process.env.TELEMETRY_ENABLED === "true"
) {
try {
// Get internal ip address to ping the telemetry endpoint
var networkInterfaces = os.networkInterfaces();
var ip = undefined;
if ("eth0" in networkInterfaces) {
var ipaddresses = networkInterfaces["eth0"]
.filter((networkInterface) => networkInterface.family === "IPv4")
.map((networkInterface) => networkInterface.address);
if (ipaddresses.length > 0) {
ip = ipaddresses[0];
}
}
// Schedule cron job
if (ip) {
var job = new CronJob("*/5 * * * *", function () {
// PORT set to 3000 by Dockerfile, can be overridden by env
fetch(
`http://${ip}:${process.env.PORT ?? 3000}/api/cron/telemetry`,
).catch((err) => console.log(err));
});
job.start();
} else {
console.log("CRON: No ip address found, not starting cron job");
}
} catch (err) {
console.log("CRON: Error", err);
}
} else {
console.log("CRON: Telemetry disabled via env");
}
-3
View File
@@ -14,8 +14,5 @@ if [ $status -ne 0 ]; then
exit $status
fi
# Start background cron job
node cron.js &
# Start server
node server.js
+1 -1
View File
@@ -5,7 +5,7 @@ service:
base-path: /api/public
endpoints:
create:
docs: Add a score to the database
docs: Add a score to the database, upserts on id
method: POST
path: /scores
request: CreateScoreRequest
+5 -5
View File
@@ -146,16 +146,16 @@ types:
errors:
Error:
status-code: 400
type: string
type: unknown
UnauthorizedError:
status-code: 401
type: string
type: unknown
AccessDeniedError:
status-code: 403
type: string
type: unknown
NotFoundError:
status-code: 404
type: string
type: unknown
MethodNotAllowedError:
status-code: 405
type: string
type: unknown
+15
View File
@@ -0,0 +1,15 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/fern-api/fern/main/fern.schema.json
service:
auth: false
base-path: /api/public
endpoints:
health:
docs: Check health of API and database
method: GET
path: /health
errors:
- ServiceUnavailableError
errors:
ServiceUnavailableError:
status-code: 503
+22
View File
@@ -0,0 +1,22 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/fern-api/fern/main/fern.schema.json
imports:
commons: ./commons.yml
pagination: ./utils/pagination.yml
service:
auth: true
base-path: /api/public
endpoints:
get:
method: GET
path: /projects
response: Projects
types:
Projects:
properties:
data: list<Project>
Project:
properties:
id: string
name: string
+1 -1
View File
@@ -7,7 +7,7 @@ service:
base-path: /api/public
endpoints:
create:
docs: Add a score to the database
docs: Add a score to the database, upserts on id
method: POST
path: /scores
request: CreateScoreRequest
+1 -1
View File
@@ -11,7 +11,7 @@ groups:
namespaceExport: Langfuse
allowCustomFetcher: true
- name: fernapi/fern-python-sdk
version: 0.3.7
version: 0.6.6
output:
location: local-file-system
path: ../../../generated/python
+1 -1
View File
@@ -1,4 +1,4 @@
{
"organization": "finto",
"version": "0.15.0-rc68"
"version": "0.15.3"
}
+1 -1
View File
@@ -5,7 +5,7 @@ info:
paths:
/api/public/scores:
post:
description: Add a score to the database
description: Add a score to the database, upserts on id
operationId: score_create
tags:
- Score
+187 -180
View File
@@ -21,32 +21,27 @@ paths:
description: ''
content:
application/json:
schema:
type: string
schema: {}
'401':
description: ''
content:
application/json:
schema:
type: string
schema: {}
'403':
description: ''
content:
application/json:
schema:
type: string
schema: {}
'404':
description: ''
content:
application/json:
schema:
type: string
schema: {}
'405':
description: ''
content:
application/json:
schema:
type: string
schema: {}
security: &ref_0
- BasicAuth: []
requestBody:
@@ -73,32 +68,27 @@ paths:
description: ''
content:
application/json:
schema:
type: string
schema: {}
'401':
description: ''
content:
application/json:
schema:
type: string
schema: {}
'403':
description: ''
content:
application/json:
schema:
type: string
schema: {}
'404':
description: ''
content:
application/json:
schema:
type: string
schema: {}
'405':
description: ''
content:
application/json:
schema:
type: string
schema: {}
security: *ref_0
requestBody:
required: true
@@ -129,32 +119,27 @@ paths:
description: ''
content:
application/json:
schema:
type: string
schema: {}
'401':
description: ''
content:
application/json:
schema:
type: string
schema: {}
'403':
description: ''
content:
application/json:
schema:
type: string
schema: {}
'404':
description: ''
content:
application/json:
schema:
type: string
schema: {}
'405':
description: ''
content:
application/json:
schema:
type: string
schema: {}
security: *ref_0
/api/public/datasets:
post:
@@ -174,32 +159,27 @@ paths:
description: ''
content:
application/json:
schema:
type: string
schema: {}
'401':
description: ''
content:
application/json:
schema:
type: string
schema: {}
'403':
description: ''
content:
application/json:
schema:
type: string
schema: {}
'404':
description: ''
content:
application/json:
schema:
type: string
schema: {}
'405':
description: ''
content:
application/json:
schema:
type: string
schema: {}
security: *ref_0
requestBody:
required: true
@@ -235,32 +215,27 @@ paths:
description: ''
content:
application/json:
schema:
type: string
schema: {}
'401':
description: ''
content:
application/json:
schema:
type: string
schema: {}
'403':
description: ''
content:
application/json:
schema:
type: string
schema: {}
'404':
description: ''
content:
application/json:
schema:
type: string
schema: {}
'405':
description: ''
content:
application/json:
schema:
type: string
schema: {}
security: *ref_0
/api/public/events:
post:
@@ -280,32 +255,27 @@ paths:
description: ''
content:
application/json:
schema:
type: string
schema: {}
'401':
description: ''
content:
application/json:
schema:
type: string
schema: {}
'403':
description: ''
content:
application/json:
schema:
type: string
schema: {}
'404':
description: ''
content:
application/json:
schema:
type: string
schema: {}
'405':
description: ''
content:
application/json:
schema:
type: string
schema: {}
security: *ref_0
requestBody:
required: true
@@ -330,32 +300,27 @@ paths:
description: ''
content:
application/json:
schema:
type: string
schema: {}
'401':
description: ''
content:
application/json:
schema:
type: string
schema: {}
'403':
description: ''
content:
application/json:
schema:
type: string
schema: {}
'404':
description: ''
content:
application/json:
schema:
type: string
schema: {}
'405':
description: ''
content:
application/json:
schema:
type: string
schema: {}
security: *ref_0
requestBody:
required: true
@@ -379,32 +344,27 @@ paths:
description: ''
content:
application/json:
schema:
type: string
schema: {}
'401':
description: ''
content:
application/json:
schema:
type: string
schema: {}
'403':
description: ''
content:
application/json:
schema:
type: string
schema: {}
'404':
description: ''
content:
application/json:
schema:
type: string
schema: {}
'405':
description: ''
content:
application/json:
schema:
type: string
schema: {}
security: *ref_0
requestBody:
required: true
@@ -412,6 +372,43 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/UpdateGenerationRequest'
/api/public/health:
get:
description: Check health of API and database
operationId: health_health
tags:
- Health
parameters: []
responses:
'204':
description: ''
'400':
description: ''
content:
application/json:
schema: {}
'401':
description: ''
content:
application/json:
schema: {}
'403':
description: ''
content:
application/json:
schema: {}
'404':
description: ''
content:
application/json:
schema: {}
'405':
description: ''
content:
application/json:
schema: {}
'503':
description: ''
/api/public/ingestion:
post:
description: Ingest multiple events to Langfuse
@@ -430,32 +427,27 @@ paths:
description: ''
content:
application/json:
schema:
type: string
schema: {}
'401':
description: ''
content:
application/json:
schema:
type: string
schema: {}
'403':
description: ''
content:
application/json:
schema:
type: string
schema: {}
'404':
description: ''
content:
application/json:
schema:
type: string
schema: {}
'405':
description: ''
content:
application/json:
schema:
type: string
schema: {}
security: *ref_0
requestBody:
required: true
@@ -496,32 +488,27 @@ paths:
description: ''
content:
application/json:
schema:
type: string
schema: {}
'401':
description: ''
content:
application/json:
schema:
type: string
schema: {}
'403':
description: ''
content:
application/json:
schema:
type: string
schema: {}
'404':
description: ''
content:
application/json:
schema:
type: string
schema: {}
'405':
description: ''
content:
application/json:
schema:
type: string
schema: {}
security: *ref_0
/api/public/observations:
get:
@@ -583,36 +570,70 @@ paths:
description: ''
content:
application/json:
schema:
type: string
schema: {}
'401':
description: ''
content:
application/json:
schema:
type: string
schema: {}
'403':
description: ''
content:
application/json:
schema:
type: string
schema: {}
'404':
description: ''
content:
application/json:
schema:
type: string
schema: {}
'405':
description: ''
content:
application/json:
schema: {}
security: *ref_0
/api/public/projects:
get:
operationId: projects_get
tags:
- Projects
parameters: []
responses:
'200':
description: ''
content:
application/json:
schema:
type: string
$ref: '#/components/schemas/Projects'
'400':
description: ''
content:
application/json:
schema: {}
'401':
description: ''
content:
application/json:
schema: {}
'403':
description: ''
content:
application/json:
schema: {}
'404':
description: ''
content:
application/json:
schema: {}
'405':
description: ''
content:
application/json:
schema: {}
security: *ref_0
/api/public/scores:
post:
description: Add a score to the database
description: Add a score to the database, upserts on id
operationId: score_create
tags:
- Score
@@ -628,32 +649,27 @@ paths:
description: ''
content:
application/json:
schema:
type: string
schema: {}
'401':
description: ''
content:
application/json:
schema:
type: string
schema: {}
'403':
description: ''
content:
application/json:
schema:
type: string
schema: {}
'404':
description: ''
content:
application/json:
schema:
type: string
schema: {}
'405':
description: ''
content:
application/json:
schema:
type: string
schema: {}
security: *ref_0
requestBody:
required: true
@@ -702,32 +718,27 @@ paths:
description: ''
content:
application/json:
schema:
type: string
schema: {}
'401':
description: ''
content:
application/json:
schema:
type: string
schema: {}
'403':
description: ''
content:
application/json:
schema:
type: string
schema: {}
'404':
description: ''
content:
application/json:
schema:
type: string
schema: {}
'405':
description: ''
content:
application/json:
schema:
type: string
schema: {}
security: *ref_0
/api/public/spans:
post:
@@ -747,32 +758,27 @@ paths:
description: ''
content:
application/json:
schema:
type: string
schema: {}
'401':
description: ''
content:
application/json:
schema:
type: string
schema: {}
'403':
description: ''
content:
application/json:
schema:
type: string
schema: {}
'404':
description: ''
content:
application/json:
schema:
type: string
schema: {}
'405':
description: ''
content:
application/json:
schema:
type: string
schema: {}
security: *ref_0
requestBody:
required: true
@@ -797,32 +803,27 @@ paths:
description: ''
content:
application/json:
schema:
type: string
schema: {}
'401':
description: ''
content:
application/json:
schema:
type: string
schema: {}
'403':
description: ''
content:
application/json:
schema:
type: string
schema: {}
'404':
description: ''
content:
application/json:
schema:
type: string
schema: {}
'405':
description: ''
content:
application/json:
schema:
type: string
schema: {}
security: *ref_0
requestBody:
required: true
@@ -848,32 +849,27 @@ paths:
description: ''
content:
application/json:
schema:
type: string
schema: {}
'401':
description: ''
content:
application/json:
schema:
type: string
schema: {}
'403':
description: ''
content:
application/json:
schema:
type: string
schema: {}
'404':
description: ''
content:
application/json:
schema:
type: string
schema: {}
'405':
description: ''
content:
application/json:
schema:
type: string
schema: {}
security: *ref_0
requestBody:
required: true
@@ -922,32 +918,27 @@ paths:
description: ''
content:
application/json:
schema:
type: string
schema: {}
'401':
description: ''
content:
application/json:
schema:
type: string
schema: {}
'403':
description: ''
content:
application/json:
schema:
type: string
schema: {}
'404':
description: ''
content:
application/json:
schema:
type: string
schema: {}
'405':
description: ''
content:
application/json:
schema:
type: string
schema: {}
security: *ref_0
/api/public/traces/{traceId}:
get:
@@ -973,32 +964,27 @@ paths:
description: ''
content:
application/json:
schema:
type: string
schema: {}
'401':
description: ''
content:
application/json:
schema:
type: string
schema: {}
'403':
description: ''
content:
application/json:
schema:
type: string
schema: {}
'404':
description: ''
content:
application/json:
schema:
type: string
schema: {}
'405':
description: ''
content:
application/json:
schema:
type: string
schema: {}
security: *ref_0
components:
schemas:
@@ -1577,6 +1563,27 @@ components:
required:
- data
- meta
Projects:
title: Projects
type: object
properties:
data:
type: array
items:
$ref: '#/components/schemas/Project'
required:
- data
Project:
title: Project
type: object
properties:
id:
type: string
name:
type: string
required:
- id
- name
CreateScoreRequest:
title: CreateScoreRequest
type: object
+65 -1
View File
@@ -341,6 +341,38 @@
}
]
},
{
"_type": "container",
"description": null,
"name": "Health",
"item": [
{
"_type": "endpoint",
"name": "Health",
"request": {
"description": "Check health of API and database",
"url": {
"raw": "{{baseUrl}}/api/public/health",
"host": [
"{{baseUrl}}"
],
"path": [
"api",
"public",
"health"
],
"query": [],
"variable": []
},
"header": [],
"method": "GET",
"auth": null,
"body": null
},
"response": []
}
]
},
{
"_type": "container",
"description": null,
@@ -481,6 +513,38 @@
}
]
},
{
"_type": "container",
"description": null,
"name": "Projects",
"item": [
{
"_type": "endpoint",
"name": "Get",
"request": {
"description": null,
"url": {
"raw": "{{baseUrl}}/api/public/projects",
"host": [
"{{baseUrl}}"
],
"path": [
"api",
"public",
"projects"
],
"query": [],
"variable": []
},
"header": [],
"method": "GET",
"auth": null,
"body": null
},
"response": []
}
]
},
{
"_type": "container",
"description": null,
@@ -490,7 +554,7 @@
"_type": "endpoint",
"name": "Create",
"request": {
"description": "Add a score to the database",
"description": "Add a score to the database, upserts on id",
"url": {
"raw": "{{baseUrl}}/api/public/scores",
"host": [
+13 -7
View File
@@ -27,17 +27,23 @@ const nextConfig = {
if (env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION !== undefined)
return [
{
source: '/api/auth/session',
source: "/api/auth/session",
headers: [
{ key: "Access-Control-Allow-Origin", value: "https://langfuse.com" },
{
key: "Access-Control-Allow-Origin",
value: "https://langfuse.com",
},
{ key: "Access-Control-Allow-Credentials", value: "true" },
{ key: "Access-Control-Allow-Methods", value: "GET,POST" },
{ key: "Access-Control-Allow-Headers", value: "Content-Type, Authorization" },
]
}
]
{
key: "Access-Control-Allow-Headers",
value: "Content-Type, Authorization",
},
],
},
];
return []
return [];
},
// webassembly support for @dqbd/tiktoken
+4310 -1224
View File
File diff suppressed because it is too large Load Diff
+55 -32
View File
@@ -1,6 +1,6 @@
{
"name": "langfuse-core",
"version": "0.1.0",
"version": "1.7.0",
"private": true,
"scripts": {
"prebuild": "cp generated/openapi-client/openapi.yml public/openapi-client.yml && cp generated/openapi-server/openapi.yml public/openapi-server.yml",
@@ -16,7 +16,8 @@
"test:e2e": "playwright test",
"db:migrate": "npx prisma migrate dev",
"db:seed": "npx prisma db seed",
"db:seed:examples": "npx prisma db seed -- --environment examples"
"db:seed:examples": "npx prisma db seed -- --environment examples",
"release": "release-it"
},
"prisma": {
"seed": "ts-node -r tsconfig-paths/register --compiler-options {\"module\":\"CommonJS\"} prisma/seed.ts"
@@ -30,7 +31,7 @@
"@heroicons/react": "^2.0.18",
"@hookform/resolvers": "^3.3.2",
"@next-auth/prisma-adapter": "^1.0.7",
"@prisma/client": "^5.5.2",
"@prisma/client": "^5.6.0",
"@radix-ui/react-avatar": "^1.0.4",
"@radix-ui/react-collapsible": "^1.0.3",
"@radix-ui/react-dialog": "^1.0.5",
@@ -47,7 +48,7 @@
"@radix-ui/react-tabs": "^1.0.4",
"@radix-ui/react-toggle": "^1.0.3",
"@radix-ui/react-tooltip": "^1.0.7",
"@sentry/nextjs": "^7.77.0",
"@sentry/nextjs": "^7.81.1",
"@sentry/profiling-node": "^1.2.6",
"@t3-oss/env-nextjs": "^0.7.1",
"@tailwindcss/forms": "^0.5.7",
@@ -55,12 +56,12 @@
"@tanstack/react-query": "^4.36.1",
"@tanstack/react-table": "^8.10.7",
"@tremor/react": "^3.11.1",
"@trpc/client": "^10.43.3",
"@trpc/next": "^10.43.3",
"@trpc/react-query": "^10.43.1",
"@trpc/server": "^10.43.1",
"@trpc/client": "^10.44.1",
"@trpc/next": "^10.44.1",
"@trpc/react-query": "^10.44.1",
"@trpc/server": "^10.44.1",
"@vercel/edge-config": "^0.4.1",
"axios": "^1.6.1",
"axios": "^1.6.2",
"bcryptjs": "^2.4.3",
"class-variance-authority": "^0.7.0",
"clsx": "^2.0.0",
@@ -72,65 +73,87 @@
"js-yaml": "^4.1.0",
"lodash": "^4.17.21",
"lucide-react": "^0.292.0",
"next": "^13.5.6",
"next": "^14.0.3",
"next-auth": "^4.24.5",
"next-query-params": "^4.2.3",
"next-query-params": "^4.3.0",
"next-swagger-doc": "^0.4.0",
"posthog-js": "^1.88.4",
"posthog-js": "^1.93.2",
"posthog-node": "^3.1.3",
"react": "18.2.0",
"react-day-picker": "^8.9.1",
"react-dom": "18.2.0",
"react-hook-form": "^7.47.0",
"react-icons": "^4.11.0",
"react-hook-form": "^7.48.2",
"react-icons": "^4.12.0",
"react-responsive": "^9.0.2",
"react18-json-view": "^0.2.7-canary.1",
"superagent": "^8.1.2",
"superjson": "2.0.0",
"tailwind-merge": "^1.14.0",
"superjson": "2.2.1",
"tailwind-merge": "^2.0.0",
"tailwindcss-animate": "^1.0.7",
"tiktoken": "^1.0.10",
"tiktoken": "^1.0.11",
"ts-pattern": "^5.0.5",
"use-query-params": "^2.2.1",
"uuid": "^9.0.1",
"zod": "^3.22.4"
},
"devDependencies": {
"@playwright/test": "^1.39.0",
"@playwright/test": "^1.40.0",
"@release-it/bumper": "^6.0.1",
"@testing-library/jest-dom": "^6.1.4",
"@types/bcryptjs": "^2.4.6",
"@types/cors": "^2.8.16",
"@types/eslint": "^8.44.6",
"@types/jest": "^29.5.8",
"@types/cors": "^2.8.17",
"@types/eslint": "^8.44.7",
"@types/jest": "^29.5.10",
"@types/js-yaml": "^4.0.9",
"@types/lodash": "^4.14.201",
"@types/node": "^20.8.10",
"@types/react": "^18.2.37",
"@types/react-dom": "^18.2.15",
"@types/lodash": "^4.14.202",
"@types/node": "18.16.0",
"@types/react": "^18.2.38",
"@types/react-dom": "^18.2.17",
"@types/swagger-ui-react": "^4.18.3",
"@types/uuid": "^9.0.7",
"@typescript-eslint/eslint-plugin": "^6.9.1",
"@typescript-eslint/parser": "^6.9.1",
"@typescript-eslint/eslint-plugin": "^6.12.0",
"@typescript-eslint/parser": "^6.12.0",
"autoprefixer": "^10.4.16",
"dotenv-cli": "^7.3.0",
"eslint": "^8.52.0",
"eslint-config-next": "^13.5.6",
"eslint": "^8.54.0",
"eslint-config-next": "^14.0.3",
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
"postcss": "^8.4.31",
"prettier": "^3.0.3",
"prettier": "^3.1.0",
"prettier-plugin-tailwindcss": "^0.5.7",
"prisma": "^5.5.2",
"prisma": "^5.6.0",
"release-it": "^17.0.0",
"swagger-jsdoc": "^6.2.8",
"tailwindcss": "^3.3.5",
"ts-node": "^10.9.1",
"tsconfig-paths": "^4.2.0",
"typescript": "^5.2.2"
"typescript": "^5.3.2"
},
"ct3aMetadata": {
"initVersion": "7.13.0"
},
"optionalDependencies": {
"crisp-sdk-web": "^1.0.21"
},
"release-it": {
"git": {
"commitMessage": "chore: release v${version}",
"tagName": "v${version}"
},
"github": {
"release": true,
"web": true,
"autoGenerate": true,
"releaseName": "v${version}"
},
"plugins": {
"@release-it/bumper": {
"out": {
"file": "./src/constants/VERSION.ts",
"type": "application/typescript"
}
}
}
}
}
@@ -0,0 +1,8 @@
/*
Warnings:
- A unique constraint covering the columns `[id,trace_id]` on the table `scores` will be added. If there are existing duplicate values, this will fail.
*/
-- CreateIndex
CREATE UNIQUE INDEX "scores_id_trace_id_key" ON "scores"("id", "trace_id");
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "cron_jobs" ADD COLUMN "job_started_at" TIMESTAMP(3);
+5 -3
View File
@@ -217,6 +217,7 @@ model Score {
observationId String? @map("observation_id")
observation Observation? @relation(fields: [observationId], references: [id], onDelete: SetNull)
@@unique([id, traceId]) // used for upsert
@@index([value])
@@index([traceId], type: Hash)
@@index([observationId], type: Hash)
@@ -246,9 +247,10 @@ model Pricing {
}
model CronJobs {
name String @id
lastRun DateTime? @map("last_run")
state String?
name String @id
lastRun DateTime? @map("last_run")
jobStartedAt DateTime? @map("job_started_at")
state String?
@@map("cron_jobs")
}
+32
View File
@@ -1,9 +1,12 @@
import { prisma } from "@/src/server/db";
import { jsonSchema } from "@/src/utils/zod";
import * as Sentry from "@sentry/nextjs";
import { ProfilingIntegration } from "@sentry/profiling-node";
import type { TransactionEvent } from "@sentry/types";
if (process.env.NEXT_PUBLIC_SENTRY_DSN)
Sentry.init({
// debug: true,
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
// Set tracesSampleRate to 1.0 to capture 100%
@@ -18,6 +21,35 @@ if (process.env.NEXT_PUBLIC_SENTRY_DSN)
new Sentry.Integrations.Prisma({ client: prisma }),
],
// filter out passwords from the signup request body
// transaction events are sentry transactions which include logs and spans.
beforeSendTransaction(transaction: TransactionEvent) {
if (
transaction.request &&
typeof transaction.request.data === "string" &&
transaction.request.url &&
transaction.request.url.includes("api/auth/signup")
) {
const jsonBody = jsonSchema.safeParse(transaction.request.data);
if (
jsonBody.success &&
typeof jsonBody.data === "object" &&
"data" in jsonBody.data
) {
delete jsonBody.data.password;
transaction.request.data = JSON.stringify(jsonBody.data);
transaction.request.data = JSON.stringify(jsonBody);
} else {
console.log("Signup: Non Json Request body");
}
return transaction;
}
return transaction;
},
// ...
// Note: if you want to override the automatic release value, do not set a
+102
View File
@@ -247,6 +247,108 @@ describe("/api/public/ingestion API Endpoint", () => {
expect(dbTrace.length).toBe(1);
});
it("should fail for auth errors", async () => {
const traceId = v4();
const scoreId = v4();
const responseOne = await makeAPICall("POST", "/api/public/ingestion", {
batch: [
{
id: v4(),
type: "trace-create",
timestamp: new Date().toISOString(),
body: {
id: traceId,
name: "trace-name",
userId: "user-1",
metadata: { key: "value" },
release: "1.0.0",
version: "2.0.0",
},
},
{
id: v4(),
type: "score-create",
timestamp: new Date().toISOString(),
body: {
id: scoreId,
name: "score-name",
value: 100.5,
traceId: "some-random-id",
},
},
],
});
console.log(responseOne.body);
expect(responseOne.status).toBe(207);
expect("errors" in responseOne.body).toBe(true);
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
expect(responseOne.body?.errors.length).toBe(1);
expect("successes" in responseOne.body).toBe(true);
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
expect(responseOne.body?.successes.length).toBe(1);
const dbTrace = await prisma.trace.findMany({
where: {
name: "trace-name",
},
});
expect(dbTrace.length).toBe(1);
});
it("should fail for resource not found", async () => {
const traceId = v4();
const responseOne = await makeAPICall("POST", "/api/public/ingestion", {
batch: [
{
id: v4(),
type: "trace-create",
timestamp: new Date().toISOString(),
body: {
id: traceId,
name: "trace-name",
userId: "user-1",
metadata: { key: "value" },
release: "1.0.0",
version: "2.0.0",
},
},
{
id: v4(),
type: "observation-update",
timestamp: new Date().toISOString(),
body: {
id: "some-random-id",
type: "GENERATION",
output: { key: "this is a great gpt output" },
},
},
],
});
console.log(responseOne.body);
expect(responseOne.status).toBe(207);
expect("errors" in responseOne.body).toBe(true);
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
expect(responseOne.body?.errors.length).toBe(1);
expect("successes" in responseOne.body).toBe(true);
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
expect(responseOne.body?.successes.length).toBe(1);
const dbTrace = await prisma.trace.findMany({
where: {
name: "trace-name",
},
});
expect(dbTrace.length).toBe(1);
});
it("should update all token counts if update does not contain model name", async () => {
const traceId = v4();
const generationId = v4();
+83
View File
@@ -38,6 +38,7 @@ describe("/api/public/scores API Endpoint", () => {
name: "score-name",
value: 100.5,
traceId: traceId,
comment: "comment",
});
expect(createScore.status).toBe(200);
@@ -52,6 +53,7 @@ describe("/api/public/scores API Endpoint", () => {
expect(dbScore?.name).toBe("score-name");
expect(dbScore?.value).toBe(100.5);
expect(dbScore?.observationId).toBeNull();
expect(dbScore?.comment).toBe("comment");
});
it("should create score for a trace with int", async () => {
@@ -148,4 +150,85 @@ describe("/api/public/scores API Endpoint", () => {
expect(dbScore?.name).toBe("score-name");
expect(dbScore?.value).toBe(100);
});
it("should upsert a score", async () => {
await pruneDatabase();
const traceId = uuidv4();
await makeAPICall("POST", "/api/public/traces", {
id: traceId,
name: "trace-name",
userId: "user-1",
projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
metadata: { key: "value" },
release: "1.0.0",
version: "2.0.0",
});
const dbTrace = await prisma.trace.findMany({
where: {
id: traceId,
},
});
expect(dbTrace.length).toBeGreaterThan(0);
expect(dbTrace[0]?.id).toBe(traceId);
const generationId = uuidv4();
await makeAPICall("POST", "/api/public/generations", {
id: generationId,
name: "generation-name",
traceId,
startTime: "2021-01-01T00:00:00.000Z",
endTime: "2021-01-01T00:00:00.000Z",
model: "model-name",
modelParameters: { key: "value" },
prompt: { key: "value" },
metadata: { key: "value" },
version: "2.0.0",
});
const dbGeneration = await prisma.observation.findMany({
where: {
id: generationId,
},
});
expect(dbGeneration.length).toBeGreaterThan(0);
expect(dbGeneration[0]?.id).toBe(generationId);
const scoreId = uuidv4();
const createScore = await makeAPICall("POST", "/api/public/scores", {
id: scoreId,
name: "score-name",
value: 100,
traceId: traceId,
comment: "comment",
});
expect(createScore.status).toBe(200);
const upsertScore = await makeAPICall("POST", "/api/public/scores", {
id: scoreId,
traceId: traceId,
name: "score-name-updated",
value: 200,
comment: "comment-updated",
observationId: dbGeneration[0]!.id,
});
expect(upsertScore.status).toBe(200);
const dbScore = await prisma.score.findUnique({
where: {
id: scoreId,
},
});
expect(dbScore?.id).toBe(scoreId);
expect(dbScore?.traceId).toBe(traceId);
expect(dbScore?.name).toBe("score-name-updated");
expect(dbScore?.value).toBe(200);
expect(dbScore?.comment).toBe("comment-updated");
expect(dbScore?.observationId).toBe(dbGeneration[0]!.id);
});
});
+77
View File
@@ -0,0 +1,77 @@
import { TrashIcon } from "lucide-react";
import { useRouter } from "next/router";
import { Button } from "@/src/components/ui/button";
import { api } from "@/src/utils/api";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/src/components/ui/popover";
import { useHasAccess } from "@/src/features/rbac/utils/checkAccess";
export function DeleteTrace({
traceId,
projectId,
isTableAction,
}: {
traceId: string;
projectId: string;
isTableAction?: boolean;
}) {
const router = useRouter();
const utils = api.useUtils();
const hasAccess = useHasAccess({ projectId, scope: "traces:delete" });
const mutDeleteTrace = api.traces.delete.useMutation({
onSuccess: () => {
if (!isTableAction) {
void router
.push(`/project/${projectId}/traces`)
.then(() => utils.traces.invalidate());
} else {
void utils.traces.invalidate();
}
},
});
if (!hasAccess) {
return null;
}
return (
<Popover>
<PopoverTrigger asChild>
{isTableAction ? (
<Button variant="ghost" size="xs">
<TrashIcon className="h-4 w-4" />
</Button>
) : (
<Button variant="outline" type="button">
<TrashIcon className="h-4 w-4" />
</Button>
)}
</PopoverTrigger>
<PopoverContent>
<h2 className="text-md mb-3 font-semibold">Please confirm</h2>
<p className="mb-3 text-sm">
This action cannot be undone and removes all the data associated with
this trace.
</p>
<div className="flex justify-end space-x-4">
<Button
type="button"
variant="destructive"
loading={mutDeleteTrace.isLoading}
onClick={() =>
void mutDeleteTrace.mutateAsync({ traceId, projectId })
}
>
Delete trace
</Button>
</div>
</PopoverContent>
</Popover>
);
}
+14
View File
@@ -1,5 +1,7 @@
import { ChevronLeftIcon, ChevronRightIcon } from "@heroicons/react/20/solid";
import Link from "next/link";
import { useRouter } from "next/router";
import { useSession } from "next-auth/react";
export default function Header(props: {
title: string;
@@ -7,6 +9,13 @@ export default function Header(props: {
live?: boolean;
actionButtons?: React.ReactNode;
}) {
const router = useRouter();
const session = useSession();
const currentPath = router.pathname;
const projectId = router.query.projectId;
const project = session.data?.user?.projects.find((p) => p.id === projectId);
const backHref =
props.breadcrumb &&
[...props.breadcrumb.map((i) => i.href).filter(Boolean)].pop();
@@ -62,6 +71,11 @@ export default function Header(props: {
<div className="mt-2 flex flex-wrap items-center justify-between gap-5">
<div className="flex items-center gap-3 md:gap-5">
<div className="min-w-0">
{project && projectId && currentPath !== "/project/[projectId]" ? (
<div className="text-sm font-medium text-gray-500">
{project.name}
</div>
) : null}
<h2 className="text-2xl font-bold leading-7 text-gray-900 sm:truncate sm:text-3xl sm:tracking-tight">
{props.title}
</h2>
+8 -22
View File
@@ -21,7 +21,8 @@ import { FeedbackButtonWrapper } from "@/src/features/feedback/component/Feedbac
import { Button } from "@/src/components/ui/button";
import Head from "next/head";
import { env } from "@/src/env.mjs";
import { LangfuseIcon, LangfuseLogo } from "@/src/components/LangfuseLogo";
import { LangfuseLogo } from "@/src/components/LangfuseLogo";
import { Spinner } from "@/src/components/layouts/spinner";
const userNavigation = [
{
@@ -41,7 +42,7 @@ export default function Layout(props: PropsWithChildren) {
const router = useRouter();
const session = useSession();
const enableExperimentalFeatures =
api.environment.enableExperimentalFeatures.useQuery().data;
api.environment.enableExperimentalFeatures.useQuery().data ?? false;
const projectId = router.query.projectId as string | undefined;
const navigation = ROUTES.filter(
@@ -66,9 +67,7 @@ export default function Layout(props: PropsWithChildren) {
const currentPathName = navigation.find(({ current }) => current)?.name;
const projects = api.projects.all.useQuery(undefined, {
enabled: session.status === "authenticated",
});
const projects = session.data?.user?.projects ?? [];
if (session.status === "loading") return <Spinner message="Loading" />;
@@ -104,12 +103,12 @@ export default function Layout(props: PropsWithChildren) {
const hideNavigation =
session.status === "unauthenticated" ||
projects.data?.length === 0 ||
projects.length === 0 ||
pathsWithoutNavigation.includes(router.pathname) ||
router.pathname.startsWith("/public/");
if (hideNavigation)
return (
<main className="h-full bg-gray-50 px-4 py-4 sm:px-6 lg:px-8">
<main className="min-h-screen bg-gray-50 px-4 py-4 sm:px-6 lg:px-8">
{props.children}
</main>
);
@@ -241,7 +240,7 @@ export default function Layout(props: PropsWithChildren) {
<NewProjectButton size="xs" />
</div>
<ul role="list" className="-mx-2 mt-2 space-y-1">
{projects.data?.map((project) => (
{projects.map((project) => (
<li key={project.name}>
<Link
href={`/project/${project.id}`}
@@ -344,7 +343,7 @@ export default function Layout(props: PropsWithChildren) {
<NewProjectButton size="xs" />
</div>
<ul role="list" className="-mx-2 mt-2 space-y-1">
{projects.data?.map((project, index) => (
{projects.map((project, index) => (
<li key={project.name}>
<Link
href={`/project/${project.id}`}
@@ -531,16 +530,3 @@ export default function Layout(props: PropsWithChildren) {
</>
);
}
function Spinner(props: { message: string }) {
return (
<div className="flex min-h-full flex-1 flex-col justify-center py-12 sm:px-6 lg:px-8">
<div className="sm:mx-auto sm:w-full sm:max-w-md">
<LangfuseIcon className="mx-auto motion-safe:animate-spin" size={42} />
<h2 className="mt-5 text-center text-2xl font-bold leading-9 tracking-tight text-gray-900">
{props.message} ...
</h2>
</div>
</div>
);
}
+14
View File
@@ -0,0 +1,14 @@
import { LangfuseIcon } from "@/src/components/LangfuseLogo";
export function Spinner(props: { message: string }) {
return (
<div className="flex min-h-full flex-1 flex-col justify-center py-12 sm:px-6 lg:px-8">
<div className="sm:mx-auto sm:w-full sm:max-w-md">
<LangfuseIcon className="mx-auto motion-safe:animate-spin" size={42} />
<h2 className="mt-5 text-center text-2xl font-bold leading-9 tracking-tight text-gray-900">
{props.message} ...
</h2>
</div>
</div>
);
}
+25 -10
View File
@@ -1,3 +1,4 @@
import { DeleteTrace } from "@/src/components/delete-trace";
import { GroupedScoreBadges } from "@/src/components/grouped-score-badge";
import { DataTable } from "@/src/components/table/data-table";
import { DataTableToolbar } from "@/src/components/table/data-table-toolbar";
@@ -217,6 +218,20 @@ export default function TracesTable({
accessorKey: "release",
header: "Release",
},
{
accessorKey: "action",
header: "Action",
cell: ({ row }) => {
const traceId = row.getValue("id");
return traceId && typeof traceId === "string" ? (
<DeleteTrace
traceId={traceId}
isTableAction={true}
projectId={projectId}
/>
) : undefined;
},
},
];
return (
@@ -239,16 +254,16 @@ export default function TracesTable({
traces.isLoading
? { isLoading: true, isError: false }
: traces.isError
? {
isLoading: false,
isError: true,
error: traces.error.message,
}
: {
isLoading: false,
isError: false,
data: traces.data?.map((t) => convertToTableRow(t)),
}
? {
isLoading: false,
isError: true,
error: traces.error.message,
}
: {
isLoading: false,
isError: false,
data: traces.data?.map((t) => convertToTableRow(t)),
}
}
pagination={{
pageCount: Math.ceil(totalCount / paginationState.pageSize),
+5
View File
@@ -13,6 +13,7 @@ import { DetailPageNav } from "@/src/features/navigate-detail-pages/DetailPageNa
import { useRouter } from "next/router";
import { type ObservationReturnType } from "@/src/server/api/routers/traces";
import { api } from "@/src/utils/api";
import { DeleteTrace } from "@/src/components/delete-trace";
export function Trace(props: {
observations: Array<ObservationReturnType>;
@@ -102,6 +103,10 @@ export function TracePage({ traceId }: { traceId: string }) {
}
listKey="traces"
/>
<DeleteTrace
traceId={trace.data.id}
projectId={trace.data.projectId}
/>
</>
}
/>
+1
View File
@@ -0,0 +1 @@
export const VERSION = "v1.7.0";
+3
View File
@@ -39,6 +39,7 @@ export const env = createEnv({
AUTH_GITHUB_CLIENT_ID: z.string().optional(),
AUTH_GITHUB_CLIENT_SECRET: z.string().optional(),
AUTH_DOMAINS_WITH_SSO_ENFORCEMENT: z.string().optional(),
AUTH_DISABLE_USERNAME_PASSWORD: z.enum(["true", "false"]).optional(),
},
/**
@@ -81,5 +82,7 @@ export const env = createEnv({
AUTH_GITHUB_CLIENT_SECRET: process.env.AUTH_GITHUB_CLIENT_SECRET,
AUTH_DOMAINS_WITH_SSO_ENFORCEMENT:
process.env.AUTH_DOMAINS_WITH_SSO_ENFORCEMENT,
AUTH_DISABLE_USERNAME_PASSWORD:
process.env.AUTH_DISABLE_USERNAME_PASSWORD,
},
});
@@ -0,0 +1,32 @@
import { env } from "@/src/env.mjs";
export const CloudPrivacyNotice = ({ action }: { action: string }) =>
env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION !== undefined ? (
<div className="mt-10 text-center text-xs text-gray-500">
By {action} you are agreeing to our{" "}
<a
href="https://langfuse.com/tos"
target="_blank"
rel="noopener noreferrer"
className="italic"
>
Terms of Service
</a>
,{" "}
<a
href="https://langfuse.com/privacy"
rel="noopener noreferrer"
className="italic"
>
Privacy Policy
</a>
, and{" "}
<a
href="https://langfuse.com/cookie-policy"
rel="noopener noreferrer"
className="italic"
>
Cookie Policy
</a>
</div>
) : null;
@@ -0,0 +1,74 @@
import { env } from "@/src/env.mjs";
import { Tabs, TabsList, TabsTrigger } from "@/src/components/ui/tabs";
import { Divider } from "@tremor/react";
import { usePostHog } from "posthog-js/react";
const regions = [
{
name: "US",
hostname: "us.cloud.langfuse.com",
flag: "🇺🇸",
},
{
name: "EU",
hostname: "cloud.langfuse.com",
flag: "🇪🇺",
},
];
export function CloudRegionSwitch({
isSignUpPage,
}: {
isSignUpPage?: boolean;
}) {
const posthog = usePostHog();
if (env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION === undefined) return null;
return (
<div>
<div className="flex w-full flex-col justify-between gap-3 md:flex-row md:items-center">
<div>
<span className="text-sm font-medium leading-none">Data Region</span>
<p className="text-xs text-gray-500">
Regions are strictly separated.
</p>
{env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION !== undefined &&
isSignUpPage ? (
<p className="text-xs text-gray-500">
{env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION === "EU"
? "✅ Demo project available"
: "❌ Choose EU for demo project access"}
</p>
) : null}
</div>
<Tabs value={env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION}>
<TabsList>
{regions.map((region) => (
<TabsTrigger
key={region.name}
value={region.name}
onClick={() => {
posthog.capture(
"cloud_region_switch",
{
region: region.name,
},
{
send_instantly: true,
},
);
window.location.hostname = region.hostname;
}}
>
<span className="mr-2 text-xl leading-none">{region.flag}</span>
{region.name}
</TabsTrigger>
))}
</TabsList>
</Tabs>
</div>
<Divider className="text-gray-400" />
</div>
);
}
@@ -5,7 +5,6 @@ import { DashboardCard } from "@/src/features/dashboard/components/cards/Dashboa
import { compactNumberFormatter, usdFormatter } from "@/src/utils/numbers";
import { TabComponent } from "@/src/features/dashboard/components/TabsComponent";
import { BarList } from "@tremor/react";
import { isUndefinedOrNull } from "@/src/utils/types";
import { TotalMetric } from "@/src/features/dashboard/components/TotalMetric";
import { ExpandListButton } from "@/src/features/dashboard/components/cards/ChevronButton";
import { useState } from "react";
@@ -84,7 +83,6 @@ export const UserChart = ({
value: item.totalTokenCost ? (item.totalTokenCost as number) : 0,
};
})
.filter((i) => (isUndefinedOrNull(i.name) ? true : false))
: [];
const totalCost = user.data?.reduce(
@@ -31,7 +31,7 @@ export function DatasetItemsTable({
projectId: string;
datasetId: string;
}) {
const utils = api.useContext();
const utils = api.useUtils();
const items = api.datasets.itemsByDatasetId.useQuery({
projectId,
datasetId,
@@ -156,16 +156,16 @@ export function DatasetItemsTable({
items.isLoading
? { isLoading: true, isError: false }
: items.isError
? {
isLoading: false,
isError: true,
error: items.error.message,
}
: {
isLoading: false,
isError: false,
data: items.data?.map((t) => convertToTableRow(t)),
}
? {
isLoading: false,
isError: true,
error: items.error.message,
}
: {
isLoading: false,
isError: false,
data: items.data?.map((t) => convertToTableRow(t)),
}
}
/>
<NewDatasetItemButton
@@ -29,7 +29,7 @@ type RowData = {
};
export function DatasetsTable(props: { projectId: string }) {
const utils = api.useContext();
const utils = api.useUtils();
const datasets = api.datasets.allDatasets.useQuery({
projectId: props.projectId,
});
@@ -146,16 +146,16 @@ export function DatasetsTable(props: { projectId: string }) {
datasets.isLoading
? { isLoading: true, isError: false }
: datasets.isError
? {
isLoading: false,
isError: true,
error: datasets.error.message,
}
: {
isLoading: false,
isError: false,
data: datasets.data?.map((t) => convertToTableRow(t)),
}
? {
isLoading: false,
isError: true,
error: datasets.error.message,
}
: {
isLoading: false,
isError: false,
data: datasets.data?.map((t) => convertToTableRow(t)),
}
}
/>
<NewDatasetButton projectId={props.projectId} className="mt-4" />
@@ -56,7 +56,7 @@ export const EditDatasetItem = ({
projectId: projectId,
scope: "datasets:CUD",
});
const utils = api.useContext();
const utils = api.useUtils();
const item = api.datasets.itemById.useQuery({
datasetId,
projectId,
@@ -32,7 +32,7 @@ export const NewDatasetForm = (props: {
},
});
const utils = api.useContext();
const utils = api.useUtils();
const createDatasetMutation = api.datasets.createDataset.useMutation({
onSuccess: () => utils.datasets.invalidate(),
onError: (error) => setFormError(error.message),
@@ -83,7 +83,7 @@ export const NewDatasetItemForm = (props: {
projectId: props.projectId,
});
const utils = api.useContext();
const utils = api.useUtils();
const createDatasetItemMutation = api.datasets.createDatasetItem.useMutation({
onSuccess: () => utils.datasets.invalidate(),
onError: (error) => setFormError(error.message),
+20 -1
View File
@@ -6,6 +6,10 @@ import { Prisma } from "@prisma/client";
const operatorReplacements = {
"any of": "IN",
"none of": "NOT IN",
contains: "ILIKE",
"does not contain": "NOT ILIKE",
"starts with": "ILIKE",
"ends with": "ILIKE",
};
export function filterToPrismaSql(
@@ -63,8 +67,23 @@ export function filterToPrismaSql(
filter.type === "numberObject"
? [Prisma.raw("cast("), Prisma.raw(" as double precision)")]
: [Prisma.empty, Prisma.empty];
const [valuePrefix, valueSuffix] =
filter.type === "string" || filter.type === "stringObject"
? [
["contains", "does not contain", "ends with"].includes(
filter.operator,
)
? Prisma.raw("'%' || ")
: Prisma.empty,
["contains", "does not contain", "starts with"].includes(
filter.operator,
)
? Prisma.raw(" || '%'")
: Prisma.empty,
]
: [Prisma.empty, Prisma.empty];
return Prisma.sql`${cast1}${colPrisma}${jsonKeyPrisma}${cast2} ${operatorPrisma} ${valuePrisma}`;
return Prisma.sql`${cast1}${colPrisma}${jsonKeyPrisma}${cast2} ${operatorPrisma} ${valuePrefix}${valuePrisma}${valueSuffix}`;
});
if (statements.length === 0) {
return Prisma.empty;
@@ -58,7 +58,7 @@ export function ManualScoreButton({
: s.observationId === null),
);
const utils = api.useContext();
const utils = api.useUtils();
const onSuccess = async () => {
await Promise.all([utils.scores.invalidate(), utils.traces.invalidate()]);
};
@@ -179,8 +179,8 @@ export function ManualScoreButton({
{form.formState.isSubmitting
? "Loading ..."
: score
? "Update"
: "Create"}
? "Update"
: "Create"}
</Button>
{score && (
<Button
@@ -25,7 +25,7 @@ import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
export function DeleteProjectButton(props: { projectId: string }) {
const utils = api.useContext();
const utils = api.useUtils();
const router = useRouter();
const session = useSession();
const posthog = usePostHog();
@@ -77,8 +77,6 @@ export function DeleteProjectButton(props: { projectId: string }) {
});
};
if (!hasAccess) return null;
return (
<div>
<h2 className="mb-6 text-base font-semibold leading-6 text-gray-900">
@@ -86,7 +84,9 @@ export function DeleteProjectButton(props: { projectId: string }) {
</h2>
<Dialog>
<DialogTrigger asChild>
<Button variant="destructive">Delete Project</Button>
<Button variant="destructive" disabled={!hasAccess}>
Delete Project
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-[425px]">
<DialogHeader>
@@ -44,7 +44,7 @@ export function NewProjectButton({ size = "default" }: NewProjectButtonProps) {
name: "",
},
});
const utils = api.useContext();
const utils = api.useUtils();
const router = useRouter();
const posthog = usePostHog();
const createProjectMutation = api.projects.create.useMutation({
@@ -0,0 +1,130 @@
import { Button } from "@/src/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/src/components/ui/dialog";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/src/components/ui/form";
import { Input } from "@/src/components/ui/input";
import { api } from "@/src/utils/api";
import { usePostHog } from "posthog-js/react";
import { useRouter } from "next/router";
import * as z from "zod";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { useSession } from "next-auth/react";
import { useHasAccess } from "@/src/features/rbac/utils/checkAccess";
const formSchema = z.object({
newOwnerEmail: z.string().email(),
});
export function TransferOwnershipButton(props: { projectId: string }) {
const utils = api.useUtils();
const router = useRouter();
const posthog = usePostHog();
const session = useSession();
const project = session?.data?.user?.projects?.find(
(project) => project.id == props.projectId,
);
const hasAccess = useHasAccess({
projectId: props.projectId,
scope: "project:transfer",
});
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
newOwnerEmail: "",
},
});
const transferProject = api.projects.transfer.useMutation({
onSuccess: () => utils.projects.invalidate(),
onError: (error) =>
form.setError("newOwnerEmail", { message: error.message }),
});
function onSubmit(values: z.infer<typeof formSchema>) {
form.clearErrors();
transferProject
.mutateAsync({
projectId: props.projectId,
newOwnerEmail: values.newOwnerEmail,
})
.then(() => {
posthog.capture("project_settings:project_transfer");
void router.push("/");
})
.catch((error) => {
console.error(error);
});
}
return (
<div>
<Dialog>
<DialogTrigger asChild>
<Button variant="destructive" disabled={!hasAccess}>
Transfer Ownership
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle className="text-lg font-semibold">
Transfer project{project?.name ? ` (${project.name})` : ""}
</DialogTitle>
<DialogDescription>
You will lose ownership of this project and will become an admin.
You cannot undo this action.
</DialogDescription>
</DialogHeader>
<Form {...form}>
<form
// eslint-disable-next-line @typescript-eslint/no-misused-promises
onSubmit={form.handleSubmit(onSubmit)}
className="space-y-8"
data-testid="new-project-form"
>
<FormField
control={form.control}
name="newOwnerEmail"
render={({ field }) => (
<FormItem>
<FormLabel>New owner</FormLabel>
<FormControl>
<Input
placeholder="user@example.com"
{...field}
data-testid="new-project-name-input"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button
type="submit"
variant="destructive"
loading={transferProject.isLoading}
className="w-full"
>
Transfer project
</Button>
</form>
</Form>
</DialogContent>
</Dialog>
</div>
);
}
@@ -67,4 +67,61 @@ export const projectsRouter = createTRPCRouter({
return true;
}),
transfer: protectedProcedure
.input(
z.object({
projectId: z.string(),
newOwnerEmail: z.string().email(),
}),
)
.mutation(async ({ input, ctx }) => {
throwIfNoAccess({
session: ctx.session,
projectId: input.projectId,
scope: "project:transfer",
});
// Check if new owner exists
const newOwner = await ctx.prisma.user.findUnique({
where: {
email: input.newOwnerEmail.toLowerCase(),
},
});
if (!newOwner) throw new Error("User not found");
if (newOwner.id === ctx.session.user.id)
throw new Error("You cannot transfer project to yourself");
return ctx.prisma.$transaction([
// Add new owner, upsert to update role if already exists
ctx.prisma.membership.upsert({
where: {
projectId_userId: {
projectId: input.projectId,
userId: newOwner.id,
},
},
update: {
role: "OWNER",
},
create: {
userId: newOwner.id,
projectId: input.projectId,
role: "OWNER",
},
}),
// Update old owner to admin
ctx.prisma.membership.update({
where: {
projectId_userId: {
projectId: input.projectId,
userId: ctx.session.user.id,
},
},
data: {
role: "ADMIN",
},
}),
]);
}),
});
@@ -104,7 +104,7 @@ function DeleteApiKeyButton(props: { projectId: string; apiKeyId: string }) {
scope: "apiKeys:delete",
});
const utils = api.useContext();
const utils = api.useUtils();
const mutDeleteApiKey = api.apiKeys.delete.useMutation({
onSuccess: () => utils.apiKeys.invalidate(),
});
@@ -5,22 +5,25 @@ import { PlusIcon } from "lucide-react";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/src/components/ui/dialog";
import { CodeView } from "@/src/components/ui/code";
import { useHasAccess } from "@/src/features/rbac/utils/checkAccess";
import { usePostHog } from "posthog-js/react";
import { env } from "@/src/env.mjs";
export function CreateApiKeyButton(props: { projectId: string }) {
const utils = api.useContext();
const utils = api.useUtils();
const posthog = usePostHog();
const hasAccess = useHasAccess({
projectId: props.projectId,
scope: "apiKeys:create",
});
const hostname =
env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION !== "EU" ? window.origin : undefined;
const mutCreateApiKey = api.apiKeys.create.useMutation({
onSuccess: () => utils.apiKeys.invalidate(),
});
@@ -64,9 +67,19 @@ export function CreateApiKeyButton(props: { projectId: string }) {
</Button>
</DialogTrigger>
<DialogContent onPointerDownOutside={(e) => e.preventDefault()}>
<DialogHeader>
<DialogTitle>API Keys</DialogTitle>
</DialogHeader>
{hostname ? (
<>
<DialogTitle>Hostname</DialogTitle>
<div className="mb-6">
<div className="my-2">
When connecting to Langfuse, use this hostname / baseurl.
</div>
<CodeView content={hostname} />
</div>
</>
) : null}
<DialogTitle>API Keys</DialogTitle>
<div className="mb-2">
<div className="text-md font-semibold">Secret Key</div>
<div className="my-2">
@@ -15,7 +15,7 @@ export const PublishTraceSwitch = (props: {
projectId: props.projectId,
scope: "traces:publish",
});
const utils = api.useContext();
const utils = api.useUtils();
const mut = api.publishTraces.update.useMutation({
onSuccess: () => utils.traces.invalidate(),
});
@@ -40,7 +40,7 @@ const availableRoles = [
] as const;
const formSchema = z.object({
email: z.string().email(),
email: z.string().trim().email(),
role: z.enum(availableRoles),
});
@@ -51,7 +51,7 @@ export function CreateProjectMemberButton(props: { projectId: string }) {
scope: "members:create",
});
const utils = api.useContext();
const utils = api.useUtils();
const mutCreateProjectMember = api.projectMembers.create.useMutation({
onSuccess: () => utils.projectMembers.invalidate(),
});
@@ -12,6 +12,7 @@ import { Button } from "@/src/components/ui/button";
import { TrashIcon } from "lucide-react";
import { useHasAccess } from "@/src/features/rbac/utils/checkAccess";
import { CreateProjectMemberButton } from "@/src/features/rbac/components/CreateProjectMemberButton";
import { useSession } from "next-auth/react";
export function ProjectMembersTable({ projectId }: { projectId: string }) {
const hasReadAccess = useHasAccess({
@@ -23,7 +24,9 @@ export function ProjectMembersTable({ projectId }: { projectId: string }) {
scope: "members:delete",
});
const utils = api.useContext();
const session = useSession();
const utils = api.useUtils();
const memberships = api.projectMembers.get.useQuery(
{
projectId: projectId,
@@ -59,7 +62,9 @@ export function ProjectMembersTable({ projectId }: { projectId: string }) {
<TableCell>{m.user.name}</TableCell>
<TableCell>{m.user.email}</TableCell>
<TableCell>{m.role}</TableCell>
{hasDeleteAccess && m.role !== "OWNER" ? (
{hasDeleteAccess &&
m.user.id !== session.data?.user?.id &&
m.role !== "OWNER" ? (
<TableCell>
<Button
variant="ghost"
@@ -10,10 +10,12 @@ const scopes = [
"apiKeys:delete",
"traces:publish",
"traces:delete",
"scores:CUD",
"project:delete",
"project:transfer",
"datasets:CUD",
] as const;
@@ -30,8 +32,10 @@ export const roleAccessRights: Record<MembershipRole, Scope[]> = {
"apiKeys:create",
"apiKeys:delete",
"traces:publish",
"traces:delete",
"scores:CUD",
"project:delete",
"project:transfer",
"datasets:CUD",
],
ADMIN: [
@@ -42,8 +46,8 @@ export const roleAccessRights: Record<MembershipRole, Scope[]> = {
"apiKeys:create",
"apiKeys:delete",
"traces:publish",
"traces:delete",
"scores:CUD",
"project:delete",
"datasets:CUD",
],
MEMBER: ["members:read", "traces:publish", "scores:CUD", "datasets:CUD"],
+7 -7
View File
@@ -34,13 +34,13 @@ function hasAccess(p: HasAccessParams): boolean {
? // MembershipRole
p.role
: "data" in p.session
? // SessionContextValue
p.session.data?.user?.projects.find(
(project) => project.id === p.projectId,
)?.role
: // Session
p.session.user?.projects.find((project) => project.id === p.projectId)
?.role;
? // SessionContextValue
p.session.data?.user?.projects.find(
(project) => project.id === p.projectId,
)?.role
: // Session
p.session.user?.projects.find((project) => project.id === p.projectId)
?.role;
if (role === undefined) return false;
return roleAccessRights[role].includes(p.scope);
+12
View File
@@ -0,0 +1,12 @@
# Telemetry service for Docker deployments
By default, Langfuse automatically reports basic usage statistics to a centralized server (PostHog).
This helps us to:
1. Understand how Langfuse is used and improve the most relevant features.
2. Track overall usage for internal and external (e.g. fundraising) reporting.
None of the data is shared with third parties and does not include any sensitive information. We want to be super transparent about this and you can find the exact data we collect [here](/src/features/telemetry/index.ts).
You can opt-out by setting `TELEMETRY_ENABLED=false`.
+272
View File
@@ -0,0 +1,272 @@
import { VERSION } from "@/src/constants/VERSION";
import { prisma } from "@/src/server/db";
import { Prisma } from "@prisma/client";
import { PostHog } from "posthog-node";
import { v4 as uuidv4 } from "uuid";
// Safe as it is intended to be public
const POSTHOG_API_KEY = "phc_zkMwFajk8ehObUlMth0D7DtPItFnxETi3lmSvyQDrwB";
// Interval between jobs in milliseconds
const JOB_INTERVAL_MINUTES = Prisma.raw("60");
// Timeout for job in minutes, if job is not finished in this time, it will be retried
const JOB_TIMEOUT_MINUTES = Prisma.raw("10");
export async function telemetry() {
try {
// Only run in prod
if (process.env.NODE_ENV !== "production") return;
// Do not run in Lanfuse cloud, separate telemetry is used
if (process.env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION !== undefined) return;
// Check if telemetry is not disabled
if (process.env.TELEMETRY_ENABLED === "false") return;
// Do not run in CI
if (process.env.CI) return;
// Check via db cron_jobs table if it is time to run job
const job = await jobScheduler();
if (job.shouldRunJob) {
const { jobStartedAt, lastRun, clientId } = job;
// Run telemetry job
await posthogTelemetry({
startTimeframe: lastRun,
endTimeframe: jobStartedAt,
clientId,
});
// Update cron_jobs table
await prisma.cronJobs.update({
where: { name: "telemetry" },
data: { lastRun: jobStartedAt, state: clientId, jobStartedAt: null },
});
}
} catch (error) {
// Catch all errors to be sure telemetry does not break the application
console.error("Telemetry, unexpected error:", error);
}
}
/**
* Checks if a job should be scheduled and returns the necessary information.
* @returns A promise that resolves to an object with the following properties:
* - shouldRunJob: A boolean indicating whether the job should be run.
* - job_started_at: The timestamp when the job was started.
* - last_run: The timestamp of the last run of the job, or null if it has never run.
* - clientId: A unique clienId that identifies the host.
*/
async function jobScheduler(): Promise<
| { shouldRunJob: false }
| {
shouldRunJob: true;
jobStartedAt: Date;
lastRun: Date | null;
clientId: string;
}
> {
// Check if job should run, without a lock to not impact performance
// "not exists" triggers when this is run for the very first time in a container
const checkNoLock = await prisma.$queryRaw<Array<{ status: boolean }>>`
SELECT (
EXISTS (
SELECT 1
FROM cron_jobs
WHERE name = 'telemetry'
AND (last_run IS NULL OR last_run <= (NOW() - INTERVAL '${JOB_INTERVAL_MINUTES} minute'))
AND (job_started_at IS NULL OR job_started_at <= (NOW() - INTERVAL '${JOB_TIMEOUT_MINUTES} minute'))
)
OR NOT EXISTS (
SELECT 1
FROM cron_jobs
WHERE name = 'telemetry'
)
) AS status;`;
// Return if job should not run
if (checkNoLock.length !== 1) {
console.error("Telemetry failed to check if job should run");
return { shouldRunJob: false };
}
if (checkNoLock[0]!.status === false) return { shouldRunJob: false };
// Lock table and update job_started_at if no other job was created in the meantime
const res = await prisma.$transaction([
prisma.$executeRaw`LOCK TABLE cron_jobs IN SHARE ROW EXCLUSIVE MODE`,
prisma.$queryRaw<
Array<{
name: string;
last_run: Date | null;
job_started_at: Date | null;
state: string | null;
}>
>`INSERT INTO cron_jobs (name, last_run, job_started_at, state)
VALUES ('telemetry', NULL, CURRENT_TIMESTAMP, NULL)
ON CONFLICT (name)
DO UPDATE
SET job_started_at = CASE
WHEN (cron_jobs.last_run IS NULL OR cron_jobs.last_run <= (NOW() - INTERVAL '${JOB_INTERVAL_MINUTES} minutes'))
AND (cron_jobs.job_started_at IS NULL OR cron_jobs.job_started_at <= (NOW() - INTERVAL '${JOB_TIMEOUT_MINUTES} minutes'))
THEN CURRENT_TIMESTAMP
ELSE cron_jobs.job_started_at
END
WHERE cron_jobs.name = 'telemetry'
AND (cron_jobs.last_run IS NULL OR cron_jobs.last_run <= (NOW() - INTERVAL '${JOB_INTERVAL_MINUTES} minutes'))
AND (cron_jobs.job_started_at IS NULL OR cron_jobs.job_started_at <= (NOW() - INTERVAL '${JOB_TIMEOUT_MINUTES} minutes'))
RETURNING *`,
]);
const createJobLocked = res[1];
// Other job was created in the meantime
if (createJobLocked.length !== 1) {
console.error("Telemetry job is locked");
return { shouldRunJob: false };
}
const jobStartedAt = createJobLocked[0]?.job_started_at ?? null;
// should not happen
if (!jobStartedAt) {
console.error("Telemetry failed to create job_started_at");
return { shouldRunJob: false };
}
// set vars
const lastRun = createJobLocked[0]?.last_run ?? null;
const clientId = createJobLocked[0]?.state ?? uuidv4();
return { shouldRunJob: true, jobStartedAt, lastRun, clientId };
}
async function posthogTelemetry({
startTimeframe,
endTimeframe,
clientId,
}: {
startTimeframe: Date | null;
endTimeframe: Date;
clientId: string;
}) {
try {
const posthog = new PostHog(POSTHOG_API_KEY, {
host: "https://eu.posthog.com",
});
if (process.env.NODE_ENV === "development") posthog.debug();
// Count projects
const totalProjects = await prisma.project.count();
// Count traces
const countTraces = await prisma.trace.count({
where: {
timestamp: {
gte: startTimeframe?.toISOString(),
lt: endTimeframe.toISOString(),
},
},
});
// Count scores
const countScores = await prisma.score.count({
where: {
timestamp: {
gte: startTimeframe?.toISOString(),
lt: endTimeframe.toISOString(),
},
},
});
// Count observations
const countObservations = await prisma.observation.count({
where: {
startTime: {
gte: startTimeframe?.toISOString(),
lt: endTimeframe.toISOString(),
},
},
});
// Count datasets
const countDatasets = await prisma.dataset.count({
where: {
createdAt: {
gte: startTimeframe?.toISOString(),
lt: endTimeframe.toISOString(),
},
},
});
// Count dataset items
const countDatasetItems = await prisma.datasetItem.count({
where: {
createdAt: {
gte: startTimeframe?.toISOString(),
lt: endTimeframe.toISOString(),
},
},
});
// Count dataset runs
const countDatasetRuns = await prisma.datasetRuns.count({
where: {
createdAt: {
gte: startTimeframe?.toISOString(),
lt: endTimeframe.toISOString(),
},
},
});
// Count dataset run items
const countDatasetRunItems = await prisma.datasetRunItems.count({
where: {
createdAt: {
gte: startTimeframe?.toISOString(),
lt: endTimeframe.toISOString(),
},
},
});
// Domains (no PII)
const domains = await prisma.$queryRaw<Array<{ domain: string }>>`
SELECT
substring(email FROM position('@' in email) + 1) as domain,
count(id)::int as "userCount"
FROM users
WHERE email ILIKE '%@%'
GROUP BY 1
ORDER BY count(id) desc
LIMIT 30
`;
posthog.identify({
distinctId: "docker:" + clientId,
properties: {
environment: process.env.NODE_ENV,
userDomains: domains,
docker: true,
langfuseVersion: VERSION,
},
});
posthog.capture({
distinctId: "docker:" + clientId,
event: "telemetry",
properties: {
langfuseVersion: VERSION,
totalProjects: totalProjects,
traces: countTraces,
scores: countScores,
observations: countObservations,
datasets: countDatasets,
datasetItems: countDatasetItems,
datasetRuns: countDatasetRuns,
datasetRunItems: countDatasetRunItems,
startTimeframe: startTimeframe?.toISOString(),
endTimeframe: endTimeframe.toISOString(),
},
});
await posthog.shutdownAsync();
} catch (error) {
console.error(error);
}
}
+11
View File
@@ -3,6 +3,10 @@ import { createUserEmailPassword } from "@/src/features/auth/lib/emailPassword";
import { signupSchema } from "@/src/features/auth/lib/signupSchema";
import type { NextApiRequest, NextApiResponse } from "next";
/*
* Sign-up endpoint (email/password users), creates user in database.
* SSO users are created by the NextAuth adapters.
*/
export default async function handler(
req: NextApiRequest,
res: NextApiResponse,
@@ -12,6 +16,13 @@ export default async function handler(
res.status(422).json({ message: "Sign up is disabled." });
return;
}
if (env.AUTH_DISABLE_USERNAME_PASSWORD === "true") {
res.status(422).json({
message:
"Sign up with email and password is disabled for this instance. Please use SSO.",
});
return;
}
// parse and type check the request body with zod
const validBody = signupSchema.safeParse(req.body);
-137
View File
@@ -1,137 +0,0 @@
import { prisma } from "@/src/server/db";
import { type NextApiRequest, type NextApiResponse } from "next";
import { PostHog } from "posthog-node";
import { v4 as uuidv4 } from "uuid";
// Safe as it is intended to be public
const POSTHOG_API_KEY = "phc_zkMwFajk8ehObUlMth0D7DtPItFnxETi3lmSvyQDrwB";
export default async function handler(
req: NextApiRequest,
res: NextApiResponse,
) {
try {
const posthog = new PostHog(POSTHOG_API_KEY, {
host: "https://eu.posthog.com",
});
if (process.env.NODE_ENV === "development") posthog.debug();
// Time frame is the last time this cron job ran until now
const dbCronState = await prisma.cronJobs.findUnique({
where: { name: "telemetry" },
});
const startTimeframe = dbCronState?.lastRun ?? undefined;
const clientId = dbCronState?.state ?? uuidv4();
const endTimeframe = new Date(Date.now());
// Count projects
const totalProjects = await prisma.project.count();
// Count traces
const countTraces = await prisma.trace.count({
where: {
timestamp: {
gte: startTimeframe?.toISOString(),
lt: endTimeframe.toISOString(),
},
},
});
// Count scores
const countScores = await prisma.score.count({
where: {
timestamp: {
gte: startTimeframe?.toISOString(),
lt: endTimeframe.toISOString(),
},
},
});
// Count observations
const countObservations = await prisma.observation.count({
where: {
startTime: {
gte: startTimeframe?.toISOString(),
lt: endTimeframe.toISOString(),
},
},
});
// Count datasets
const countDatasets = await prisma.dataset.count({
where: {
createdAt: {
gte: startTimeframe?.toISOString(),
lt: endTimeframe.toISOString(),
},
},
});
// Count dataset items
const countDatasetItems = await prisma.datasetItem.count({
where: {
createdAt: {
gte: startTimeframe?.toISOString(),
lt: endTimeframe.toISOString(),
},
},
});
// Count dataset runs
const countDatasetRuns = await prisma.datasetRuns.count({
where: {
createdAt: {
gte: startTimeframe?.toISOString(),
lt: endTimeframe.toISOString(),
},
},
});
// Count dataset run items
const countDatasetRunItems = await prisma.datasetRunItems.count({
where: {
createdAt: {
gte: startTimeframe?.toISOString(),
lt: endTimeframe.toISOString(),
},
},
});
posthog.identify({
distinctId: "docker:" + clientId,
properties: {
environment: process.env.NODE_ENV,
docker: true,
},
});
posthog.capture({
distinctId: "docker:" + clientId,
event: "telemetry",
properties: {
totalProjects: totalProjects,
traces: countTraces,
scores: countScores,
observations: countObservations,
datasets: countDatasets,
datasetItems: countDatasetItems,
datasetRuns: countDatasetRuns,
datasetRunItems: countDatasetRunItems,
startTimeframe: startTimeframe?.toISOString(),
endTimeframe: endTimeframe.toISOString(),
},
});
await posthog.shutdownAsync();
await prisma.cronJobs.upsert({
where: { name: "telemetry" },
update: { lastRun: endTimeframe, state: clientId },
create: { name: "telemetry", lastRun: endTimeframe, state: clientId },
});
return res.status(200).json({ message: "OK" });
} catch (error) {
console.error(error);
return res.status(500).json({ message: "Internal server error" });
}
}
+3 -1
View File
@@ -1,4 +1,5 @@
import { cors, runMiddleware } from "@/src/features/public-api/server/cors";
import { telemetry } from "@/src/features/telemetry";
import { prisma } from "@/src/server/db";
import { type NextApiRequest, type NextApiResponse } from "next";
@@ -7,10 +8,11 @@ export default async function handler(
res: NextApiResponse,
) {
await runMiddleware(req, res, cors);
await telemetry();
try {
await prisma.$queryRaw`SELECT 1;`;
} catch (e) {
return res.status(500).json({ status: "Database not available" });
return res.status(503).json({ status: "Database not available" });
}
return res.status(200).json({ status: "OK" });
}
+22 -16
View File
@@ -20,6 +20,7 @@ import { ObservationProcessor } from "../../../server/api/services/EventProcesso
import { TraceProcessor } from "../../../server/api/services/EventProcessor";
import { ScoreProcessor } from "../../../server/api/services/EventProcessor";
import { isNotNullOrUndefined } from "@/src/utils/types";
import { telemetry } from "@/src/features/telemetry";
export default async function handler(
req: NextApiRequest,
@@ -81,6 +82,8 @@ export default async function handler(
const filteredBatch: z.infer<typeof singleEventSchema>[] =
batch.filter(isNotNullOrUndefined);
await telemetry();
const sortedBatch = sortBatch(filteredBatch);
const result = await handleBatch(sortedBatch, req, authCheck);
@@ -242,24 +245,27 @@ export const handleBatchResult = (
message: "Invalid request data",
error: error.error.message,
});
} else if (error.error instanceof AuthenticationError) {
returnedErrors.push({
id: error.id,
status: 401,
message: "Authentication error",
error: error.error.message,
});
} else if (error.error instanceof ResourceNotFoundError) {
if (!errors.some((res) => res.id === error.id)) {
returnedErrors.push({
id: error.id,
status: 404,
message: "Resource not found",
error: error.error.message,
});
}
returnedErrors.push({
id: error.id,
status: 404,
message: "Resource not found",
error: error.error.message,
});
} else {
if (!errors.some((res) => res.id === error.id)) {
returnedErrors.push({
id: error.id,
status: 500,
message: "Error processing events",
error: "Internal Server Error",
});
}
returnedErrors.push({
id: error.id,
status: 500,
message: "Error processing events",
error: "Internal Server Error",
});
}
});
+47
View File
@@ -0,0 +1,47 @@
import { verifyAuthHeaderAndReturnScope } from "@/src/features/public-api/server/apiAuth";
import { cors, runMiddleware } from "@/src/features/public-api/server/cors";
import { prisma } from "@/src/server/db";
import { type NextApiRequest, type NextApiResponse } from "next";
export default async function handler(
req: NextApiRequest,
res: NextApiResponse,
) {
await runMiddleware(req, res, cors);
// CHECK AUTH
const authCheck = await verifyAuthHeaderAndReturnScope(
req.headers.authorization,
);
if (!authCheck.validKey)
return res.status(401).json({
message: authCheck.error,
});
// END CHECK AUTH
if (req.method === "GET") {
try {
const projects = await prisma.project.findMany({
where: {
id: authCheck.scope.projectId,
},
});
return res.status(200).json({
data: projects.map((project) => ({
id: project.id,
name: project.name,
})),
});
} catch (error) {
console.error(error);
return res.status(500).json({ message: "Internal server error" });
}
} else {
console.error(
`Method not allowed for ${req.method} on /api/public/projects`,
);
return res.status(405).json({ message: "Method not allowed" });
}
}
+4
View File
@@ -14,6 +14,7 @@ import {
eventTypes,
} from "@/src/features/public-api/server/ingestion-api-schema";
import { v4 } from "uuid";
import { telemetry } from "@/src/features/telemetry";
const GetTracesSchema = z.object({
...paginationZod,
@@ -52,6 +53,9 @@ export default async function handler(
});
const body = TraceSchema.parse(req.body);
await telemetry();
const event = {
id: v4(),
type: eventTypes.TRACE_CREATE,
+75 -52
View File
@@ -21,6 +21,9 @@ import { useState } from "react";
import { useForm } from "react-hook-form";
import * as z from "zod";
import { usePostHog } from "posthog-js/react";
import { Divider } from "@tremor/react";
import { CloudPrivacyNotice } from "@/src/features/auth/components/AuthCloudPrivacyNotice";
import { CloudRegionSwitch } from "@/src/features/auth/components/AuthCloudRegionSwitch";
const credentialAuthForm = z.object({
email: z.string().email(),
@@ -29,7 +32,8 @@ const credentialAuthForm = z.object({
}),
});
type PageProps = {
// Also used in src/pages/auth/sign-up.tsx
export type PageProps = {
authProviders: {
credentials: boolean;
google: boolean;
@@ -37,6 +41,7 @@ type PageProps = {
};
};
// Also used in src/pages/auth/sign-up.tsx
// eslint-disable-next-line @typescript-eslint/require-await
export const getServerSideProps: GetServerSideProps<PageProps> = async () => {
return {
@@ -48,13 +53,60 @@ export const getServerSideProps: GetServerSideProps<PageProps> = async () => {
github:
env.AUTH_GITHUB_CLIENT_ID !== undefined &&
env.AUTH_GITHUB_CLIENT_SECRET !== undefined,
credentials: true,
credentials: env.AUTH_DISABLE_USERNAME_PASSWORD !== "true",
},
},
};
};
export default function SignIn(props: PageProps) {
// Also used in src/pages/auth/sign-up.tsx
export function SSOButtons({
authProviders,
action = "Sign in",
}: PageProps & { action?: string }) {
const posthog = usePostHog();
return (
// any authprovider from props is enanbles
Object.entries(authProviders).some(
([name, enabled]) => enabled && name !== "credentials",
) ? (
<div>
{authProviders.credentials ? (
<Divider className="text-gray-400" />
) : null}
<div className="flex flex-row flex-wrap items-center justify-center gap-4">
{authProviders.google ? (
<Button
onClick={() => {
posthog.capture("sign_in:google_button_click");
void signIn("google");
}}
variant="secondary"
>
<FcGoogle className="mr-3" size={18} />
{action} with Google
</Button>
) : null}
{authProviders.github ? (
<Button
onClick={() => {
posthog.capture("sign_in:github_button_click");
void signIn("github");
}}
variant="secondary"
>
<FaGithub className="mr-3" size={18} />
{action} with Github
</Button>
) : null}
</div>
</div>
) : null
);
}
export default function SignIn({ authProviders }: PageProps) {
const [credentialsFormError, setCredentialsFormError] = useState<
string | null
>(null);
@@ -98,12 +150,13 @@ export default function SignIn(props: PageProps) {
</h2>
</div>
<div className="mt-14 sm:mx-auto sm:w-full sm:max-w-[480px]">
<div className="divide-y bg-white p-6 py-6 shadow sm:rounded-lg sm:px-12">
{props.authProviders.credentials ? (
<div className="mt-14 bg-white px-6 py-10 shadow sm:mx-auto sm:w-full sm:max-w-[480px] sm:rounded-lg sm:px-12">
<div className="space-y-8">
<CloudRegionSwitch />
{authProviders.credentials ? (
<Form {...credentialsForm}>
<form
className="space-y-6 py-6"
className="space-y-6"
// eslint-disable-next-line @typescript-eslint/no-misused-promises
onSubmit={credentialsForm.handleSubmit(onCredentialsSubmit)}
>
@@ -150,53 +203,23 @@ export default function SignIn(props: PageProps) {
</form>
</Form>
) : null}
{
// any authprovider from props is enanbles
Object.values(props.authProviders).some((enabled) => enabled) ? (
<div className="flex flex-row flex-wrap items-center justify-center gap-4 py-6">
{props.authProviders.google ? (
<Button
onClick={() => {
posthog.capture("sign_in:google_button_click");
void signIn("google");
}}
variant="secondary"
>
<FcGoogle className="mr-3" size={18} />
Sign in with Google
</Button>
) : null}
{props.authProviders.github ? (
<Button
onClick={() => {
posthog.capture("sign_in:github_button_click");
void signIn("github");
}}
variant="secondary"
>
<FaGithub className="mr-3" size={18} />
Sign in with Github
</Button>
) : null}
</div>
) : null
}
<SSOButtons authProviders={authProviders} />
</div>
{env.NEXT_PUBLIC_SIGN_UP_DISABLED !== "true" &&
props.authProviders.credentials ? (
<p className="mt-10 text-center text-sm text-gray-500">
No account yet?{" "}
<Link
href="/auth/sign-up"
className="font-semibold leading-6 text-indigo-600 hover:text-indigo-500"
>
Sign up
</Link>
</p>
) : null}
<CloudPrivacyNotice action="signing in" />
</div>
{env.NEXT_PUBLIC_SIGN_UP_DISABLED !== "true" &&
authProviders.credentials ? (
<p className="mt-10 text-center text-sm text-gray-500">
No account yet?{" "}
<Link
href="/auth/sign-up"
className="font-semibold leading-6 text-indigo-600 hover:text-indigo-500"
>
Sign up
</Link>
</p>
) : null}
</div>
</>
);
+98 -120
View File
@@ -19,8 +19,14 @@ import { env } from "@/src/env.mjs";
import { useState } from "react";
import { LangfuseIcon } from "@/src/components/LangfuseLogo";
import { usePostHog } from "posthog-js/react";
import { CloudPrivacyNotice } from "@/src/features/auth/components/AuthCloudPrivacyNotice";
import { CloudRegionSwitch } from "@/src/features/auth/components/AuthCloudRegionSwitch";
import { SSOButtons, type PageProps } from "@/src/pages/auth/sign-in";
export default function SignIn() {
// Use the same getServerSideProps function as src/pages/auth/sign-in.tsx
export { getServerSideProps } from "@/src/pages/auth/sign-in";
export default function SignIn({ authProviders }: PageProps) {
const posthog = usePostHog();
const [formError, setFormError] = useState<string | null>(null);
const form = useForm<z.infer<typeof signupSchema>>({
@@ -84,130 +90,102 @@ export default function SignIn() {
</div>
{env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION !== undefined ? (
<div className="text-center sm:mx-auto sm:w-full sm:max-w-[480px]">
No credit card required. All users have access to a demo project.
No credit card required.
</div>
) : null}
<div className="mt-14 sm:mx-auto sm:w-full sm:max-w-[480px]">
<div className="bg-white px-6 py-12 shadow sm:rounded-lg sm:px-12">
<Form {...form}>
<form
className="space-y-6"
// eslint-disable-next-line @typescript-eslint/no-misused-promises
onSubmit={form.handleSubmit(onSubmit)}
>
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input placeholder="Jane Doe" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input placeholder="jsdoe@example.com" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>Password</FormLabel>
<FormControl>
<Input type="password" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION !== undefined ? (
<FormField
control={form.control}
name="referralSource"
render={({ field }) => (
<FormItem>
<FormLabel>
Where did you hear about us?{" "}
<span className="font-normal">(optional)</span>
</FormLabel>
<FormControl>
<Input type="referralSource" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
) : null}
<Button
type="submit"
className="w-full"
loading={form.formState.isSubmitting}
>
Sign up
</Button>
{formError ? (
<div className="text-center text-sm font-medium text-destructive">
{formError}
</div>
) : null}
</form>
</Form>
{env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION !== undefined ? (
<div className="-mb-4 mt-8 text-center text-xs text-gray-500">
By creating an account you are agreeing to our{" "}
<a
href="https://app.termly.io/document/terms-of-service/baf80a2e-dc67-46de-9ca8-2f7457179c32"
target="_blank"
rel="noopener noreferrer"
className="italic"
>
Terms of Service
</a>
,{" "}
<a
href="https://app.termly.io/document/privacy-policy/47905712-56e1-4ad0-9bb7-8958f3263f90"
rel="noopener noreferrer"
className="italic"
>
Privacy Policy
</a>
, and{" "}
<a
href="https://app.termly.io/document/cookie-policy/f97945a3-cb02-4db7-9370-c57023d92838"
rel="noopener noreferrer"
className="italic"
>
Cookie Policy
</a>
</div>
) : null}
</div>
<p className="mt-10 text-center text-sm text-gray-500">
Already have an account?{" "}
<Link
href="/auth/sign-in"
className="font-semibold leading-6 text-indigo-600 hover:text-indigo-500"
<div className="mt-14 bg-white px-6 py-10 shadow sm:mx-auto sm:w-full sm:max-w-[480px] sm:rounded-lg sm:px-12">
<CloudRegionSwitch isSignUpPage />
<Form {...form}>
<form
className="space-y-6"
// eslint-disable-next-line @typescript-eslint/no-misused-promises
onSubmit={form.handleSubmit(onSubmit)}
>
Sign in
</Link>
</p>
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input placeholder="Jane Doe" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input placeholder="jsdoe@example.com" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>Password</FormLabel>
<FormControl>
<Input type="password" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION !== undefined ? (
<FormField
control={form.control}
name="referralSource"
render={({ field }) => (
<FormItem>
<FormLabel>
Where did you hear about us?{" "}
<span className="font-normal">(optional)</span>
</FormLabel>
<FormControl>
<Input type="referralSource" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
) : null}
<Button
type="submit"
className="w-full"
loading={form.formState.isSubmitting}
>
Sign up
</Button>
{formError ? (
<div className="text-center text-sm font-medium text-destructive">
{formError}
</div>
) : null}
</form>
</Form>
<SSOButtons authProviders={authProviders} action="Sign up" />
<CloudPrivacyNotice action="creating an account" />
</div>
<p className="mt-10 text-center text-sm text-gray-500">
Already have an account?{" "}
<Link
href="/auth/sign-in"
className="font-semibold leading-6 text-indigo-600 hover:text-indigo-500"
>
Sign in
</Link>
</p>
</div>
</>
);
+16 -29
View File
@@ -9,47 +9,34 @@ import {
import Link from "next/link";
import { NewProjectButton } from "@/src/features/projects/components/NewProjectButton";
import Header from "@/src/components/layouts/header";
import { api } from "@/src/utils/api";
import { useRouter } from "next/router";
import { useState, useEffect } from "react";
import { env } from "@/src/env.mjs";
import { cn } from "@/src/utils/tailwind";
import { useSession } from "next-auth/react";
import { Spinner } from "@/src/components/layouts/spinner";
export default function GetStartedPage() {
const projects = api.projects.all.useQuery();
const router = useRouter();
const [loading, setLoading] = useState(true);
// check url query for welcome = 1
const getStarted = router.query.getStarted === "1";
useEffect(() => {
if (projects.data) {
if (
projects.data.filter((p) => p.id !== env.NEXT_PUBLIC_DEMO_PROJECT_ID)
.length > 0 &&
loading === true &&
!getStarted
)
void router.push(
`/project/${
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
projects.data.filter(
(p) => p.id !== env.NEXT_PUBLIC_DEMO_PROJECT_ID,
)[0]!.id
}`,
);
else setLoading(false);
}
}, [projects.data, router, loading, getStarted]);
const session = useSession();
const projects = session.data?.user?.projects;
const redirectProject = projects?.filter(
(p) => p.id !== env.NEXT_PUBLIC_DEMO_PROJECT_ID,
)[0];
if (loading || projects.status === "loading") {
return <div>Loading...</div>;
if (session.status === "authenticated" && redirectProject && !getStarted) {
void router.push(`/project/${redirectProject.id}`);
return <Spinner message="Redirecting" />;
}
if (session.status === "loading") {
return <Spinner message="Loading" />;
}
const demoProject =
env.NEXT_PUBLIC_DEMO_PROJECT_ID !== undefined && projects.data?.length
? projects.data.find(
env.NEXT_PUBLIC_DEMO_PROJECT_ID !== undefined
? projects?.find(
(project) => project.id === env.NEXT_PUBLIC_DEMO_PROJECT_ID,
)
: undefined;
+5 -1
View File
@@ -8,6 +8,7 @@ import { Code, Bird, GraduationCap } from "lucide-react";
import { ProjectMembersTable } from "@/src/features/rbac/components/ProjectMembersTable";
import { DeleteProjectButton } from "@/src/features/projects/components/DeleteProjectButton";
import { ProjectUsageChart } from "@/src/features/usage-metering/ProjectUsageChart";
import { TransferOwnershipButton } from "@/src/features/projects/components/TransferOwnershipButton";
export default function SettingsPage() {
const router = useRouter();
@@ -20,7 +21,10 @@ export default function SettingsPage() {
<ApiKeyList projectId={projectId} />
<ProjectUsageChart projectId={projectId} />
<Instructions />
<DeleteProjectButton projectId={projectId} />
<div className="space-y-3">
<DeleteProjectButton projectId={projectId} />
<TransferOwnershipButton projectId={projectId} />
</div>
</div>
</div>
);
+8 -2
View File
@@ -2,10 +2,16 @@ import { z } from "zod";
export const filterOperators = {
datetime: [">", "<", ">=", "<="],
string: ["="],
string: ["=", "contains", "does not contain", "starts with", "ends with"],
stringOptions: ["any of", "none of"],
number: ["=", ">", "<", ">=", "<="],
stringObject: ["="],
stringObject: [
"=",
"contains",
"does not contain",
"starts with",
"ends with",
],
numberObject: ["=", ">", "<", ">=", "<="],
} as const;
+37 -3
View File
@@ -23,6 +23,7 @@ import {
datetimeFilterToPrismaSql,
filterToPrismaSql,
} from "@/src/features/filters/server/filterToPrisma";
import { throwIfNoAccess } from "@/src/features/rbac/utils/checkAccess";
const TraceFilterOptions = z.object({
projectId: z.string(), // Required for protectedProjectProcedure
@@ -246,9 +247,9 @@ export const traceRouter = createTRPCRouter({
? (obsEndTimes[obsEndTimes.length - 1] as Date).getTime() -
obsStartTimes[0]!.getTime()
: obsStartTimes.length > 1
? obsStartTimes[obsStartTimes.length - 1]!.getTime() -
obsStartTimes[0]!.getTime()
: undefined
? obsStartTimes[obsStartTimes.length - 1]!.getTime() -
obsStartTimes[0]!.getTime()
: undefined
: undefined;
const enrichedObservations = observations.map(
@@ -275,4 +276,37 @@ export const traceRouter = createTRPCRouter({
observations: enrichedObservations as ObservationReturnType[],
};
}),
delete: protectedProjectProcedure
.input(z.object({ traceId: z.string(), projectId: z.string() }))
.mutation(async ({ input, ctx }) => {
throwIfNoAccess({
session: ctx.session,
projectId: input.projectId,
scope: "traces:delete",
});
const trace = await ctx.prisma.trace.findFirst({
where: {
id: input.traceId,
projectId: input.projectId,
},
});
if (!trace) {
throw new Error("Trace not found in project");
}
return ctx.prisma.$transaction([
ctx.prisma.trace.delete({
where: {
id: input.traceId,
},
}),
ctx.prisma.observation.deleteMany({
where: {
traceId: input.traceId,
},
}),
]);
}),
});
+22 -4
View File
@@ -292,14 +292,32 @@ export class ScoreProcessor implements EventProcessor {
if (!accessCheck)
throw new AuthenticationError("Access denied for score creation");
return await prisma.score.create({
data: {
id: body.id ?? v4(),
const id = body.id ?? v4();
// access control via traceId
return await prisma.score.upsert({
where: {
id_traceId: {
id,
traceId: body.traceId,
},
},
create: {
id,
trace: { connect: { id: body.traceId } },
timestamp: new Date(),
value: body.value,
name: body.name,
comment: body.comment,
...(body.observationId && {
observation: { connect: { id: body.observationId } },
}),
},
update: {
timestamp: new Date(),
value: body.value,
name: body.name,
comment: body.comment,
trace: { connect: { id: body.traceId } },
...(body.observationId && {
observation: { connect: { id: body.observationId } },
}),
+4
View File
@@ -52,6 +52,10 @@ const providers: Provider[] = [
},
async authorize(credentials, _req) {
if (!credentials) throw new Error("No credentials");
if (env.AUTH_DISABLE_USERNAME_PASSWORD === "true")
throw new Error(
"Sign in with email and password is disabled for this instance. Please use SSO.",
);
const blockedDomains =
env.AUTH_DOMAINS_WITH_SSO_ENFORCEMENT?.split(",") ?? [];