Compare commits

...
141 Commits
Author SHA1 Message Date
Marc Klingen 9b76029686 chore: release v1.13.2
CI/CD / push-docker-image (push) Blocked by required conditions
CI/CD / tests (18) (push) Failing after 1m5s
CI/CD / tests (20) (push) Failing after 1m2s
CI/CD / e2e-tests (push) Failing after 1m1s
CI/CD / lint (push) Successful in 4m13s
CI/CD / all-ci-passed (push) Waiting to run
release.yml / release (push) Failing after 3s
2023-12-05 20:09:28 +01:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
d6902e124d chore(deps): bump the patches group with 8 updates (#638)
Bumps the patches group with 8 updates:

| Package | From | To |
| --- | --- | --- |
| [posthog-js](https://github.com/PostHog/posthog-js) | `1.93.3` | `1.93.6` |
| [ts-pattern](https://github.com/gvergnaud/ts-pattern) | `5.0.5` | `5.0.6` |
| [@types/react](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react) | `18.2.40` | `18.2.42` |
| [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) | `6.13.1` | `6.13.2` |
| [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) | `6.13.1` | `6.13.2` |
| [postcss](https://github.com/postcss/postcss) | `8.4.31` | `8.4.32` |
| [prettier-plugin-tailwindcss](https://github.com/tailwindlabs/prettier-plugin-tailwindcss) | `0.5.7` | `0.5.8` |
| [tailwindcss](https://github.com/tailwindlabs/tailwindcss) | `3.3.5` | `3.3.6` |


Updates `posthog-js` from 1.93.3 to 1.93.6
- [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.93.3...v1.93.6)

Updates `ts-pattern` from 5.0.5 to 5.0.6
- [Release notes](https://github.com/gvergnaud/ts-pattern/releases)
- [Commits](https://github.com/gvergnaud/ts-pattern/compare/v5.0.5...v5.0.6)

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

Updates `@typescript-eslint/eslint-plugin` from 6.13.1 to 6.13.2
- [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.13.2/packages/eslint-plugin)

Updates `@typescript-eslint/parser` from 6.13.1 to 6.13.2
- [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.13.2/packages/parser)

Updates `postcss` from 8.4.31 to 8.4.32
- [Release notes](https://github.com/postcss/postcss/releases)
- [Changelog](https://github.com/postcss/postcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/postcss/postcss/compare/8.4.31...8.4.32)

Updates `prettier-plugin-tailwindcss` from 0.5.7 to 0.5.8
- [Release notes](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/releases)
- [Changelog](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/compare/v0.5.7...v0.5.8)

Updates `tailwindcss` from 3.3.5 to 3.3.6
- [Release notes](https://github.com/tailwindlabs/tailwindcss/releases)
- [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/master/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/tailwindcss/compare/v3.3.5...v3.3.6)

---
updated-dependencies:
- dependency-name: posthog-js
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: patches
- dependency-name: ts-pattern
  dependency-type: direct:production
  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: "@typescript-eslint/eslint-plugin"
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: patches
- dependency-name: "@typescript-eslint/parser"
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: patches
- dependency-name: postcss
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: patches
- dependency-name: prettier-plugin-tailwindcss
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: patches
- dependency-name: tailwindcss
  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-12-05 16:57:34 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
075deb99c2 chore(deps): bump tailwind-merge from 2.0.0 to 2.1.0 (#632)
Bumps [tailwind-merge](https://github.com/dcastil/tailwind-merge) from 2.0.0 to 2.1.0.
- [Release notes](https://github.com/dcastil/tailwind-merge/releases)
- [Commits](https://github.com/dcastil/tailwind-merge/compare/v2.0.0...v2.1.0)

---
updated-dependencies:
- dependency-name: tailwind-merge
  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-12-05 16:30:18 +00:00
Marc KlingenandGitHub 6da21c6c22 refactor: sequential database access (#637) 2023-12-05 16:23:33 +00:00
Marc Klingen 59ef3e898e Merge branch 'main' of github.com:langfuse/langfuse 2023-12-05 16:50:07 +01:00
Marc Klingen c737447a83 chore: add middleware 2023-12-05 16:50:03 +01:00
Max Deichmann 474a1e28ff chore: release v1.13.1
CI/CD / push-docker-image (push) Blocked by required conditions
CI/CD / tests (18) (push) Failing after 1m16s
CI/CD / tests (20) (push) Failing after 1m7s
CI/CD / e2e-tests (push) Failing after 1m2s
CI/CD / lint (push) Successful in 4m22s
CI/CD / all-ci-passed (push) Waiting to run
release.yml / release (push) Failing after 3s
2023-12-04 17:34:25 +01:00
Max DeichmannandGitHub a0b84773dc fix(ingestion): filter NULL string values from ingested events (#628) 2023-12-04 17:23:04 +01:00
Marc Klingen 00547bf93d chore: release v1.13.0
CI/CD / push-docker-image (push) Blocked by required conditions
CI/CD / tests (18) (push) Failing after 1m17s
CI/CD / tests (20) (push) Failing after 1m1s
CI/CD / e2e-tests (push) Failing after 1m7s
CI/CD / lint (push) Successful in 4m23s
CI/CD / all-ci-passed (push) Waiting to run
release.yml / release (push) Failing after 3s
2023-12-04 13:22:22 +01:00
Marc Klingen feb964f12b fix(ui): move column selector to the right of the toolbar 2023-12-04 12:57:33 +01:00
Max DeichmannandGitHub be72460af2 fix: remove custom hook for column selector (#627) 2023-12-04 10:57:25 +00:00
Marc KlingenandGitHub d5a86d394c feat(api): publish trace (#626) 2023-12-04 09:30:42 +00:00
Max DeichmannGitHub@RichardKruemmel
4e89d7d65a feature: configure table column visibility (#623) (#624)
Co-authored-by: @RichardKruemmel
2023-12-04 10:09:14 +01:00
Marc Klingen 28b7eab060 chore: release v1.12.0
CI/CD / push-docker-image (push) Blocked by required conditions
CI/CD / tests (18) (push) Failing after 1m18s
CI/CD / tests (20) (push) Failing after 57s
CI/CD / e2e-tests (push) Failing after 1m8s
CI/CD / lint (push) Successful in 4m21s
CI/CD / all-ci-passed (push) Waiting to run
release.yml / release (push) Failing after 4s
2023-12-03 01:00:37 +01:00
Marc KlingenandGitHub 2ccd140e9a feat(ui): copy formatted json (#621)
Fixes #617
2023-12-02 22:31:59 +00:00
Marc Klingen d47a4d67d4 chore: release v1.11.1
CI/CD / push-docker-image (push) Blocked by required conditions
CI/CD / tests (18) (push) Failing after 1m18s
CI/CD / tests (20) (push) Failing after 1m3s
CI/CD / e2e-tests (push) Failing after 1m7s
CI/CD / lint (push) Successful in 4m14s
CI/CD / all-ci-passed (push) Waiting to run
release.yml / release (push) Failing after 3s
2023-12-02 01:20:59 +01:00
Marc Klingen e20ad85a13 fix(ui): main menu css 2023-12-02 01:20:26 +01:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
1f3beabbdf chore(deps-dev): bump eslint from 8.54.0 to 8.55.0 (#620)
Bumps [eslint](https://github.com/eslint/eslint) from 8.54.0 to 8.55.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.54.0...v8.55.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-12-01 23:11:44 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2353c90cc1 chore(deps): bump the patches group with 3 updates (#619)
Bumps the patches group with 3 updates: [@react-email/components](https://github.com/resendlabs/react-email/tree/HEAD/packages/components), [@react-email/render](https://github.com/resendlabs/react-email/tree/HEAD/packages/render) and [@types/react](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react).


Updates `@react-email/components` from 0.0.11 to 0.0.12
- [Release notes](https://github.com/resendlabs/react-email/releases)
- [Changelog](https://github.com/resendlabs/react-email/blob/canary/docs/changelog.mdx)
- [Commits](https://github.com/resendlabs/react-email/commits/v0.0.12/packages/components)

Updates `@react-email/render` from 0.0.9 to 0.0.10
- [Release notes](https://github.com/resendlabs/react-email/releases)
- [Changelog](https://github.com/resendlabs/react-email/blob/canary/docs/changelog.mdx)
- [Commits](https://github.com/resendlabs/react-email/commits/HEAD/packages/render)

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

---
updated-dependencies:
- dependency-name: "@react-email/components"
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: patches
- dependency-name: "@react-email/render"
  dependency-type: direct:production
  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
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-02 00:07:41 +01:00
Marc Klingen df6c0cfa8a chore: release v1.11.0
CI/CD / push-docker-image (push) Blocked by required conditions
CI/CD / tests (18) (push) Failing after 1m17s
CI/CD / tests (20) (push) Failing after 1m7s
CI/CD / e2e-tests (push) Failing after 57s
CI/CD / lint (push) Successful in 4m21s
CI/CD / all-ci-passed (push) Waiting to run
release.yml / release (push) Failing after 4s
2023-12-01 23:48:00 +01:00
Marc KlingenandGitHub 2e8a9062f4 feat: improved rbac error message (#618) 2023-12-01 22:22:50 +00:00
7cc52b3c8e feat(ui): keep logo static on large screens while scrolling the main menu (#571)
---------

Co-authored-by: Marc Klingen <git@marcklingen.com>
2023-12-01 22:45:23 +01:00
Marc KlingenandGitHub 92d2def19f docs: update CONTRIBUTING.md 2023-12-01 21:16:46 +01:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
3cd7f6b3a9 chore(deps-dev): bump @adobe/css-tools from 4.3.1 to 4.3.2 (#614)
Bumps [@adobe/css-tools](https://github.com/adobe/css-tools) from 4.3.1 to 4.3.2.
- [Changelog](https://github.com/adobe/css-tools/blob/main/History.md)
- [Commits](https://github.com/adobe/css-tools/commits)

---
updated-dependencies:
- dependency-name: "@adobe/css-tools"
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-01 19:54:22 +00:00
Marc KlingenandGitHub 65c4fbf9e8 fix: improved state during trace deletion (#615) 2023-12-01 17:24:28 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
5c5accf4f8 chore(deps-dev): bump the patches group with 2 updates (#609)
Bumps the patches group with 2 updates: [@testing-library/jest-dom](https://github.com/testing-library/jest-dom) and [@types/eslint](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/eslint).


Updates `@testing-library/jest-dom` from 6.1.4 to 6.1.5
- [Release notes](https://github.com/testing-library/jest-dom/releases)
- [Changelog](https://github.com/testing-library/jest-dom/blob/main/CHANGELOG.md)
- [Commits](https://github.com/testing-library/jest-dom/compare/v6.1.4...v6.1.5)

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

---
updated-dependencies:
- dependency-name: "@testing-library/jest-dom"
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: patches
- dependency-name: "@types/eslint"
  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-12-01 17:11:36 +00:00
5bfb02b48a feat(auth): Add access to default project upon sign-up (#611)
* Add access to default project upon sign-up

* refactor: createProjectMembershipsOnSignup

---------

Co-authored-by: Marc Klingen <git@marcklingen.com>
2023-12-01 18:00:48 +01:00
Marc KlingenandGitHub e0650469c8 chore: remove next-swagger-doc (#612) 2023-12-01 01:54:50 +00:00
b20d4da827 feat: add html email template for project invitations (#601)
---------

Co-authored-by: Marc Klingen <git@marcklingen.com>
2023-12-01 02:14:20 +01:00
Marc Klingen 029577bb0d chore: release v1.10.0
CI/CD / push-docker-image (push) Blocked by required conditions
CI/CD / tests (20) (push) Failing after 56s
CI/CD / tests (18) (push) Failing after 1m9s
CI/CD / e2e-tests (push) Failing after 56s
release.yml / release (push) Failing after 2s
CI/CD / lint (push) Successful in 4m30s
CI/CD / all-ci-passed (push) Waiting to run
2023-11-30 23:48:42 +01:00
Marc KlingenandGitHub 86851322c9 feat(api): upsert and get dataset items (#606) 2023-11-30 22:44:50 +00:00
Marc KlingenandGitHub cd665d8fd5 fix(ui): extend checks to not trigger keyboard shortcuts when editing inputs (#607) 2023-11-30 21:26:12 +00:00
Marc Klingen f547fd710c Revert "chore(deps): bump sentry"
This reverts commit da7699fa68.
2023-11-30 22:21:34 +01:00
Marc Klingen da7699fa68 chore(deps): bump sentry 2023-11-30 22:17:26 +01:00
Max Deichmann b4a4f5683e chore: release v1.9.2
CI/CD / push-docker-image (push) Blocked by required conditions
CI/CD / tests (18) (push) Failing after 50s
CI/CD / tests (20) (push) Failing after 38s
CI/CD / e2e-tests (push) Failing after 1m5s
CI/CD / lint (push) Successful in 4m1s
CI/CD / all-ci-passed (push) Waiting to run
release.yml / release (push) Failing after 2s
2023-11-30 13:51:28 +01:00
Max DeichmannandGitHub 6ed893de18 fix(core): add start time to patches of ingestion events (#605) 2023-11-30 13:50:33 +01:00
Max Deichmann 92e834e490 chore: release v1.9.1
CI/CD / push-docker-image (push) Blocked by required conditions
CI/CD / tests (20) (push) Failing after 36s
CI/CD / tests (18) (push) Failing after 50s
CI/CD / e2e-tests (push) Failing after 36s
CI/CD / lint (push) Successful in 3m56s
CI/CD / all-ci-passed (push) Waiting to run
release.yml / release (push) Failing after 3s
2023-11-30 11:48:01 +01:00
Max DeichmannandGitHub ed9d647d31 fix(core): add logging to trace router (#603) 2023-11-30 10:41:49 +00:00
Marc Klingen 58f807b7de feat(cloud): demo project link to q&a chatbot 2023-11-29 13:07:19 +01:00
Veniamin ViflyantsevandGitHub 2a623ba65b fix: NEXTAUTH_URL environment variable in docker compose files (#596) 2023-11-29 11:48:08 +01:00
Marc Klingen de7919b7ec fix: auth url on Langfuse Cloud (staging) 2023-11-29 04:15:33 +01:00
Marc Klingen 88f96a37c1 fix: auth url on Langfuse Cloud 2023-11-29 04:12:57 +01:00
Marc Klingen e4f58de9ba chore: release v1.9.0
CI/CD / push-docker-image (push) Blocked by required conditions
CI/CD / tests (18) (push) Failing after 48s
CI/CD / e2e-tests (push) Failing after 35s
CI/CD / tests (20) (push) Failing after 42s
CI/CD / lint (push) Successful in 3m57s
CI/CD / all-ci-passed (push) Waiting to run
release.yml / release (push) Failing after 3s
2023-11-29 03:50:43 +01:00
Marc Klingen c00c1833dd chore(deps): bump 2023-11-29 03:47:46 +01:00
Marc Klingen 8657c61bb8 fix: remove user invite alert 2023-11-29 03:33:15 +01:00
144aada5ec feat(auth): invite users to a project that do not have an account yet (#574)
---------

Co-authored-by: Marc Klingen <git@marcklingen.com>
2023-11-29 03:17:25 +01:00
Marc Klingen 7e6ce12ca8 chore: release v1.8.2
CI/CD / push-docker-image (push) Blocked by required conditions
CI/CD / tests (18) (push) Failing after 36s
CI/CD / tests (20) (push) Failing after 34s
CI/CD / e2e-tests (push) Failing after 39s
CI/CD / lint (push) Successful in 4m18s
CI/CD / all-ci-passed (push) Waiting to run
release.yml / release (push) Failing after 3s
2023-11-29 01:48:35 +01:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
951b89c08a chore(deps): bump the patches group with 3 updates (#590)
Bumps the patches group with 3 updates: [posthog-js](https://github.com/PostHog/posthog-js), [@playwright/test](https://github.com/microsoft/playwright) and [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin).


Updates `posthog-js` from 1.93.2 to 1.93.3
- [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.93.2...v1.93.3)

Updates `@playwright/test` from 1.40.0 to 1.40.1
- [Release notes](https://github.com/microsoft/playwright/releases)
- [Commits](https://github.com/microsoft/playwright/compare/v1.40.0...v1.40.1)

Updates `@typescript-eslint/eslint-plugin` from 6.13.0 to 6.13.1
- [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.13.1/packages/eslint-plugin)

---
updated-dependencies:
- dependency-name: posthog-js
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: patches
- dependency-name: "@playwright/test"
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: patches
- dependency-name: "@typescript-eslint/eslint-plugin"
  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-29 00:34:57 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
70f9f3b50a chore(deps-dev): bump @typescript-eslint/parser from 6.12.0 to 6.13.1 (#592)
Bumps [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) from 6.12.0 to 6.13.1.
- [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.13.1/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-29 00:25:54 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2a90e548a6 chore(deps): bump @sentry/nextjs from 7.82.0 to 7.83.0 (#593)
Bumps [@sentry/nextjs](https://github.com/getsentry/sentry-javascript) from 7.82.0 to 7.83.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.82.0...7.83.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-29 00:21:31 +00:00
Marc Klingen 7797982b52 fix(ui): dom nesting of DocPopup 2023-11-29 01:16:55 +01:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
9858c139bc chore(deps): bump lucide-react from 0.293.0 to 0.294.0 (#591)
Bumps [lucide-react](https://github.com/lucide-icons/lucide/tree/HEAD/packages/lucide-react) from 0.293.0 to 0.294.0.
- [Release notes](https://github.com/lucide-icons/lucide/releases)
- [Commits](https://github.com/lucide-icons/lucide/commits/0.294.0/packages/lucide-react)

---
updated-dependencies:
- dependency-name: lucide-react
  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-29 00:49:48 +01:00
Marc Klingen 05472524f3 docs: add note that staging env is sometimes reset 2023-11-29 00:01:17 +01:00
Marc Klingen 27e5eb5feb refactor: simplify DocPopup 2023-11-28 23:54:38 +01:00
Marc KlingenandGitHub 03062c3fd5 feat(ui): project name in breadcrumb (#589) 2023-11-28 22:42:02 +00:00
Marc Klingen fe40d8b139 chore: release v1.8.1
CI/CD / push-docker-image (push) Blocked by required conditions
CI/CD / tests (18) (push) Failing after 41s
CI/CD / tests (20) (push) Failing after 36s
CI/CD / e2e-tests (push) Failing after 37s
CI/CD / lint (push) Successful in 3m50s
CI/CD / all-ci-passed (push) Waiting to run
release.yml / release (push) Failing after 4s
2023-11-28 03:47:00 +01:00
Marc Klingen 81e9ac0cea feat: debug log for auth errors 2023-11-28 03:46:24 +01:00
Marc Klingen 9bb4a55a4c chore: release v1.8.0
CI/CD / push-docker-image (push) Blocked by required conditions
CI/CD / tests (18) (push) Failing after 44s
CI/CD / e2e-tests (push) Failing after 37s
CI/CD / tests (20) (push) Failing after 1m10s
CI/CD / lint (push) Successful in 3m51s
CI/CD / all-ci-passed (push) Waiting to run
release.yml / release (push) Failing after 2s
2023-11-28 02:54:32 +01:00
Marc KlingenandGitHub f4e7d5f3e9 feat: cache bookmark state in component (#586) 2023-11-28 01:52:07 +00:00
Marc Klingen cc0c0c9246 fix: hide demo banner on staging 2023-11-28 02:26:34 +01:00
93c080813e feat(ui): bookmark traces (#579)
---------

Co-authored-by: Marc Klingen <git@marcklingen.com>
2023-11-28 02:00:12 +01:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
926a5195c6 chore(deps): bump next-query-params from 4.3.0 to 5.0.0 (#583)
Bumps [next-query-params](https://github.com/amannn/next-query-params) from 4.3.0 to 5.0.0.
- [Changelog](https://github.com/amannn/next-query-params/blob/main/CHANGELOG.md)
- [Commits](https://github.com/amannn/next-query-params/compare/v4.3.0...v5.0.0)

---
updated-dependencies:
- dependency-name: next-query-params
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-11-27 23:54:21 +00:00
Marc KlingenandGitHub 4b8253f452 chore: remove dependabot-merge workflow 2023-11-28 00:50:32 +01:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
fe3e8eea5f chore(deps): bump @sentry/nextjs from 7.81.1 to 7.82.0 (#584)
Bumps [@sentry/nextjs](https://github.com/getsentry/sentry-javascript) from 7.81.1 to 7.82.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.81.1...7.82.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-27 23:46:37 +00:00
Marc KlingenandGitHub 26cc72488b refactor: use protectedProjectProcedure on most routes (#585) 2023-11-27 23:43:00 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
0e53210cd4 chore(deps): bump lucide-react from 0.292.0 to 0.293.0 (#570)
Bumps [lucide-react](https://github.com/lucide-icons/lucide/tree/HEAD/packages/lucide-react) from 0.292.0 to 0.293.0.
- [Release notes](https://github.com/lucide-icons/lucide/releases)
- [Commits](https://github.com/lucide-icons/lucide/commits/0.293.0/packages/lucide-react)

---
updated-dependencies:
- dependency-name: lucide-react
  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-27 23:39:58 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
47597077f6 chore(deps-dev): bump @typescript-eslint/eslint-plugin from 6.12.0 to 6.13.0 (#582)
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.12.0 to 6.13.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.13.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-27 23:36:19 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
c21786c647 chore(deps-dev): bump the patches group with 1 update (#581)
Bumps the patches group with 1 update: [@types/react](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react).

- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react)

---
updated-dependencies:
- dependency-name: "@types/react"
  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-28 00:32:19 +01:00
Marc Klingen b579ac547c docs: typo 2023-11-27 20:28:43 +01:00
Marc Klingen ce0d1bfb0a docs: production environment to contributing.md 2023-11-27 20:21:02 +01:00
18491f3eec chore: license check for dependencies in GH action (#575)
---------

Co-authored-by: Marc Klingen <git@marcklingen.com>
2023-11-26 16:35:42 +01:00
Marc Klingen cb394a1bd9 chore: release v1.7.2
CI/CD / push-docker-image (push) Blocked by required conditions
CI/CD / tests (20) (push) Failing after 36s
CI/CD / tests (18) (push) Failing after 1m1s
CI/CD / e2e-tests (push) Failing after 39s
release.yml / release (push) Failing after 15s
CI/CD / lint (push) Successful in 4m39s
CI/CD / all-ci-passed (push) Waiting to run
2023-11-26 05:37:59 +01:00
Marc Klingen 41b44d6c09 fix: ci to release on semver bump 2023-11-26 05:37:42 +01:00
Marc Klingen 678dbaf3d0 fix: github access token 2023-11-26 05:33:42 +01:00
Marc Klingen 0b1b0aca11 chore: release v1.7.1
CI/CD / push-docker-image (push) Blocked by required conditions
CI/CD / tests (18) (push) Failing after 42s
CI/CD / tests (20) (push) Failing after 43s
CI/CD / lint (push) Successful in 3m49s
release.yml / release (push) Failing after 4s
CI/CD / e2e-tests (push) Failing after 15m49s
CI/CD / all-ci-passed (push) Waiting to run
2023-11-26 05:29:05 +01:00
Marc Klingen d5c117cf5d feat: support for staging environment 2023-11-26 05:11:47 +01:00
Marc KlingenandGitHub c380d16feb docs: staging environment in CONTRIBUTING.md (#576) 2023-11-26 04:58:09 +01:00
Marc Klingen 6b03507ca0 ci: use "protected branches" environment 2023-11-26 04:44:28 +01:00
Marc Klingen ff8a8b8ee6 ci: push 2023-11-26 04:32:51 +01:00
Marc Klingen d3981125a1 ci: next try 2023-11-26 04:15:48 +01:00
Marc Klingen b4965d386e ci: try GITHUB_TOKEN 2023-11-26 04:09:08 +01:00
Marc Klingen bda28bd385 ci: release tagged versions to production branch 2023-11-26 04:00:34 +01:00
Marc Klingen edfb90c6c7 feat(compliance): data accuracy notice on sign up 2023-11-25 20:23:27 +01:00
Marc Klingen a1e3762b9d docs: add CI/CD to CONTRIBUTING.md 2023-11-25 16:04:18 +01:00
Marc Klingen ea54da12ee fix(ci): wait for postgres to accept connections in e2e 2023-11-25 15:27:43 +01:00
Marc Klingen d552d61cfd fix(ci): wait for postgres to accept connections 2023-11-25 15:25:29 +01:00
Marc Klingen 6332418cb1 docs: CONTRIBUTING.md 2023-11-25 15:13:29 +01:00
Marc KlingenandGitHub 824c37c227 ci: add matrix tests for node 18 & 20 (#573)
Node 20 is curently in beta for serverless functions on Vercel (AWS Lambda)
2023-11-25 13:58:35 +00:00
1c116282d7 chore: upgrade to Node 20 (#553)
---------

Co-authored-by: Marc Klingen <git@marcklingen.com>
2023-11-25 14:49:42 +01:00
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
137 changed files with 6024 additions and 2835 deletions
+5 -1
View File
@@ -16,4 +16,8 @@ NEXTAUTH_SECRET="secret"
# Langfuse experimental features
LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES="true"
SALT="salt"
SALT="salt"
# Email
EMAIL_FROM_ADDRESS="" # Defines the email address to use as the from address.
SMTP_CONNECTION_URL="" # Defines the connection url for smtp server.
+6 -1
View File
@@ -13,4 +13,9 @@ NEXTAUTH_URL="http://localhost:3000"
# feature flag to enable experimental features locally
LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES="false"
SALT="salt"
SALT="salt"
# Email
EMAIL_FROM_ADDRESS="" # Defines the email address to use as the from address.
SMTP_CONNECTION_URL="" # Defines the connection url for smtp server.
+6 -1
View File
@@ -42,4 +42,9 @@ AUTH_GOOGLE_CLIENT_ID=
AUTH_GOOGLE_CLIENT_SECRET=
AUTH_GITHUB_CLIENT_ID=
AUTH_GITHUB_CLIENT_SECRET=
# AUTH_DOMAINS_WITH_SSO_ENFORCEMENT=domain1.com,domain2.com
# AUTH_DOMAINS_WITH_SSO_ENFORCEMENT=domain1.com,domain2.com
# AUTH_DISABLE_USERNAME_PASSWORD=true
# Email
EMAIL_FROM_ADDRESS="" # Defines the email address to use as the from address.
SMTP_CONNECTION_URL="" # Defines the connection url for smtp server.
+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"
-26
View File
@@ -1,26 +0,0 @@
name: Dependabot auto-merge
on:
workflow_dispatch:
pull_request:
branches: [main]
permissions:
contents: write
pull-requests: write
jobs:
dependabot:
runs-on: ubuntu-latest
if: ${{ github.actor == 'dependabot[bot]' }}
steps:
- name: Dependabot metadata
id: metadata
uses: dependabot/fetch-metadata@v1
with:
github-token: "${{ secrets.GITHUB_TOKEN }}"
- name: Enable auto-merge for Dependabot PRs
if: ${{steps.metadata.outputs.update-type == 'version-update:semver-patch'}}
run: gh pr merge --auto --squash "$PR_URL"
env:
PR_URL: ${{github.event.pull_request.html_url}}
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
@@ -9,9 +9,10 @@ on:
jobs:
rebase-dependabot:
runs-on: ubuntu-latest
environment: "protected branches"
steps:
- name: "Rebase open Dependabot PR"
uses: orange-buffalo/dependabot-auto-rebase@v1
with:
api-token: ${{ secrets.DEP_REBASE_PAT }}
api-token: ${{ secrets.GH_ACCESS_TOKEN }}
repository: ${{ github.repository }}
+48
View File
@@ -0,0 +1,48 @@
name: License Compliance Check
on:
workflow_dispatch:
push:
branches:
- "main"
pull_request:
branches:
- "main"
jobs:
license_check:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Setup node
uses: actions/setup-node@v2
with:
node-version: 18
- name: Install license-checker
run: npm install -g license-checker
- name: Install yui-lint
run: npm install yui-lint
- name: Generate license-checker CSV file
run: license-checker --production --csv > npm-license-checker.csv
- name: Check license-checker CSV file without headers
id: license_check_report
uses: pilosus/action-pip-license-checker@v2
with:
external: "npm-license-checker.csv"
external-format: "csv"
external-options: "{:skip-header true}"
fail: "WeakCopyleft,StrongCopyleft,NetworkCopyleft"
fails-only: true
totals: true
verbose: 1
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Echo error
if: failure()
run: echo "::error::${{ steps.license_check_report.outputs.report }}"
- name: Delete license-checker CSV file
run: rm npm-license-checker.csv
+12 -5
View File
@@ -18,7 +18,7 @@ jobs:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 18
node-version: 20
cache: "npm"
- name: install dependencies
run: |
@@ -31,11 +31,15 @@ jobs:
tests:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [18, 20]
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with:
node-version: 18
node-version: ${{ matrix.node-version }}
cache: "npm"
- name: install dependencies
@@ -49,6 +53,7 @@ jobs:
- name: Run, migrate, seed DB
run: |
docker-compose -f docker-compose.dev.yml up -d
sleep 5 # Wait for PostgreSQL to accept connections
npx --yes prisma migrate reset --force --skip-generate
- name: Build
@@ -66,7 +71,7 @@ jobs:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 18
node-version: 20
cache: "npm"
- name: install dependencies
@@ -80,6 +85,7 @@ jobs:
- name: Run, migrate, seed DB
run: |
docker-compose -f docker-compose.dev.yml up -d
sleep 5 # Wait for PostgreSQL to accept connections
npx --yes prisma migrate reset --force --skip-generate
- name: Build
@@ -107,6 +113,7 @@ jobs:
push-docker-image:
needs: all-ci-passed
if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/'))
environment: "protected branches"
runs-on: ubuntu-latest
env:
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/postgres
@@ -123,7 +130,7 @@ jobs:
- name: Setup node
uses: actions/setup-node@v3
with:
node-version: 18
node-version: 20
- name: Checkout
uses: actions/checkout@v3
+19
View File
@@ -0,0 +1,19 @@
on:
workflow_dispatch:
push:
# Pattern matched against refs/tags
tags:
- "v[0-9]+.[0-9]+.[0-9]+" # Semantic version tags
jobs:
release:
runs-on: ubuntu-latest
environment: "protected branches"
steps:
- uses: actions/checkout@v4
with:
ref: main # Always checkout main even for tagged releases
fetch-depth: 0
token: ${{ secrets.GH_ACCESS_TOKEN }}
- name: Push to production
run: git push origin +main:production
+3
View File
@@ -20,6 +20,9 @@ next-env.d.ts
# production
/build
# idea
.idea
# misc
.DS_Store
*.pem
+1 -1
View File
@@ -1 +1 @@
v18.16.0
v20
+99 -32
View File
@@ -5,18 +5,16 @@ First off, thanks for taking the time to contribute! ❤️
Langfuse is an open-source observability and analytics solution for LLM-based applications. We welcome contributions through GitHub pull requests. This document outlines our conventions regarding development workflow, commit message formatting, contact points, and other resources. Our goal is to simplify the process and ensure that your contributions are easily accepted.
We gratefully welcome improvements to documentation as well as to code.
<!-- The community looks forward to your contributions. 🎉 -->
The maintainers are available on [Discord](https://langfuse.com/discord) in case you have any questions.
> And if you like the project, but just don't have time to contribute, that's fine. There are other easy ways to support the project and show your appreciation, which we would also be very happy about:
>
> - Star the project;
> - Tweet about it;
> - Refer to this project in your project's readme;
> - Mention the project at local meetups and tell your friends/colleagues.
The maintainers are available on [Discord](https://langfuse.com/discord) in case you have any questions.
# How to contribute to Langfuse
## Making a change
_Before making any significant changes, please [open an issue](https://github.com/langfuse/langfuse/issues)._ Discussing your proposed changes ahead of time will make the contribution process smooth for everyone.
@@ -27,45 +25,114 @@ Once we've discussed your changes and you've got your code ready, make sure that
A good first step is to search for open [issues](https://github.com/langfuse/langfuse/issues). Issues are labeled, and some good issues to start with are labeled: [good first issue](https://github.com/langfuse/langfuse/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22).
## Setup
### Technologies we use
First, the preferred version for development is Node.js >=18. Additionally, you should have npm and Docker installed
- NextJS, pages router
- NextAuth.js / Auth.js
- TRPC
- Prisma ORM
- Zod
- Tailwind CSS
- shadcn/ui tailwind components (using Radix and tanstack)
Then, to begin development fork repository and go to root folder by run `cd ./langfuse` .
## Development Setup
```bash
# Install dependencies
npm install
Requirements
# Run the db
docker-compose -f docker-compose.dev.yml up -d
- Node.js 20 as specified in the [.nvmrc](.nvmrc)
- Docker to run the database locally
# create an env file
cp .env.dev.example .env
**Steps**
# Migration
npm run db:migrate
1. Fork the the repository and clone it locally
2. Install dependencies
# Optional: seed the database
# npm run db:seed
# npm run db:seed:examples
```bash
npm install
```
# Start the server
npm run dev
```
3. Run the development database
```bash
docker-compose -f docker-compose.dev.yml up -d
```
4. Create an env file
```bash
cp .env.dev.example .env
```
5. Run the migrations
```bash
npm run db:migrate
# Optional: seed the database
# npm run db:seed
# npm run db:seed:examples
```
6. Start the development server
```bash
npm run dev
```
## Commit messages
On the main branch, we adhere to the best practices of [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/). All pull requests and branches are squash-merged to maintain a clean and readable history. This approach ensures the addition of a conventional commit message when merging contributions.
On the main branch, we adhere to the best practices of [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/). All pull requests and branches are squash-merged to maintain a clean and readable history. This approach ensures the addition of a conventional commit message when merging contributions.
## Test the public API
The API is tested using Jest. With the development server running, you can run the tests with:
Run all
```bash
npm run test
```
Run interactively in watch mode
```bash
npm run test:watch
```
These tests are also run in CI.
## CI/CD
We use GitHub Actions for CI/CD, the configuration is in [`.github/workflows/pipeline.yml`](.github/workflows/pipeline.yml)
CI on `main` and `pull_request`
- Check Linting
- E2E test of API using Jest
- E2E tests of UI using Playwright
CD on `main`
- Publish Docker image to GitHub Packages if CI passes. Done on every push to `main` branch. Only released versions are tagged with `latest`.
## Staging environment
We run a staging environment at [https://staging.langfuse.com](https://staging.langfuse.com) that is automatically deployed on every push to `main` branch.
The same environment is also used for preview deployments of pull requests. Limitations:
- SSO is not available as dynamic domains are not supported by most SSO providers.
- When making changes to the database, migrations to the staging database need to be applied manually by a maintainer. If you want to interactively test database changes in the staging environment, please reach out.
You can use the staging environment end-to-end with the Langfuse integrations or SDKs (host: `https://staging.langfuse.com`). However, please note that the staging environment is not intended for production use and may be reset at any time.
## Production environment
When a new release is tagged on the `main` branch (excluding prereleases), it triggers a production deployment. The deployment process consists of two steps:
1. The Docker image is published to GitHub Packages with the version number and `latest` tag.
2. The deployment is carried out on Langfuse Cloud. This is done by force pushing the `main` branch to the `production` branch during every release, using the [`release.yml`](.github/workflows/release.yml) GitHub Action.
## License
Langfuse is MIT licensed, except for `ee/` folder. See [LICENSE](LICENSE) and [docs](https://langfuse.com/docs/open-source) for more details.
+1 -3
View File
@@ -1,5 +1,5 @@
# Base image
FROM node:18-alpine AS base
FROM node:20-alpine AS base
ARG DATABASE_URL
ARG NEXTAUTH_SECRET
ARG NEXTAUTH_URL
@@ -70,7 +70,6 @@ ENV NEXT_TELEMETRY_DISABLED 1
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
RUN npm install --no-package-lock --no-save cron
RUN npm install -g --no-package-lock --no-save prisma
COPY --from=builder /app/public ./public
@@ -81,7 +80,6 @@ COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
COPY --from=builder --chown=nextjs:nodejs /app/prisma ./prisma
COPY --chown=nextjs:nodejs cron.js ./cron.js
COPY --chown=nextjs:nodejs entrypoint.sh ./entrypoint.sh
RUN chmod +x ./entrypoint.sh
+2 -29
View File
@@ -218,34 +218,7 @@ To contribute, send us a PR, raise a GitHub issue, or email at contributing@lang
### Development setup
Requirements: Node.js >=18, npm, Docker
```bash
# Install dependencies
npm install
# Run the db
docker-compose -f docker-compose.dev.yml up -d
# create an env file
cp .env.dev.example .env
# Migration
npm run db:migrate
# Optional: seed the database
# npm run db:seed
# npm run db:seed:examples
# Start the server
npm run dev
```
Run tests
```bash
npm run test
```
See [CONTRIBUTING.md](CONTRIBUTING.md) for details on how to setup a development environment.
## License
@@ -280,6 +253,6 @@ This helps us to:
1. Understand how Langfuse is used and improve the most relevant features.
2. Track overall usage for internal and external (e.g. fundraising) reporting.
None of the data is shared with third parties and does not include any sensitive information. We want to be super transparent about this and you can find the exact data we collect [here](/src/pages/api/cron/telemetry.ts).
None of the data is shared with third parties and does not include any sensitive information. We want to be super transparent about this and you can find the exact data we collect [here](/src/features/telemetry/index.ts).
You can opt-out by setting `TELEMETRY_ENABLED=false`.
-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");
}
+2 -2
View File
@@ -8,7 +8,7 @@ services:
- DATABASE_URL=postgresql://postgres:postgres@db:5432/postgres
- NEXTAUTH_SECRET=mysecret
- SALT=mysalt
- NEXTAUTH_URL=http:localhost:3000
- NEXTAUTH_URL=http://localhost:3000
depends_on:
- db
ports:
@@ -18,7 +18,7 @@ services:
- DATABASE_URL=postgresql://postgres:postgres@db:5432/postgres
- NEXTAUTH_SECRET=mysecret
- SALT=mysalt
- NEXTAUTH_URL=http:localhost:3000
- NEXTAUTH_URL=http://localhost:3000
- TELEMETRY_ENABLED=${TELEMETRY_ENABLED:-true}
- NEXT_PUBLIC_SIGN_UP_DISABLED=${NEXT_PUBLIC_SIGN_UP_DISABLED:-false}
- LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES=${LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES:-false}
+1 -1
View File
@@ -12,7 +12,7 @@ services:
- DATABASE_URL=postgresql://postgres:postgres@db:5432/postgres
- NEXTAUTH_SECRET=mysecret
- SALT=mysalt
- NEXTAUTH_URL=http:localhost:3000
- NEXTAUTH_URL=http://localhost:3000
- TELEMETRY_ENABLED=${TELEMETRY_ENABLED:-true}
- NEXT_PUBLIC_SIGN_UP_DISABLED=${NEXT_PUBLIC_SIGN_UP_DISABLED:-false}
- LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES=${LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES:-false}
-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
+8 -5
View File
@@ -39,6 +39,9 @@ types:
version: optional<string>
userId: optional<string>
metadata: optional<unknown>
public:
type: optional<boolean>
docs: Public traces are accessible via url without login
TraceWithDetails:
extends: Trace
properties:
@@ -146,16 +149,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
+11 -1
View File
@@ -7,13 +7,23 @@ service:
endpoints:
create:
method: POST
docs: Create a dataset item
docs: Create a dataset item, upserts on id
path: /dataset-items
request: CreateDatasetItemRequest
response: commons.DatasetItem
get:
docs: Get a specific dataset item
method: GET
path: /dataset-items/{id}
path-parameters:
id:
type: string
response: commons.DatasetItem
types:
CreateDatasetItemRequest:
properties:
datasetName: string
input: unknown
expectedOutput: optional<unknown>
id: optional<string>
@@ -23,6 +23,7 @@ types:
generationId: string
traceId: optional<string>
name: optional<string>
startTime: optional<datetime>
endTime: optional<datetime>
completionStartTime: optional<datetime>
model: optional<string>
+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
View File
@@ -23,6 +23,7 @@ types:
properties:
spanId: string
traceId: optional<string>
startTime: optional<datetime>
endTime: optional<datetime>
name: optional<string>
metadata: optional<unknown>
+3
View File
@@ -44,6 +44,9 @@ types:
release: optional<string>
version: optional<string>
metadata: optional<unknown>
public:
type: optional<boolean>
docs: Make trace publicly accessible via url
Traces:
properties:
data: list<commons.TraceWithDetails>
+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"
}
+251 -180
View File
@@ -5,7 +5,7 @@ info:
paths:
/api/public/dataset-items:
post:
description: Create a dataset item
description: Create a dataset item, upserts on id
operationId: datasetItems_create
tags:
- DatasetItems
@@ -21,32 +21,27 @@ paths:
description: ''
content:
application/json:
schema:
type: string
schema: {}
'401':
description: ''
content:
application/json:
schema:
type: string
schema: {}
'403':
description: ''
content:
application/json:
schema:
type: string
schema: {}
'404':
description: ''
content:
application/json:
schema:
type: string
schema: {}
'405':
description: ''
content:
application/json:
schema:
type: string
schema: {}
security: &ref_0
- BasicAuth: []
requestBody:
@@ -55,6 +50,51 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/CreateDatasetItemRequest'
/api/public/dataset-items/{id}:
get:
description: Get a specific dataset item
operationId: datasetItems_get
tags:
- DatasetItems
parameters:
- name: id
in: path
required: true
schema:
type: string
responses:
'200':
description: ''
content:
application/json:
schema:
$ref: '#/components/schemas/DatasetItem'
'400':
description: ''
content:
application/json:
schema: {}
'401':
description: ''
content:
application/json:
schema: {}
'403':
description: ''
content:
application/json:
schema: {}
'404':
description: ''
content:
application/json:
schema: {}
'405':
description: ''
content:
application/json:
schema: {}
security: *ref_0
/api/public/dataset-run-items:
post:
description: Create a dataset run item
@@ -73,32 +113,27 @@ paths:
description: ''
content:
application/json:
schema:
type: string
schema: {}
'401':
description: ''
content:
application/json:
schema:
type: string
schema: {}
'403':
description: ''
content:
application/json:
schema:
type: string
schema: {}
'404':
description: ''
content:
application/json:
schema:
type: string
schema: {}
'405':
description: ''
content:
application/json:
schema:
type: string
schema: {}
security: *ref_0
requestBody:
required: true
@@ -129,32 +164,27 @@ paths:
description: ''
content:
application/json:
schema:
type: string
schema: {}
'401':
description: ''
content:
application/json:
schema:
type: string
schema: {}
'403':
description: ''
content:
application/json:
schema:
type: string
schema: {}
'404':
description: ''
content:
application/json:
schema:
type: string
schema: {}
'405':
description: ''
content:
application/json:
schema:
type: string
schema: {}
security: *ref_0
/api/public/datasets:
post:
@@ -174,32 +204,27 @@ paths:
description: ''
content:
application/json:
schema:
type: string
schema: {}
'401':
description: ''
content:
application/json:
schema:
type: string
schema: {}
'403':
description: ''
content:
application/json:
schema:
type: string
schema: {}
'404':
description: ''
content:
application/json:
schema:
type: string
schema: {}
'405':
description: ''
content:
application/json:
schema:
type: string
schema: {}
security: *ref_0
requestBody:
required: true
@@ -235,32 +260,27 @@ paths:
description: ''
content:
application/json:
schema:
type: string
schema: {}
'401':
description: ''
content:
application/json:
schema:
type: string
schema: {}
'403':
description: ''
content:
application/json:
schema:
type: string
schema: {}
'404':
description: ''
content:
application/json:
schema:
type: string
schema: {}
'405':
description: ''
content:
application/json:
schema:
type: string
schema: {}
security: *ref_0
/api/public/events:
post:
@@ -280,32 +300,27 @@ paths:
description: ''
content:
application/json:
schema:
type: string
schema: {}
'401':
description: ''
content:
application/json:
schema:
type: string
schema: {}
'403':
description: ''
content:
application/json:
schema:
type: string
schema: {}
'404':
description: ''
content:
application/json:
schema:
type: string
schema: {}
'405':
description: ''
content:
application/json:
schema:
type: string
schema: {}
security: *ref_0
requestBody:
required: true
@@ -330,32 +345,27 @@ paths:
description: ''
content:
application/json:
schema:
type: string
schema: {}
'401':
description: ''
content:
application/json:
schema:
type: string
schema: {}
'403':
description: ''
content:
application/json:
schema:
type: string
schema: {}
'404':
description: ''
content:
application/json:
schema:
type: string
schema: {}
'405':
description: ''
content:
application/json:
schema:
type: string
schema: {}
security: *ref_0
requestBody:
required: true
@@ -379,32 +389,27 @@ paths:
description: ''
content:
application/json:
schema:
type: string
schema: {}
'401':
description: ''
content:
application/json:
schema:
type: string
schema: {}
'403':
description: ''
content:
application/json:
schema:
type: string
schema: {}
'404':
description: ''
content:
application/json:
schema:
type: string
schema: {}
'405':
description: ''
content:
application/json:
schema:
type: string
schema: {}
security: *ref_0
requestBody:
required: true
@@ -412,6 +417,43 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/UpdateGenerationRequest'
/api/public/health:
get:
description: Check health of API and database
operationId: health_health
tags:
- Health
parameters: []
responses:
'204':
description: ''
'400':
description: ''
content:
application/json:
schema: {}
'401':
description: ''
content:
application/json:
schema: {}
'403':
description: ''
content:
application/json:
schema: {}
'404':
description: ''
content:
application/json:
schema: {}
'405':
description: ''
content:
application/json:
schema: {}
'503':
description: ''
/api/public/ingestion:
post:
description: Ingest multiple events to Langfuse
@@ -430,32 +472,27 @@ paths:
description: ''
content:
application/json:
schema:
type: string
schema: {}
'401':
description: ''
content:
application/json:
schema:
type: string
schema: {}
'403':
description: ''
content:
application/json:
schema:
type: string
schema: {}
'404':
description: ''
content:
application/json:
schema:
type: string
schema: {}
'405':
description: ''
content:
application/json:
schema:
type: string
schema: {}
security: *ref_0
requestBody:
required: true
@@ -496,32 +533,27 @@ paths:
description: ''
content:
application/json:
schema:
type: string
schema: {}
'401':
description: ''
content:
application/json:
schema:
type: string
schema: {}
'403':
description: ''
content:
application/json:
schema:
type: string
schema: {}
'404':
description: ''
content:
application/json:
schema:
type: string
schema: {}
'405':
description: ''
content:
application/json:
schema:
type: string
schema: {}
security: *ref_0
/api/public/observations:
get:
@@ -583,32 +615,66 @@ paths:
description: ''
content:
application/json:
schema:
type: string
schema: {}
'401':
description: ''
content:
application/json:
schema:
type: string
schema: {}
'403':
description: ''
content:
application/json:
schema:
type: string
schema: {}
'404':
description: ''
content:
application/json:
schema:
type: string
schema: {}
'405':
description: ''
content:
application/json:
schema: {}
security: *ref_0
/api/public/projects:
get:
operationId: projects_get
tags:
- Projects
parameters: []
responses:
'200':
description: ''
content:
application/json:
schema:
type: string
$ref: '#/components/schemas/Projects'
'400':
description: ''
content:
application/json:
schema: {}
'401':
description: ''
content:
application/json:
schema: {}
'403':
description: ''
content:
application/json:
schema: {}
'404':
description: ''
content:
application/json:
schema: {}
'405':
description: ''
content:
application/json:
schema: {}
security: *ref_0
/api/public/scores:
post:
@@ -628,32 +694,27 @@ paths:
description: ''
content:
application/json:
schema:
type: string
schema: {}
'401':
description: ''
content:
application/json:
schema:
type: string
schema: {}
'403':
description: ''
content:
application/json:
schema:
type: string
schema: {}
'404':
description: ''
content:
application/json:
schema:
type: string
schema: {}
'405':
description: ''
content:
application/json:
schema:
type: string
schema: {}
security: *ref_0
requestBody:
required: true
@@ -702,32 +763,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 +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
@@ -797,32 +848,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 +894,27 @@ paths:
description: ''
content:
application/json:
schema:
type: string
schema: {}
'401':
description: ''
content:
application/json:
schema:
type: string
schema: {}
'403':
description: ''
content:
application/json:
schema:
type: string
schema: {}
'404':
description: ''
content:
application/json:
schema:
type: string
schema: {}
'405':
description: ''
content:
application/json:
schema:
type: string
schema: {}
security: *ref_0
requestBody:
required: true
@@ -922,32 +963,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 +1009,27 @@ paths:
description: ''
content:
application/json:
schema:
type: string
schema: {}
'401':
description: ''
content:
application/json:
schema:
type: string
schema: {}
'403':
description: ''
content:
application/json:
schema:
type: string
schema: {}
'404':
description: ''
content:
application/json:
schema:
type: string
schema: {}
'405':
description: ''
content:
application/json:
schema:
type: string
schema: {}
security: *ref_0
components:
schemas:
@@ -1096,6 +1127,10 @@ components:
nullable: true
metadata:
nullable: true
public:
type: boolean
nullable: true
description: Public traces are accessible via url without login
required:
- id
- timestamp
@@ -1388,6 +1423,9 @@ components:
input: {}
expectedOutput:
nullable: true
id:
type: string
nullable: true
required:
- datasetName
- input
@@ -1425,6 +1463,10 @@ components:
name:
type: string
nullable: true
startTime:
type: string
format: date-time
nullable: true
endTime:
type: string
format: date-time
@@ -1577,6 +1619,27 @@ components:
required:
- data
- meta
Projects:
title: Projects
type: object
properties:
data:
type: array
items:
$ref: '#/components/schemas/Project'
required:
- data
Project:
title: Project
type: object
properties:
id:
type: string
name:
type: string
required:
- id
- name
CreateScoreRequest:
title: CreateScoreRequest
type: object
@@ -1623,6 +1686,10 @@ components:
traceId:
type: string
nullable: true
startTime:
type: string
format: date-time
nullable: true
endTime:
type: string
format: date-time
@@ -1671,6 +1738,10 @@ components:
nullable: true
metadata:
nullable: true
public:
type: boolean
nullable: true
description: Make trace publicly accessible via url
Traces:
title: Traces
type: object
+102 -6
View File
@@ -46,7 +46,7 @@
"_type": "endpoint",
"name": "Create",
"request": {
"description": "Create a dataset item",
"description": "Create a dataset item, upserts on id",
"url": {
"raw": "{{baseUrl}}/api/public/dataset-items",
"host": [
@@ -65,7 +65,7 @@
"auth": null,
"body": {
"mode": "raw",
"raw": "{\n \"datasetName\": \"example\",\n \"input\": \"UNKNOWN\",\n \"expectedOutput\": \"UNKNOWN\"\n}",
"raw": "{\n \"datasetName\": \"example\",\n \"input\": \"UNKNOWN\",\n \"expectedOutput\": \"UNKNOWN\",\n \"id\": \"example\"\n}",
"options": {
"raw": {
"language": "json"
@@ -74,6 +74,38 @@
}
},
"response": []
},
{
"_type": "endpoint",
"name": "Get",
"request": {
"description": "Get a specific dataset item",
"url": {
"raw": "{{baseUrl}}/api/public/dataset-items/:id",
"host": [
"{{baseUrl}}"
],
"path": [
"api",
"public",
"dataset-items",
":id"
],
"query": [],
"variable": [
{
"key": "id",
"value": "",
"description": null
}
]
},
"header": [],
"method": "GET",
"auth": null,
"body": null
},
"response": []
}
]
},
@@ -329,7 +361,7 @@
"auth": null,
"body": {
"mode": "raw",
"raw": "{\n \"generationId\": \"example\",\n \"traceId\": \"example\",\n \"name\": \"example\",\n \"endTime\": \"1994-11-05T13:15:30Z\",\n \"completionStartTime\": \"1994-11-05T13:15:30Z\",\n \"model\": \"example\",\n \"modelParameters\": {\n \"example\": \"example\"\n },\n \"prompt\": \"UNKNOWN\",\n \"version\": \"example\",\n \"metadata\": \"UNKNOWN\",\n \"completion\": \"UNKNOWN\",\n \"usage\": {\n \"promptTokens\": 0,\n \"completionTokens\": 0,\n \"totalTokens\": 0\n },\n \"level\": \"DEBUG\",\n \"statusMessage\": \"example\"\n}",
"raw": "{\n \"generationId\": \"example\",\n \"traceId\": \"example\",\n \"name\": \"example\",\n \"startTime\": \"1994-11-05T13:15:30Z\",\n \"endTime\": \"1994-11-05T13:15:30Z\",\n \"completionStartTime\": \"1994-11-05T13:15:30Z\",\n \"model\": \"example\",\n \"modelParameters\": {\n \"example\": \"example\"\n },\n \"prompt\": \"UNKNOWN\",\n \"version\": \"example\",\n \"metadata\": \"UNKNOWN\",\n \"completion\": \"UNKNOWN\",\n \"usage\": {\n \"promptTokens\": 0,\n \"completionTokens\": 0,\n \"totalTokens\": 0\n },\n \"level\": \"DEBUG\",\n \"statusMessage\": \"example\"\n}",
"options": {
"raw": {
"language": "json"
@@ -341,6 +373,38 @@
}
]
},
{
"_type": "container",
"description": null,
"name": "Health",
"item": [
{
"_type": "endpoint",
"name": "Health",
"request": {
"description": "Check health of API and database",
"url": {
"raw": "{{baseUrl}}/api/public/health",
"host": [
"{{baseUrl}}"
],
"path": [
"api",
"public",
"health"
],
"query": [],
"variable": []
},
"header": [],
"method": "GET",
"auth": null,
"body": null
},
"response": []
}
]
},
{
"_type": "container",
"description": null,
@@ -369,7 +433,7 @@
"auth": null,
"body": {
"mode": "raw",
"raw": "{\n \"batch\": [\n {\n \"type\": \"trace-create\",\n \"id\": \"example\",\n \"timestamp\": \"example\",\n \"body\": {\n \"id\": \"example\",\n \"timestamp\": \"1994-11-05T13:15:30Z\",\n \"name\": \"example\",\n \"release\": \"example\",\n \"version\": \"example\",\n \"userId\": \"example\",\n \"metadata\": \"UNKNOWN\"\n }\n }\n ]\n}",
"raw": "{\n \"batch\": [\n {\n \"type\": \"trace-create\",\n \"id\": \"example\",\n \"timestamp\": \"example\",\n \"body\": {\n \"id\": \"example\",\n \"timestamp\": \"1994-11-05T13:15:30Z\",\n \"name\": \"example\",\n \"release\": \"example\",\n \"version\": \"example\",\n \"userId\": \"example\",\n \"metadata\": \"UNKNOWN\",\n \"public\": true\n }\n }\n ]\n}",
"options": {
"raw": {
"language": "json"
@@ -481,6 +545,38 @@
}
]
},
{
"_type": "container",
"description": null,
"name": "Projects",
"item": [
{
"_type": "endpoint",
"name": "Get",
"request": {
"description": null,
"url": {
"raw": "{{baseUrl}}/api/public/projects",
"host": [
"{{baseUrl}}"
],
"path": [
"api",
"public",
"projects"
],
"query": [],
"variable": []
},
"header": [],
"method": "GET",
"auth": null,
"body": null
},
"response": []
}
]
},
{
"_type": "container",
"description": null,
@@ -628,7 +724,7 @@
"auth": null,
"body": {
"mode": "raw",
"raw": "{\n \"spanId\": \"example\",\n \"traceId\": \"example\",\n \"endTime\": \"1994-11-05T13:15:30Z\",\n \"name\": \"example\",\n \"metadata\": \"UNKNOWN\",\n \"input\": \"UNKNOWN\",\n \"output\": \"UNKNOWN\",\n \"level\": \"DEBUG\",\n \"version\": \"example\",\n \"statusMessage\": \"example\"\n}",
"raw": "{\n \"spanId\": \"example\",\n \"traceId\": \"example\",\n \"startTime\": \"1994-11-05T13:15:30Z\",\n \"endTime\": \"1994-11-05T13:15:30Z\",\n \"name\": \"example\",\n \"metadata\": \"UNKNOWN\",\n \"input\": \"UNKNOWN\",\n \"output\": \"UNKNOWN\",\n \"level\": \"DEBUG\",\n \"version\": \"example\",\n \"statusMessage\": \"example\"\n}",
"options": {
"raw": {
"language": "json"
@@ -668,7 +764,7 @@
"auth": null,
"body": {
"mode": "raw",
"raw": "{\n \"id\": \"example\",\n \"name\": \"example\",\n \"userId\": \"example\",\n \"externalId\": \"example\",\n \"release\": \"example\",\n \"version\": \"example\",\n \"metadata\": \"UNKNOWN\"\n}",
"raw": "{\n \"id\": \"example\",\n \"name\": \"example\",\n \"userId\": \"example\",\n \"externalId\": \"example\",\n \"release\": \"example\",\n \"version\": \"example\",\n \"metadata\": \"UNKNOWN\",\n \"public\": true\n}",
"options": {
"raw": {
"language": "json"
+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
+1944 -1179
View File
File diff suppressed because it is too large Load Diff
+51 -39
View File
@@ -1,6 +1,6 @@
{
"name": "langfuse-core",
"version": "1.2.0",
"version": "1.13.2",
"private": true,
"scripts": {
"prebuild": "cp generated/openapi-client/openapi.yml public/openapi-client.yml && cp generated/openapi-server/openapi.yml public/openapi-server.yml",
@@ -31,7 +31,7 @@
"@heroicons/react": "^2.0.18",
"@hookform/resolvers": "^3.3.2",
"@next-auth/prisma-adapter": "^1.0.7",
"@prisma/client": "^5.5.2",
"@prisma/client": "^5.6.0",
"@radix-ui/react-avatar": "^1.0.4",
"@radix-ui/react-collapsible": "^1.0.3",
"@radix-ui/react-dialog": "^1.0.5",
@@ -48,7 +48,9 @@
"@radix-ui/react-tabs": "^1.0.4",
"@radix-ui/react-toggle": "^1.0.3",
"@radix-ui/react-tooltip": "^1.0.7",
"@sentry/nextjs": "^7.77.0",
"@react-email/components": "^0.0.12",
"@react-email/render": "^0.0.10",
"@sentry/nextjs": "^7.83.0",
"@sentry/profiling-node": "^1.2.6",
"@t3-oss/env-nextjs": "^0.7.1",
"@tailwindcss/forms": "^0.5.7",
@@ -56,12 +58,12 @@
"@tanstack/react-query": "^4.36.1",
"@tanstack/react-table": "^8.10.7",
"@tremor/react": "^3.11.1",
"@trpc/client": "^10.43.3",
"@trpc/next": "^10.43.3",
"@trpc/react-query": "^10.43.1",
"@trpc/server": "^10.43.1",
"@trpc/client": "^10.44.1",
"@trpc/next": "^10.44.1",
"@trpc/react-query": "^10.44.1",
"@trpc/server": "^10.44.1",
"@vercel/edge-config": "^0.4.1",
"axios": "^1.6.1",
"axios": "^1.6.2",
"bcryptjs": "^2.4.3",
"class-variance-authority": "^0.7.0",
"clsx": "^2.0.0",
@@ -72,62 +74,64 @@
"exponential-backoff": "^3.1.1",
"js-yaml": "^4.1.0",
"lodash": "^4.17.21",
"lucide-react": "^0.292.0",
"next": "^13.5.6",
"lucide-react": "^0.294.0",
"next": "^14.0.3",
"next-auth": "^4.24.5",
"next-query-params": "^4.2.3",
"next-swagger-doc": "^0.4.0",
"posthog-js": "^1.88.4",
"next-query-params": "^5.0.0",
"nodemailer": "^6.9.7",
"posthog-js": "^1.93.6",
"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",
"react18-json-view": "^0.2.7-canary.2",
"superagent": "^8.1.2",
"superjson": "2.0.0",
"tailwind-merge": "^1.14.0",
"superjson": "2.2.1",
"tailwind-merge": "^2.1.0",
"tailwindcss-animate": "^1.0.7",
"tiktoken": "^1.0.10",
"ts-pattern": "^5.0.5",
"tiktoken": "^1.0.11",
"ts-pattern": "^5.0.6",
"use-query-params": "^2.2.1",
"uuid": "^9.0.1",
"zod": "^3.22.4"
},
"devDependencies": {
"@playwright/test": "^1.39.0",
"@testing-library/jest-dom": "^6.1.4",
"@playwright/test": "^1.40.1",
"@release-it/bumper": "^6.0.1",
"@testing-library/jest-dom": "^6.1.5",
"@types/bcryptjs": "^2.4.6",
"@types/cors": "^2.8.16",
"@types/eslint": "^8.44.6",
"@types/jest": "^29.5.8",
"@types/cors": "^2.8.17",
"@types/eslint": "^8.44.8",
"@types/jest": "^29.5.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": "20.10.0",
"@types/nodemailer": "^6.4.14",
"@types/react": "^18.2.42",
"@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.13.2",
"@typescript-eslint/parser": "^6.13.2",
"autoprefixer": "^10.4.16",
"dotenv-cli": "^7.3.0",
"eslint": "^8.52.0",
"eslint-config-next": "^13.5.6",
"eslint": "^8.55.0",
"eslint-config-next": "^14.0.3",
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
"postcss": "^8.4.31",
"prettier": "^3.0.3",
"prettier-plugin-tailwindcss": "^0.5.7",
"prisma": "^5.5.2",
"postcss": "^8.4.32",
"prettier": "^3.1.0",
"prettier-plugin-tailwindcss": "^0.5.8",
"prisma": "^5.6.0",
"release-it": "^17.0.0",
"swagger-jsdoc": "^6.2.8",
"tailwindcss": "^3.3.5",
"tailwindcss": "^3.3.6",
"ts-node": "^10.9.1",
"tsconfig-paths": "^4.2.0",
"typescript": "^5.2.2"
"typescript": "^5.3.2"
},
"ct3aMetadata": {
"initVersion": "7.13.0"
@@ -145,6 +149,14 @@
"web": true,
"autoGenerate": true,
"releaseName": "v${version}"
},
"plugins": {
"@release-it/bumper": {
"out": {
"file": "./src/constants/VERSION.ts",
"type": "application/typescript"
}
}
}
}
}
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "cron_jobs" ADD COLUMN "job_started_at" TIMESTAMP(3);
@@ -0,0 +1,3 @@
-- AlterTable
ALTER TABLE "traces" ADD COLUMN "bookmarked" BOOLEAN NOT NULL DEFAULT false;
@@ -0,0 +1,27 @@
-- CreateTable
CREATE TABLE "membership_invitations" (
"id" TEXT NOT NULL,
"email" TEXT NOT NULL,
"role" "MembershipRole" NOT NULL,
"project_id" TEXT NOT NULL,
"sender_id" TEXT,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "membership_invitations_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "membership_invitations_id_key" ON "membership_invitations"("id");
-- CreateIndex
CREATE INDEX "membership_invitations_project_id_idx" ON "membership_invitations"("project_id");
-- CreateIndex
CREATE INDEX "membership_invitations_email_idx" ON "membership_invitations"("email");
-- AddForeignKey
ALTER TABLE "membership_invitations" ADD CONSTRAINT "membership_invitations_project_id_fkey" FOREIGN KEY ("project_id") REFERENCES "projects"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "membership_invitations" ADD CONSTRAINT "membership_invitations_sender_id_fkey" FOREIGN KEY ("sender_id") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE CASCADE;
+33 -13
View File
@@ -47,19 +47,20 @@ model Session {
}
model User {
id String @id @default(cuid())
id String @id @default(cuid())
name String?
email String? @unique
emailVerified DateTime? @map("email_verified")
email String? @unique
emailVerified DateTime? @map("email_verified")
password String?
image String?
admin Boolean @default(false)
admin Boolean @default(false)
accounts Account[]
sessions Session[]
memberships Membership[]
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
featureFlags String[] @default([]) @map("feature_flags")
invitations MembershipInvitation[]
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
featureFlags String[] @default([]) @map("feature_flags")
@@map("users")
}
@@ -74,9 +75,9 @@ model VerificationToken {
}
model Project {
id String @id @default(cuid())
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
id String @id @default(cuid())
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
name String
members Membership[]
traces Trace[]
@@ -84,6 +85,7 @@ model Project {
apiKeys ApiKey[]
dataset Dataset[]
RawEvents Events[]
invitations MembershipInvitation[]
@@map("projects")
}
@@ -122,6 +124,22 @@ model Membership {
@@map("memberships")
}
model MembershipInvitation {
id String @id @unique @default(cuid())
email String
role MembershipRole
projectId String @map("project_id")
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
senderId String? @map("sender_id")
sender User? @relation(fields: [senderId], references: [id], onDelete: SetNull)
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
@@index([projectId])
@@index([email])
@@map("membership_invitations")
}
enum MembershipRole {
OWNER
ADMIN
@@ -141,6 +159,7 @@ model Trace {
projectId String @map("project_id")
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
public Boolean @default(false)
bookmarked Boolean @default(false)
scores Score[]
@@ -247,9 +266,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
+187
View File
@@ -0,0 +1,187 @@
/** @jest-environment node */
import { prisma } from "@/src/server/db";
import { makeAPICall, pruneDatabase } from "@/src/__tests__/test-utils";
import { v4 } from "uuid";
describe("/api/public/datasets and /api/public/dataset-items API Endpoints", () => {
beforeEach(async () => await pruneDatabase());
afterEach(async () => await pruneDatabase());
it("should create and get a dataset", async () => {
await makeAPICall("POST", "/api/public/datasets", {
name: "dataset-name",
});
const dbDataset = await prisma.dataset.findMany({
where: {
name: "dataset-name",
},
});
expect(dbDataset.length).toBeGreaterThan(0);
const getDataset = await makeAPICall(
"GET",
`/api/public/datasets/dataset-name`,
);
expect(getDataset.status).toBe(200);
expect(getDataset.body).toMatchObject({
name: "dataset-name",
});
});
it("should create and get a dataset item (via datasets and individually)", async () => {
await makeAPICall("POST", "/api/public/datasets", {
name: "dataset-name",
});
await makeAPICall("POST", "/api/public/dataset-items", {
datasetName: "dataset-name",
input: { key: "value" },
expectedOutput: { key: "value" },
});
const dbDatasetItem = await prisma.datasetItem.findFirst({
where: {
dataset: {
name: "dataset-name",
},
},
});
expect(dbDatasetItem).not.toBeNull();
const getDataset = await makeAPICall(
"GET",
`/api/public/datasets/dataset-name`,
);
expect(getDataset.status).toBe(200);
expect(getDataset.body).toMatchObject({
name: "dataset-name",
items: [
{
id: dbDatasetItem!.id,
input: { key: "value" },
expectedOutput: { key: "value" },
},
],
});
const getDatasetItem = await makeAPICall(
"GET",
`/api/public/dataset-items/${dbDatasetItem!.id}`,
);
expect(getDatasetItem.status).toBe(200);
expect(getDatasetItem.body).toMatchObject({
id: dbDatasetItem!.id,
input: { key: "value" },
expectedOutput: { key: "value" },
});
});
it("should upsert a dataset item", async () => {
await makeAPICall("POST", "/api/public/datasets", {
name: "dataset-name",
});
const item1 = await makeAPICall("POST", "/api/public/dataset-items", {
id: "dataset-item-id",
datasetName: "dataset-name",
input: { key: "value" },
});
expect(item1.status).toBe(200);
expect(item1.body).toMatchObject({
id: "dataset-item-id",
});
const item2 = await makeAPICall("POST", "/api/public/dataset-items", {
id: "dataset-item-id",
datasetName: "dataset-name",
input: { key: "value2" },
});
expect(item2.status).toBe(200);
expect(item2.body).toMatchObject({
id: "dataset-item-id",
input: { key: "value2" },
});
const dbDatasetItem = await prisma.datasetItem.findFirst({
where: { id: "dataset-item-id" },
});
expect(dbDatasetItem).not.toBeNull();
expect(dbDatasetItem?.input).toMatchObject({ key: "value2" });
});
it("should create and get a dataset run", async () => {
const dataset = await makeAPICall("POST", "/api/public/datasets", {
name: "dataset-name",
});
expect(dataset.status).toBe(200);
expect(dataset.body).toMatchObject({
name: "dataset-name",
});
await makeAPICall("POST", "/api/public/dataset-items", {
datasetName: "dataset-name",
id: "dataset-item-id",
input: { key: "value" },
expectedOutput: { key: "value" },
});
const traceId = v4();
const observationId = v4();
const response = await makeAPICall("POST", "/api/public/ingestion", {
batch: [
{
id: v4(),
type: "trace-create",
timestamp: new Date().toISOString(),
body: {
id: traceId,
name: "trace-name",
userId: "user-1",
metadata: { key: "value" },
release: "1.0.0",
version: "2.0.0",
},
},
{
id: v4(),
type: "observation-create",
timestamp: new Date().toISOString(),
body: {
id: observationId,
traceId: traceId,
type: "GENERATION",
name: "generation-name",
startTime: "2021-01-01T00:00:00.000Z",
endTime: "2021-01-01T00:00:00.000Z",
modelParameters: { key: "value" },
input: { key: "value" },
metadata: { key: "value" },
version: "2.0.0",
},
},
],
});
expect(response.status).toBe(207);
const runItem = await makeAPICall("POST", "/api/public/dataset-run-items", {
datasetItemId: "dataset-item-id",
observationId: observationId,
runName: "run-name",
});
const dbRun = await prisma.datasetRuns.findFirst({
where: {
name: "run-name",
},
});
expect(dbRun).not.toBeNull();
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
expect(dbRun?.datasetId).toBe(dataset.body.id);
expect(runItem.status).toBe(200);
expect(runItem.body).toMatchObject({
datasetItemId: "dataset-item-id",
observationId: observationId,
datasetRunId: dbRun?.id,
});
});
});
+179
View File
@@ -1,6 +1,7 @@
/** @jest-environment node */
import { makeAPICall, pruneDatabase } from "@/src/__tests__/test-utils";
import { cleanEvent } from "@/src/pages/api/public/ingestion";
import { prisma } from "@/src/server/db";
import { v4 } from "uuid";
@@ -247,6 +248,108 @@ describe("/api/public/ingestion API Endpoint", () => {
expect(dbTrace.length).toBe(1);
});
it("should fail for auth errors", async () => {
const traceId = v4();
const scoreId = v4();
const responseOne = await makeAPICall("POST", "/api/public/ingestion", {
batch: [
{
id: v4(),
type: "trace-create",
timestamp: new Date().toISOString(),
body: {
id: traceId,
name: "trace-name",
userId: "user-1",
metadata: { key: "value" },
release: "1.0.0",
version: "2.0.0",
},
},
{
id: v4(),
type: "score-create",
timestamp: new Date().toISOString(),
body: {
id: scoreId,
name: "score-name",
value: 100.5,
traceId: "some-random-id",
},
},
],
});
console.log(responseOne.body);
expect(responseOne.status).toBe(207);
expect("errors" in responseOne.body).toBe(true);
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
expect(responseOne.body?.errors.length).toBe(1);
expect("successes" in responseOne.body).toBe(true);
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
expect(responseOne.body?.successes.length).toBe(1);
const dbTrace = await prisma.trace.findMany({
where: {
name: "trace-name",
},
});
expect(dbTrace.length).toBe(1);
});
it("should fail for resource not found", async () => {
const traceId = v4();
const responseOne = await makeAPICall("POST", "/api/public/ingestion", {
batch: [
{
id: v4(),
type: "trace-create",
timestamp: new Date().toISOString(),
body: {
id: traceId,
name: "trace-name",
userId: "user-1",
metadata: { key: "value" },
release: "1.0.0",
version: "2.0.0",
},
},
{
id: v4(),
type: "observation-update",
timestamp: new Date().toISOString(),
body: {
id: "some-random-id",
type: "GENERATION",
output: { key: "this is a great gpt output" },
},
},
],
});
console.log(responseOne.body);
expect(responseOne.status).toBe(207);
expect("errors" in responseOne.body).toBe(true);
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
expect(responseOne.body?.errors.length).toBe(1);
expect("successes" in responseOne.body).toBe(true);
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
expect(responseOne.body?.successes.length).toBe(1);
const dbTrace = await prisma.trace.findMany({
where: {
name: "trace-name",
},
});
expect(dbTrace.length).toBe(1);
});
it("should update all token counts if update does not contain model name", async () => {
const traceId = v4();
const generationId = v4();
@@ -584,4 +687,80 @@ describe("/api/public/ingestion API Endpoint", () => {
expect(dbGeneration).toBeTruthy();
});
it("filters out NULL characters", async () => {
const traceId = v4();
const generationId = v4();
const responseOne = await makeAPICall("POST", "/api/public/ingestion", {
batch: [
{
id: v4(),
type: "trace-create",
timestamp: new Date().toISOString(),
body: {
id: traceId,
name: "trace-name",
},
},
{
id: v4(),
type: "observation-create",
timestamp: new Date().toISOString(),
body: {
id: generationId,
traceId: traceId,
type: "GENERATION",
name: "generation-name",
traceIdType: "LANGFUSE",
input: {
key: "IB\nibo.org Change site\nIB Home / . . . / News / News about the IB / Why ChatGPT is an opportunity for schools IB Home / News / News about the IB / Why ChatGPT is an opportunity for schools Why ChatGPT is an opportunity for schools Published: 06 March 2023 Last updated: 06 June 2023 Date published: 28 February 2023 Dr Matthew Glanville, Head of Assessment Principles and Practice Source: Why ChatGPT is an opportunity for schools | The Times Those of us who work in the schools or exam sector should not be terri \u0000 ed by ChatGPT and the rise of AI software we should be excited. We should embrace it as an extraordinary opportunity. Contrary to some stark warnings, it is not the end of exams, nor even a huge threat to coursework, but it does bring into very sharp focus the impact that arti \u0000 ",
},
output: {
key: "제점이 있었죠. 그중 하나는 일제가 한국의 신용체계를 망가뜨린 채 한국을 떠났다는 겁니다. 해방전 일제는 조선의 신용체계를 거의 독점적으로 소유한 상황이었습니다. 1945년 6월 기준 일제는 조선의 본점을 둔 전체은행 5개의 불입자본 총액의 89.7%를",
},
},
},
],
});
expect(responseOne.status).toBe(207);
const dbTrace = await prisma.trace.findMany({
where: {
id: traceId,
},
});
expect(dbTrace.length).toEqual(1);
const dbGeneration = await prisma.observation.findUnique({
where: {
id: generationId,
},
});
expect(dbGeneration?.input).toStrictEqual({
key: `IB
ibo.org Change site
IB Home / . . . / News / News about the IB / Why ChatGPT is an opportunity for schools IB Home / News / News about the IB / Why ChatGPT is an opportunity for schools Why ChatGPT is an opportunity for schools Published: 06 March 2023 Last updated: 06 June 2023 Date published: 28 February 2023 Dr Matthew Glanville, Head of Assessment Principles and Practice Source: Why ChatGPT is an opportunity for schools | The Times Those of us who work in the schools or exam sector should not be terri ed by ChatGPT and the rise of AI software we should be excited. We should embrace it as an extraordinary opportunity. Contrary to some stark warnings, it is not the end of exams, nor even a huge threat to coursework, but it does bring into very sharp focus the impact that arti `,
});
expect(dbGeneration?.output).toStrictEqual({
key: "제점이 있었죠. 그중 하나는 일제가 한국의 신용체계를 망가뜨린 채 한국을 떠났다는 겁니다. 해방전 일제는 조선의 신용체계를 거의 독점적으로 소유한 상황이었습니다. 1945년 6월 기준 일제는 조선의 본점을 둔 전체은행 5개의 불입자본 총액의 89.7%를",
});
expect(dbGeneration).toBeTruthy();
});
[
{ input: "A\u0000hallo", expected: "Ahallo" },
{ input: ["A\u0000hallo"], expected: ["Ahallo"] },
{ input: { obj: ["A\u0000hallo"] }, expected: { obj: ["Ahallo"] } },
].forEach(({ input, expected }) => {
it(`cleans events with null values ${JSON.stringify(
input,
)} ${JSON.stringify(expected)}`, () => {
const cleanedEvent = cleanEvent(input);
console.log(cleanedEvent);
expect(cleanedEvent).toStrictEqual(expected);
});
});
});
+7 -2
View File
@@ -9,6 +9,9 @@ export const pruneDatabase = async () => {
await prisma.score.deleteMany();
await prisma.observation.deleteMany();
await prisma.trace.deleteMany();
await prisma.datasetItem.deleteMany();
await prisma.dataset.deleteMany();
await prisma.datasetRuns.deleteMany();
};
export function createBasicAuthHeader(
@@ -24,7 +27,7 @@ export function createBasicAuthHeader(
export async function makeAPICall(
method: "POST" | "GET" | "PUT" | "DELETE" | "PATCH",
url: string,
body: unknown,
body?: unknown,
) {
const finalUrl = `http://localhost:3000/${url}`;
const options = {
@@ -37,7 +40,9 @@ export async function makeAPICall(
"sk-lf-1234567890",
),
},
body: JSON.stringify(body),
// Conditionally include the body property if the method is not "GET"
...(method !== "GET" &&
body !== undefined && { body: JSON.stringify(body) }),
};
const a = await fetch(finalUrl, options);
+28 -9
View File
@@ -44,29 +44,48 @@ describe("/api/public/traces API Endpoint", () => {
metadata: { key: "value" },
release: "1.0.0",
version: "2.0.0",
public: true,
});
const dbTrace1 = await prisma.trace.findFirst({
where: {
id: "trace-id",
},
});
expect(dbTrace1).not.toBeNull();
expect(dbTrace1).toMatchObject({
name: "trace-name",
release: "1.0.0",
externalId: null,
version: "2.0.0",
public: true,
userId: "user-1",
});
await makeAPICall("POST", "/api/public/traces", {
id: "trace-id",
name: "trace-name",
userId: "user-1",
projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
metadata: { key: "value" },
release: "1.0.0",
version: "5.0.0",
public: false,
});
const dbTrace = await prisma.trace.findMany({
const dbTrace2 = await prisma.trace.findFirst({
where: {
name: "trace-name",
},
});
expect(dbTrace.length).toBeGreaterThan(0);
expect(dbTrace[0]?.name).toBe("trace-name");
expect(dbTrace[0]?.release).toBe("1.0.0");
expect(dbTrace[0]?.externalId).toBeNull();
expect(dbTrace[0]?.version).toBe("5.0.0");
expect(dbTrace[0]?.projectId).toBe("7a88fb47-b4e2-43b8-a06c-a5ce950dc53a");
expect(dbTrace2).not.toBeNull();
expect(dbTrace2).toMatchObject({
name: "trace-name",
release: "1.0.0",
externalId: null,
version: "5.0.0",
public: false,
userId: "user-1",
});
});
});
+60
View File
@@ -0,0 +1,60 @@
import { StarIcon } from "lucide-react";
import { Button } from "@/src/components/ui/button";
import { api } from "@/src/utils/api";
import { useHasAccess } from "@/src/features/rbac/utils/checkAccess";
import { useEffect, useState } from "react";
export function BookmarkTrace({
traceId,
projectId,
isBookmarked,
size = "sm",
}: {
traceId: string;
projectId: string;
isBookmarked: boolean;
size?: "sm" | "xs";
}) {
const utils = api.useUtils();
const [cacheIsBookmarked, setCacheIsBookmarked] = useState<boolean | null>(
null,
);
const localIsBookmarked = cacheIsBookmarked ?? isBookmarked;
const hasAccess = useHasAccess({ projectId, scope: "traces:bookmark" });
const mutBookmarkTrace = api.traces.bookmark.useMutation({
onSuccess: () => {
setCacheIsBookmarked(!localIsBookmarked);
void utils.traces.invalidate();
},
});
useEffect(() => {
setCacheIsBookmarked(null);
}, [isBookmarked]);
const handleBookmarkClick = () => {
if (!hasAccess) return;
void mutBookmarkTrace.mutateAsync({
traceId,
projectId,
bookmarked: !localIsBookmarked,
});
};
return (
<Button
variant="ghost"
size={size}
onClick={handleBookmarkClick}
disabled={!hasAccess}
loading={mutBookmarkTrace.isLoading}
>
<StarIcon
className={`h-4 w-4 ${
localIsBookmarked ? "fill-current text-yellow-500" : "text-gray-500"
}`}
/>
</Button>
);
}
+80
View File
@@ -0,0 +1,80 @@
import { TrashIcon } from "lucide-react";
import { useRouter } from "next/router";
import { Button } from "@/src/components/ui/button";
import { api } from "@/src/utils/api";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/src/components/ui/popover";
import { useHasAccess } from "@/src/features/rbac/utils/checkAccess";
import { useState } from "react";
export function DeleteTrace({
traceId,
projectId,
isTableAction,
}: {
traceId: string;
projectId: string;
isTableAction?: boolean;
}) {
const [isDeleted, setIsDeleted] = useState(false);
const router = useRouter();
const utils = api.useUtils();
const hasAccess = useHasAccess({ projectId, scope: "traces:delete" });
const mutDeleteTrace = api.traces.delete.useMutation({
onSuccess: () => {
setIsDeleted(true);
if (!isTableAction) {
void router
.push(`/project/${projectId}/traces`)
.then(() => utils.traces.invalidate());
} else {
void utils.traces.invalidate();
}
},
});
if (!hasAccess) {
return null;
}
return (
<Popover key={traceId}>
<PopoverTrigger asChild>
{isTableAction ? (
<Button variant="ghost" size="xs">
<TrashIcon className="h-4 w-4" />
</Button>
) : (
<Button variant="outline" type="button">
<TrashIcon className="h-4 w-4" />
</Button>
)}
</PopoverTrigger>
<PopoverContent>
<h2 className="text-md mb-3 font-semibold">Please confirm</h2>
<p className="mb-3 text-sm">
This action cannot be undone and removes all the data associated with
this trace.
</p>
<div className="flex justify-end space-x-4">
<Button
type="button"
variant="destructive"
loading={mutDeleteTrace.isLoading || isDeleted}
onClick={() =>
void mutDeleteTrace.mutateAsync({ traceId, projectId })
}
>
Delete trace
</Button>
</div>
</PopoverContent>
</Popover>
);
}
+14 -17
View File
@@ -8,30 +8,27 @@ import Link from "next/link";
export type DocPopupProps = {
description: React.ReactNode;
link: string;
href: string;
size?: "sm" | "md" | "lg";
};
export default function DocPopup({ description, link, size }: DocPopupProps) {
let sizeClass = "w-4 h-4";
switch (size) {
case "sm": {
sizeClass = "w-4 h-4";
}
case "md": {
sizeClass = "w-6 h-6";
}
case "lg": {
sizeClass = "w-8 h-8";
}
}
export default function DocPopup({
description,
href,
size = "sm",
}: DocPopupProps) {
const sizes = {
sm: "w-4 h-4",
md: "w-6 h-6",
lg: "w-8 h-8",
};
return (
<HoverCard openDelay={200}>
<HoverCardTrigger className="mx-2 cursor-pointer">
<Link href={link} rel="noopener" target="_blank">
<HoverCardTrigger className="mx-2 cursor-pointer" asChild>
<Link href={href} rel="noopener" target="_blank">
<div className="whitespace-nowrap text-gray-500 sm:pl-0">
<HelpCircle className={sizeClass} />
<HelpCircle className={sizes[size]} />
</div>
</Link>
</HoverCardTrigger>
+22 -2
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,24 @@ 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 breadcrumb = [
...(project && projectId && currentPath !== "/project/[projectId]"
? [
{
name: project.name,
href: `/project/${projectId as string}`,
},
]
: []),
...(props.breadcrumb ?? []),
];
const backHref =
props.breadcrumb &&
[...props.breadcrumb.map((i) => i.href).filter(Boolean)].pop();
@@ -28,10 +48,10 @@ export default function Header(props: {
</Link>
</nav>
) : null}
{props.breadcrumb ? (
{breadcrumb ? (
<nav className="hidden sm:flex" aria-label="Breadcrumb">
<ol role="list" className="flex items-center space-x-4">
{props.breadcrumb.map(({ name, href }, index) => (
{breadcrumb.map(({ name, href }, index) => (
<li key={index}>
<div className="flex items-center">
{index !== 0 && (
+22 -30
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}`}
@@ -294,10 +293,10 @@ export default function Layout(props: PropsWithChildren) {
{/* Static sidebar for desktop */}
<div className="hidden xl:fixed xl:inset-y-0 xl:z-50 xl:flex xl:w-72 xl:flex-col">
{/* Sidebar component, swap this element with another sidebar if you like */}
<div className="flex grow flex-col gap-y-5 overflow-y-auto border-r border-gray-200 bg-white px-6 pb-3 pt-7">
<LangfuseLogo size="xl" className="mb-4" />
<nav className="flex flex-1 flex-col">
<ul role="list" className="flex flex-1 flex-col gap-y-4">
<div className="flex h-screen grow flex-col gap-y-5 border-r border-gray-200 bg-white pt-7">
<LangfuseLogo size="xl" className="mb-2 px-6" />
<nav className="flex h-full flex-1 flex-col overflow-y-auto px-6 pb-3">
<ul role="list" className="flex h-full flex-col gap-y-4">
<li>
<ul role="list" className="-mx-2 space-y-1">
{navigation.map((item) => (
@@ -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}`}
@@ -389,7 +388,7 @@ export default function Layout(props: PropsWithChildren) {
</ul>
</li>
<li className="-mx-6 ">
<li className="-mx-6">
<Menu as="div" className="relative">
<Menu.Button className="flex w-full items-center gap-x-4 p-1.5 px-6 py-3 text-sm font-semibold leading-6 text-gray-900 hover:bg-gray-50">
<span className="sr-only">Open user menu</span>
@@ -507,6 +506,8 @@ export default function Layout(props: PropsWithChildren) {
<div className="xl:pl-72">
{env.NEXT_PUBLIC_DEMO_PROJECT_ID &&
projectId === env.NEXT_PUBLIC_DEMO_PROJECT_ID &&
env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION &&
env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION !== "STAGING" &&
!session.data?.user?.email?.endsWith("@langfuse.com") ? (
<div className="flex w-full items-center border-b border-yellow-500 bg-yellow-100 px-4 py-2 xl:sticky xl:top-0 xl:z-40">
<div className="flex flex-1 flex-wrap gap-1">
@@ -516,9 +517,13 @@ export default function Layout(props: PropsWithChildren) {
</div>
<div>Live data from the Langfuse Q&A Chatbot.</div>
</div>
<Button size="sm" variant="ghost" asChild className="ml-2">
<Link href="https://langfuse.com/docs/demo" target="_blank">
Learn more
<Button size="sm" asChild className="ml-2">
<Link
href="https://langfuse.com/docs/qa-chatbot"
target="_blank"
>
Q&A Chatbot
</Link>
</Button>
</div>
@@ -531,16 +536,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>
);
}
@@ -0,0 +1,77 @@
import React, {
useCallback,
type Dispatch,
type SetStateAction,
useState,
} from "react";
import { Button } from "@/src/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuTrigger,
DropdownMenuCheckboxItem,
} from "@/src/components/ui/dropdown-menu";
import { type ColumnDef, type VisibilityState } from "@tanstack/react-table";
import { ChevronDownIcon } from "lucide-react";
interface DataTableColumnVisibilityFilterProps<TData, TValue> {
columns: ColumnDef<TData, TValue>[];
columnVisibility: VisibilityState;
setColumnVisibility: Dispatch<SetStateAction<VisibilityState>>;
}
export function DataTableColumnVisibilityFilter<TData, TValue>({
columns,
columnVisibility,
setColumnVisibility,
}: DataTableColumnVisibilityFilterProps<TData, TValue>) {
const [isOpen, setIsOpen] = useState(false);
const toggleColumn = useCallback(
(columnId: string) => {
setColumnVisibility((old) => ({
...old,
[columnId]: !old[columnId],
}));
},
[setColumnVisibility],
);
return (
<DropdownMenu open={isOpen}>
<DropdownMenuTrigger
onClick={() => {
setIsOpen(!isOpen);
}}
className="select-none"
asChild
>
<Button variant="outline" className="ml-auto">
Columns
<ChevronDownIcon className="ml-2 h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent
align="end"
onPointerDownOutside={() => setIsOpen(false)}
>
{columns.map(
(column, index) =>
"accessorKey" in column &&
column.enableHiding && (
<DropdownMenuCheckboxItem
key={index}
className="capitalize"
checked={columnVisibility[column.accessorKey]}
onCheckedChange={() =>
toggleColumn(column.accessorKey.toString())
}
>
{column.accessorKey.toString()}
</DropdownMenuCheckboxItem>
),
)}
</DropdownMenuContent>
</DropdownMenu>
);
}
+20 -10
View File
@@ -1,9 +1,11 @@
import { Button } from "@/src/components/ui/button";
import React, { type Dispatch, type SetStateAction, useState } from "react";
import { Input } from "@/src/components/ui/input";
import { DataTableColumnVisibilityFilter } from "@/src/components/table/data-table-column-visibility-filter";
import { type FilterState } from "@/src/features/filters/types";
import { FilterBuilder } from "@/src/features/filters/components/filter-builder";
import { type ColumnDefinition } from "@/src/server/api/interfaces/tableDefinition";
import { type ColumnDef, type VisibilityState } from "@tanstack/react-table";
interface SearchConfig {
placeholder: string;
@@ -11,21 +13,27 @@ interface SearchConfig {
currentQuery?: string;
}
interface DataTableToolbarProps {
interface DataTableToolbarProps<TData, TValue> {
columns: ColumnDef<TData, TValue>[];
filterColumnDefinition: ColumnDefinition[];
searchConfig?: SearchConfig;
actionButtons?: React.ReactNode;
filterState: FilterState;
setFilterState: Dispatch<SetStateAction<FilterState>>;
columnVisibility: VisibilityState;
setColumnVisibility: Dispatch<SetStateAction<VisibilityState>>;
}
export function DataTableToolbar({
export function DataTableToolbar<TData, TValue>({
columns,
filterColumnDefinition,
searchConfig,
actionButtons,
filterState,
setFilterState,
}: DataTableToolbarProps) {
columnVisibility,
setColumnVisibility,
}: DataTableToolbarProps<TData, TValue>) {
const [searchString, setSearchString] = useState(
searchConfig?.currentQuery ?? "",
);
@@ -33,33 +41,35 @@ export function DataTableToolbar({
return (
<div className="my-2 flex max-w-full items-center justify-between overflow-x-auto">
<div className="flex flex-1 items-center space-x-2">
{searchConfig ? (
{searchConfig && (
<div className="flex max-w-md items-center space-x-2">
<Input
autoFocus
placeholder={searchConfig.placeholder}
value={searchString}
onChange={(event) => setSearchString(event.currentTarget.value)}
className="h-10 w-[200px] lg:w-[350px]"
onChange={(event) => {
setSearchString(event.currentTarget.value);
}}
/>
<Button
variant="outline"
type="submit"
onClick={() => searchConfig.updateQuery(searchString)}
>
Search
</Button>
</div>
) : undefined}
)}
<FilterBuilder
columns={filterColumnDefinition}
filterState={filterState}
onChange={setFilterState}
/>
<div className="flex-1" />
{actionButtons ? actionButtons : null}
<DataTableColumnVisibilityFilter
columns={columns}
columnVisibility={columnVisibility}
setColumnVisibility={setColumnVisibility}
/>
{actionButtons}
</div>
</div>
);
+9 -3
View File
@@ -9,6 +9,7 @@ import {
getFilteredRowModel,
type OnChangeFn,
type PaginationState,
type VisibilityState,
} from "@tanstack/react-table";
import {
Table,
@@ -29,6 +30,8 @@ interface DataTableProps<TData, TValue> {
onChange: OnChangeFn<PaginationState>;
state: PaginationState;
};
columnVisibility?: VisibilityState;
onColumnVisibilityChange?: OnChangeFn<VisibilityState>;
}
export interface AsyncTableData<T> {
@@ -42,9 +45,10 @@ export function DataTable<TData, TValue>({
columns,
data,
pagination,
columnVisibility,
onColumnVisibilityChange,
}: DataTableProps<TData, TValue>) {
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
const table = useReactTable({
data: data.data ?? [],
columns,
@@ -54,9 +58,11 @@ export function DataTable<TData, TValue>({
manualPagination: pagination !== undefined,
pageCount: pagination?.pageCount ?? 0,
onPaginationChange: pagination?.onChange,
onColumnVisibilityChange: onColumnVisibilityChange,
state: {
columnFilters,
pagination: pagination?.state,
columnVisibility,
},
manualFiltering: true,
});
@@ -70,7 +76,7 @@ export function DataTable<TData, TValue>({
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => {
return (
return header.column.getIsVisible() ? (
<TableHead
key={header.id}
className="whitespace-nowrap p-2"
@@ -82,7 +88,7 @@ export function DataTable<TData, TValue>({
header.getContext(),
)}
</TableHead>
);
) : null;
})}
</TableRow>
))}
@@ -0,0 +1,325 @@
import { api, directApi } from "@/src/utils/api";
import { type ColumnDef } from "@tanstack/react-table";
import { DataTable } from "@/src/components/table/data-table";
import TableLink from "@/src/components/table/table-link";
import { DataTableToolbar } from "@/src/components/table/data-table-toolbar";
import { useState } from "react";
import { TokenUsageBadge } from "@/src/components/token-usage-badge";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/src/components/ui/dropdown-menu";
import { Button } from "@/src/components/ui/button";
import { ChevronDownIcon, Loader } from "lucide-react";
import { type ExportFileFormats } from "@/src/server/api/routers/generations";
import { usePostHog } from "posthog-js/react";
import {
NumberParam,
StringParam,
useQueryParam,
useQueryParams,
withDefault,
} from "use-query-params";
import { useQueryFilterState } from "@/src/features/filters/hooks/useFilterState";
import { observationsTableColsWithOptions } from "@/src/server/api/definitions/observationsTable";
import { utcDateOffsetByDays } from "@/src/utils/dates";
import useColumnVisibility from "@/src/features/column-visibility/hooks/useColumnVisibility";
export type GenerationsTableRow = {
id: string;
traceId: string;
startTime: string;
endTime?: string;
latency?: number;
name?: string;
model?: string;
traceName?: string;
usage: {
promptTokens: number;
completionTokens: number;
totalTokens: number;
};
};
const exportOptions: Record<
ExportFileFormats,
{
label: string;
extension: string;
fileType: string;
}
> = {
CSV: { label: "CSV", extension: "csv", fileType: "text/csv" },
JSON: { label: "JSON", extension: "json", fileType: "application/json" },
"OPENAI-JSONL": {
label: "OpenAI JSONL (fine-tuning)",
extension: "jsonl",
fileType: "application/json",
},
} as const;
export type GenerationsTableProps = {
projectId: string;
};
export default function GenerationsTable({ projectId }: GenerationsTableProps) {
const posthog = usePostHog();
const [isExporting, setIsExporting] = useState(false);
const [searchQuery, setSearchQuery] = useQueryParam(
"search",
withDefault(StringParam, null),
);
const [paginationState, setPaginationState] = useQueryParams({
pageIndex: withDefault(NumberParam, 0),
pageSize: withDefault(NumberParam, 50),
});
const [filterState, setFilterState] = useQueryFilterState([
{
column: "start_time",
type: "datetime",
operator: ">",
value: utcDateOffsetByDays(-14),
},
]);
const generations = api.generations.all.useQuery({
page: paginationState.pageIndex,
limit: paginationState.pageSize,
projectId,
filter: filterState,
searchQuery,
});
const totalCount = generations.data?.slice(1)[0]?.totalCount ?? 0;
const filterOptions = api.generations.filterOptions.useQuery({
projectId,
});
const handleExport = async (fileFormat: ExportFileFormats) => {
if (isExporting) return;
setIsExporting(true);
posthog.capture("generations:export", { file_format: fileFormat });
const fileData = await directApi.generations.export.query({
projectId,
fileFormat,
filter: filterState,
searchQuery,
});
if (fileData) {
const file = new File(
[fileData],
`generations.${exportOptions[fileFormat].extension}`,
{
type: exportOptions[fileFormat].fileType,
},
);
// create url from file
const url = URL.createObjectURL(file);
// Use a dynamically created anchor element to trigger the download
const a = document.createElement("a");
document.body.appendChild(a);
a.href = url;
a.download = `generations.${exportOptions[fileFormat].extension}`; // name of the downloaded file
a.click();
a.remove();
// Revoke the blob URL after using it
setTimeout(() => URL.revokeObjectURL(url), 100);
}
setIsExporting(false);
};
const columns: ColumnDef<GenerationsTableRow>[] = [
{
accessorKey: "id",
header: "ID",
cell: ({ row }) => {
const observationId = row.getValue("id");
const traceId = row.getValue("traceId");
return typeof observationId === "string" &&
typeof traceId === "string" ? (
<TableLink
path={`/project/${projectId}/traces/${traceId}?observation=${observationId}`}
value={observationId}
/>
) : null;
},
},
{
accessorKey: "name",
header: "name",
},
{
accessorKey: "traceId",
header: "Trace ID",
cell: ({ row }) => {
const value = row.getValue("traceId");
return typeof value === "string" ? (
<TableLink
path={`/project/${projectId}/traces/${value}`}
value={value}
/>
) : undefined;
},
},
{
accessorKey: "traceName",
header: "Trace Name",
enableHiding: true,
},
{
accessorKey: "startTime",
header: "Start Time",
enableHiding: true,
},
{
accessorKey: "latency",
header: "Latency",
cell: ({ row }) => {
const value: number | undefined = row.getValue("latency");
return value !== undefined ? (
<span>{value.toFixed(2)} sec</span>
) : undefined;
},
enableHiding: true,
},
{
accessorKey: "model",
header: "Model",
enableHiding: true,
},
{
accessorKey: "usage",
header: "Usage",
cell: ({ row }) => {
const value: {
promptTokens: number;
completionTokens: number;
totalTokens: number;
} = row.getValue("usage");
return (
<TokenUsageBadge
promptTokens={value.promptTokens}
completionTokens={value.completionTokens}
totalTokens={value.totalTokens}
inline
/>
);
},
enableHiding: true,
},
{
accessorKey: "version",
header: "Version",
enableHiding: true,
},
];
const [columnVisibility, setColumnVisibility] =
useColumnVisibility<GenerationsTableRow>(
"generationsColumnVisibility",
columns,
);
const rows: GenerationsTableRow[] = generations.isSuccess
? generations.data.map((generation) => ({
id: generation.id,
traceId: generation.traceId,
traceName: generation.traceName,
startTime: generation.startTime.toLocaleString(),
endTime: generation.endTime?.toLocaleString() ?? undefined,
latency: generation.latency === null ? undefined : generation.latency,
name: generation.name ?? undefined,
version: generation.version ?? "",
model: generation.model ?? "",
usage: {
promptTokens: generation.promptTokens,
completionTokens: generation.completionTokens,
totalTokens: generation.totalTokens,
},
}))
: [];
return (
<div>
<DataTableToolbar
columns={columns}
filterColumnDefinition={observationsTableColsWithOptions(
filterOptions.data,
)}
filterState={filterState}
setFilterState={setFilterState}
searchConfig={{
placeholder: "Search by id, name, traceName, model",
updateQuery: setSearchQuery,
currentQuery: searchQuery ?? undefined,
}}
columnVisibility={columnVisibility}
setColumnVisibility={setColumnVisibility}
actionButtons={
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="outline"
className="ml-auto whitespace-nowrap"
size="sm"
>
{filterState.length > 0 || searchQuery
? "Export selection"
: "Export all"}{" "}
{isExporting ? (
<Loader className="ml-2 h-4 w-4 animate-spin" />
) : (
<ChevronDownIcon className="ml-2 h-4 w-4" />
)}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
{Object.entries(exportOptions).map(([key, options]) => (
<DropdownMenuItem
key={key}
className="capitalize"
onClick={() => void handleExport(key as ExportFileFormats)}
>
as {options.label}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
}
/>
<DataTable
columns={columns}
data={
generations.isLoading
? { isLoading: true, isError: false }
: generations.isError
? {
isLoading: false,
isError: true,
error: generations.error.message,
}
: {
isLoading: false,
isError: false,
data: rows,
}
}
pagination={{
pageCount: Math.ceil(totalCount / paginationState.pageSize),
onChange: setPaginationState,
state: paginationState,
}}
columnVisibility={columnVisibility}
onColumnVisibilityChange={setColumnVisibility}
/>
</div>
);
}
+26 -13
View File
@@ -1,6 +1,7 @@
import { DataTable } from "@/src/components/table/data-table";
import { DataTableToolbar } from "@/src/components/table/data-table-toolbar";
import TableLink from "@/src/components/table/table-link";
import useColumnVisibility from "@/src/features/column-visibility/hooks/useColumnVisibility";
import { useQueryFilterState } from "@/src/features/filters/hooks/useFilterState";
import { scoresTableColsWithOptions } from "@/src/server/api/definitions/scoresTable";
import { api } from "@/src/utils/api";
@@ -9,7 +10,7 @@ import { type Score } from "@prisma/client";
import { type ColumnDef } from "@tanstack/react-table";
import { useQueryParams, withDefault, NumberParam } from "use-query-params";
type RowData = {
export type ScoresTableRow = {
id: string;
traceId: string;
timestamp: string;
@@ -60,7 +61,7 @@ export default function ScoresTable({
projectId,
});
const columns: ColumnDef<RowData>[] = [
const columns: ColumnDef<ScoresTableRow>[] = [
{
accessorKey: "traceId",
enableColumnFilter: true,
@@ -95,22 +96,29 @@ export default function ScoresTable({
{
accessorKey: "timestamp",
header: "Timestamp",
enableHiding: true,
},
{
accessorKey: "name",
header: "Name",
enableHiding: true,
},
{
accessorKey: "value",
header: "Value",
enableHiding: true,
},
{
accessorKey: "comment",
header: "Comment",
enableHiding: true,
},
];
const convertToTableRow = (score: Score): RowData => {
const [columnVisibility, setColumnVisibility] =
useColumnVisibility<ScoresTableRow>("scoresColumnVisibility", columns);
const convertToTableRow = (score: Score): ScoresTableRow => {
return {
id: score.id,
timestamp: score.timestamp.toLocaleString(),
@@ -125,9 +133,12 @@ export default function ScoresTable({
return (
<div>
<DataTableToolbar
columns={columns}
filterColumnDefinition={scoresTableColsWithOptions(filterOptions.data)}
filterState={userFilterState}
setFilterState={setUserFilterState}
columnVisibility={columnVisibility}
setColumnVisibility={setColumnVisibility}
/>
<DataTable
columns={columns}
@@ -135,22 +146,24 @@ export default function ScoresTable({
scores.isLoading
? { isLoading: true, isError: false }
: scores.isError
? {
isLoading: false,
isError: true,
error: scores.error.message,
}
: {
isLoading: false,
isError: false,
data: scores.data?.map((t) => convertToTableRow(t)),
}
? {
isLoading: false,
isError: true,
error: scores.error.message,
}
: {
isLoading: false,
isError: false,
data: scores.data?.map((t) => convertToTableRow(t)),
}
}
pagination={{
pageCount: Math.ceil(totalCount / paginationState.pageSize),
onChange: setPaginationState,
state: paginationState,
}}
columnVisibility={columnVisibility}
onColumnVisibilityChange={setColumnVisibility}
/>
</div>
);
+69 -17
View File
@@ -1,8 +1,10 @@
import { DeleteTrace } from "@/src/components/delete-trace";
import { GroupedScoreBadges } from "@/src/components/grouped-score-badge";
import { DataTable } from "@/src/components/table/data-table";
import { DataTableToolbar } from "@/src/components/table/data-table-toolbar";
import TableLink from "@/src/components/table/table-link";
import { TokenUsageBadge } from "@/src/components/token-usage-badge";
import useColumnVisibility from "@/src/features/column-visibility/hooks/useColumnVisibility";
import { useQueryFilterState } from "@/src/features/filters/hooks/useFilterState";
import { type FilterState } from "@/src/features/filters/types";
import { useDetailPageLists } from "@/src/features/navigate-detail-pages/context";
@@ -20,8 +22,10 @@ import {
useQueryParams,
withDefault,
} from "use-query-params";
import { BookmarkTrace } from "@/src/components/bookmark-trace";
export type TraceTableRow = {
export type TracesTableRow = {
bookmarked: boolean;
id: string;
timestamp: string;
name: string;
@@ -38,7 +42,7 @@ export type TraceTableRow = {
};
};
export type TraceTableProps = {
export type TracesTableProps = {
projectId: string;
userId?: string;
omittedFilter?: string[];
@@ -50,13 +54,12 @@ export default function TracesTable({
projectId,
userId,
omittedFilter = [],
}: TraceTableProps) {
}: TracesTableProps) {
const { setDetailPageList } = useDetailPageLists();
const [searchQuery, setSearchQuery] = useQueryParam(
"search",
withDefault(StringParam, null),
);
const [userFilterState, setUserFilterState] = useQueryFilterState([
{
column: "timestamp",
@@ -78,7 +81,6 @@ export default function TracesTable({
: [];
const filterState = userFilterState.concat(userIdFilter);
const [paginationState, setPaginationState] = useQueryParams({
pageIndex: withDefault(NumberParam, 0),
pageSize: withDefault(NumberParam, 50),
@@ -108,8 +110,9 @@ export default function TracesTable({
const convertToTableRow = (
trace: RouterOutput["traces"]["all"][0],
): TraceTableRow => {
): TracesTableRow => {
return {
bookmarked: trace.bookmarked,
id: trace.id,
timestamp: trace.timestamp.toLocaleString(),
name: trace.name ?? "",
@@ -127,7 +130,25 @@ export default function TracesTable({
};
};
const columns: ColumnDef<TraceTableRow>[] = [
const columns: ColumnDef<TracesTableRow>[] = [
{
accessorKey: "bookmarked",
header: undefined,
cell: ({ row }) => {
const isBookmarked = row.getValue("bookmarked");
const traceId = row.getValue("id");
return typeof traceId === "string" &&
typeof isBookmarked === "boolean" ? (
<BookmarkTrace
traceId={traceId}
projectId={projectId}
isBookmarked={isBookmarked}
size="xs"
/>
) : undefined;
},
},
{
accessorKey: "id",
header: "ID",
@@ -144,10 +165,12 @@ export default function TracesTable({
{
accessorKey: "timestamp",
header: "Timestamp",
enableHiding: true,
},
{
accessorKey: "name",
header: "Name",
enableHiding: true,
},
{
accessorKey: "userId",
@@ -163,6 +186,7 @@ export default function TracesTable({
/>
) : undefined;
},
enableHiding: true,
},
{
accessorKey: "latency",
@@ -172,6 +196,7 @@ export default function TracesTable({
const value: number | undefined = row.getValue("latency");
return value !== undefined ? `${value.toFixed(2)} sec` : undefined;
},
enableHiding: true,
},
{
accessorKey: "usage",
@@ -191,6 +216,7 @@ export default function TracesTable({
/>
);
},
enableHiding: true,
},
{
accessorKey: "scores",
@@ -200,6 +226,7 @@ export default function TracesTable({
const values: Score[] = row.getValue("scores");
return <GroupedScoreBadges scores={values} variant="headings" />;
},
enableHiding: true,
},
{
accessorKey: "metadata",
@@ -208,20 +235,41 @@ export default function TracesTable({
const values: string = row.getValue("metadata");
return <div className="flex flex-wrap gap-x-3 gap-y-1">{values}</div>;
},
enableHiding: true,
},
{
accessorKey: "version",
header: "Version",
enableHiding: true,
},
{
accessorKey: "release",
header: "Release",
enableHiding: true,
},
{
accessorKey: "action",
header: "Action",
cell: ({ row }) => {
const traceId = row.getValue("id");
return traceId && typeof traceId === "string" ? (
<DeleteTrace
traceId={traceId}
isTableAction={true}
projectId={projectId}
/>
) : undefined;
},
},
];
const [columnVisibility, setColumnVisibility] =
useColumnVisibility<TracesTableRow>("tracesColumnVisibility", columns);
return (
<div>
<DataTableToolbar
columns={columns}
filterColumnDefinition={tracesTableColsWithOptions(
traceFilterOptions.data,
)}
@@ -232,6 +280,8 @@ export default function TracesTable({
}}
filterState={userFilterState}
setFilterState={setUserFilterState}
columnVisibility={columnVisibility}
setColumnVisibility={setColumnVisibility}
/>
<DataTable
columns={columns}
@@ -239,22 +289,24 @@ export default function TracesTable({
traces.isLoading
? { isLoading: true, isError: false }
: traces.isError
? {
isLoading: false,
isError: true,
error: traces.error.message,
}
: {
isLoading: false,
isError: false,
data: traces.data?.map((t) => convertToTableRow(t)),
}
? {
isLoading: false,
isError: true,
error: traces.error.message,
}
: {
isLoading: false,
isError: false,
data: traces.data?.map((t) => convertToTableRow(t)),
}
}
pagination={{
pageCount: Math.ceil(totalCount / paginationState.pageSize),
onChange: setPaginationState,
state: paginationState,
}}
columnVisibility={columnVisibility}
onColumnVisibilityChange={setColumnVisibility}
/>
</div>
);
+18
View File
@@ -13,6 +13,8 @@ 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";
import { BookmarkTrace } from "@/src/components/bookmark-trace";
export function Trace(props: {
observations: Array<ObservationReturnType>;
@@ -88,8 +90,20 @@ export function TracePage({ traceId }: { traceId: string }) {
<div className="flex flex-col overflow-hidden xl:container lg:h-[calc(100vh-100px)] xl:h-[calc(100vh-50px)]">
<Header
title="Trace Detail"
breadcrumb={[
{
name: "Traces",
href: `/project/${router.query.projectId as string}/traces`,
},
{ name: traceId },
]}
actionButtons={
<>
<BookmarkTrace
traceId={trace.data.id}
projectId={trace.data.projectId}
isBookmarked={trace.data.bookmarked}
/>
<PublishTraceSwitch
traceId={trace.data.id}
projectId={trace.data.projectId}
@@ -102,6 +116,10 @@ export function TracePage({ traceId }: { traceId: string }) {
}
listKey="traces"
/>
<DeleteTrace
traceId={trace.data.id}
projectId={trace.data.projectId}
/>
</>
}
/>
+1 -1
View File
@@ -78,7 +78,7 @@ export { Button, buttonVariants };
function Spinner() {
return (
<div className="flex h-3/4 items-center justify-center">
<div className="flex h-1/2 items-center justify-center">
<Loader2 className="h-full w-full animate-spin" />
</div>
);
+31
View File
@@ -33,6 +33,7 @@ export function JSONView(props: {
collapseStringsAfterLength={500}
displaySize={"collapsed"}
matchesURL={true}
customizeCopy={(node) => stringifyJsonNode(node)}
/>
)}
</div>
@@ -97,3 +98,33 @@ export function CodeView(props: {
</div>
);
}
function stringifyJsonNode(node: unknown) {
// return single string nodes without quotes
if (typeof node === "string") {
return node;
}
try {
return JSON.stringify(
node,
(key, value) => {
switch (typeof value) {
case "bigint":
return String(value) + "n";
case "number":
case "boolean":
case "object":
case "string":
return value as string;
default:
return String(value);
}
},
4,
);
} catch (error) {
console.error("JSON stringify error", error);
return "Error: JSON.stringify failed";
}
}
+1
View File
@@ -0,0 +1 @@
export const VERSION = "v1.13.2";
+19 -1
View File
@@ -33,12 +33,21 @@ export const env = createEnv({
required_error:
"A strong Salt is required to encrypt API keys securely. See: https://langfuse.com/docs/deployment/self-host#deploy-the-container",
}),
// Add newly signed up users to default project with role
LANGFUSE_DEFAULT_PROJECT_ID: z.string().optional(),
LANGFUSE_DEFAULT_PROJECT_ROLE: z
.enum(["ADMIN", "MEMBER", "VIEWER"])
.optional(),
// AUTH
AUTH_GOOGLE_CLIENT_ID: z.string().optional(),
AUTH_GOOGLE_CLIENT_SECRET: z.string().optional(),
AUTH_GITHUB_CLIENT_ID: z.string().optional(),
AUTH_GITHUB_CLIENT_SECRET: z.string().optional(),
AUTH_DOMAINS_WITH_SSO_ENFORCEMENT: z.string().optional(),
AUTH_DISABLE_USERNAME_PASSWORD: z.enum(["true", "false"]).optional(),
// EMAIL
EMAIL_FROM_ADDRESS: z.string().optional(),
SMTP_CONNECTION_URL: z.string().optional(),
},
/**
@@ -48,7 +57,9 @@ export const env = createEnv({
*/
client: {
// NEXT_PUBLIC_CLIENTVAR: z.string().min(1),
NEXT_PUBLIC_LANGFUSE_CLOUD_REGION: z.enum(["US", "EU"]).optional(),
NEXT_PUBLIC_LANGFUSE_CLOUD_REGION: z
.enum(["US", "EU", "STAGING"])
.optional(),
NEXT_PUBLIC_DEMO_PROJECT_ID: z.string().optional(),
NEXT_PUBLIC_SIGN_UP_DISABLED: z.enum(["true", "false"]).optional(),
},
@@ -74,6 +85,9 @@ export const env = createEnv({
LANGFUSE_NEW_USER_SIGNUP_WEBHOOK:
process.env.LANGFUSE_NEW_USER_SIGNUP_WEBHOOK,
SALT: process.env.SALT,
// Default project and role
LANGFUSE_DEFAULT_PROJECT_ID: process.env.LANGFUSE_DEFAULT_PROJECT_ID,
LANGFUSE_DEFAULT_PROJECT_ROLE: process.env.LANGFUSE_DEFAULT_PROJECT_ROLE,
// AUTH
AUTH_GOOGLE_CLIENT_ID: process.env.AUTH_GOOGLE_CLIENT_ID,
AUTH_GOOGLE_CLIENT_SECRET: process.env.AUTH_GOOGLE_CLIENT_SECRET,
@@ -81,5 +95,9 @@ export const env = createEnv({
AUTH_GITHUB_CLIENT_SECRET: process.env.AUTH_GITHUB_CLIENT_SECRET,
AUTH_DOMAINS_WITH_SSO_ENFORCEMENT:
process.env.AUTH_DOMAINS_WITH_SSO_ENFORCEMENT,
AUTH_DISABLE_USERNAME_PASSWORD: process.env.AUTH_DISABLE_USERNAME_PASSWORD,
// Email
EMAIL_FROM_ADDRESS: process.env.EMAIL_FROM_ADDRESS,
SMTP_CONNECTION_URL: process.env.SMTP_CONNECTION_URL,
},
});
@@ -0,0 +1,33 @@
import { env } from "@/src/env.mjs";
export const CloudPrivacyNotice = ({ action }: { action: string }) =>
env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION !== undefined ? (
<div className="mt-10 text-center text-xs text-gray-500">
By {action} you are agreeing to our{" "}
<a
href="https://langfuse.com/tos"
target="_blank"
rel="noopener noreferrer"
className="italic"
>
Terms of Service
</a>
,{" "}
<a
href="https://langfuse.com/privacy"
rel="noopener noreferrer"
className="italic"
>
Privacy Policy
</a>
, and{" "}
<a
href="https://langfuse.com/cookie-policy"
rel="noopener noreferrer"
className="italic"
>
Cookie Policy
</a>
. You also confirm that the entered data is accurate.
</div>
) : null;
@@ -0,0 +1,83 @@
import { env } from "@/src/env.mjs";
import { Tabs, TabsList, TabsTrigger } from "@/src/components/ui/tabs";
import { Divider } from "@tremor/react";
import { usePostHog } from "posthog-js/react";
const regions =
env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION === "STAGING"
? [
{
name: "EU (Staging)",
hostname: "staging.langfuse.com",
flag: "🇪🇺",
},
]
: [
{
name: "US",
hostname: "us.cloud.langfuse.com",
flag: "🇺🇸",
},
{
name: "EU",
hostname: "cloud.langfuse.com",
flag: "🇪🇺",
},
];
export function CloudRegionSwitch({
isSignUpPage,
}: {
isSignUpPage?: boolean;
}) {
const posthog = usePostHog();
if (env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION === undefined) return null;
return (
<div>
<div className="flex w-full flex-col justify-between gap-3 md:flex-row md:items-center">
<div>
<span className="text-sm font-medium leading-none">Data Region</span>
<p className="text-xs text-gray-500">
Regions are strictly separated.
</p>
{env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION !== undefined &&
isSignUpPage ? (
<p className="text-xs text-gray-500">
{env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION === "EU"
? "✅ Demo project available"
: "❌ Choose EU for demo project access"}
</p>
) : null}
</div>
<Tabs value={env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION}>
<TabsList>
{regions.map((region) => (
<TabsTrigger
key={region.name}
value={region.name}
onClick={() => {
posthog.capture(
"cloud_region_switch",
{
region: region.name,
},
{
send_instantly: true,
},
);
window.location.hostname = region.hostname;
}}
>
<span className="mr-2 text-xl leading-none">{region.flag}</span>
{region.name}
</TabsTrigger>
))}
</TabsList>
</Tabs>
</div>
<Divider className="text-gray-400" />
</div>
);
}
@@ -0,0 +1,85 @@
import { env } from "@/src/env.mjs";
import { prisma } from "@/src/server/db";
export async function createProjectMembershipsOnSignup(user: {
id: string;
email: string | null;
}) {
try {
// Langfuse Cloud: Demo project access
const demoProjectId = env.NEXT_PUBLIC_DEMO_PROJECT_ID
? (
await prisma.project.findUnique({
where: {
id: env.NEXT_PUBLIC_DEMO_PROJECT_ID,
},
})
)?.id
: undefined;
if (demoProjectId !== undefined) {
await prisma.membership.create({
data: {
projectId: demoProjectId,
userId: user.id,
role: "VIEWER",
},
});
}
// set default project access
const defaultProjectID = env.LANGFUSE_DEFAULT_PROJECT_ID
? (
await prisma.project.findUnique({
where: {
id: env.LANGFUSE_DEFAULT_PROJECT_ID,
},
})
)?.id
: undefined;
if (defaultProjectID !== undefined) {
await prisma.membership.create({
data: {
projectId: defaultProjectID,
userId: user.id,
role: env.LANGFUSE_DEFAULT_PROJECT_ROLE ?? "VIEWER",
},
});
}
// Invites do not work for users without emails (some future SSO users)
if (user.email) await processMembershipInvitations(user.email, user.id);
} catch (e) {
console.error("Error assigning project access to new user", e);
}
}
async function processMembershipInvitations(email: string, userId: string) {
const invitationsForUser = await prisma.membershipInvitation.findMany({
where: {
email: email.toLowerCase(),
},
});
if (invitationsForUser.length > 0) {
const membershipsData = invitationsForUser.map((invitation) => {
return {
userId: userId,
projectId: invitation.projectId,
role: invitation.role,
};
});
await prisma.$transaction([
prisma.membership.createMany({
data: membershipsData,
}),
prisma.membershipInvitation.deleteMany({
where: {
id: {
in: invitationsForUser.map((invitation) => invitation.id),
},
email: email.toLowerCase(),
},
}),
]);
}
}
+4 -23
View File
@@ -1,4 +1,4 @@
import { env } from "@/src/env.mjs";
import { createProjectMembershipsOnSignup } from "@/src/features/auth/lib/createProjectMembershipsOnSignup";
import { prisma } from "@/src/server/db";
import { compare, hash } from "bcryptjs";
@@ -30,35 +30,16 @@ export async function createUserEmailPassword(
throw new Error("User with email already exists. Please sign in.");
}
// set demoProjectId if env exists and project exists in db
const demoProjectId = env.NEXT_PUBLIC_DEMO_PROJECT_ID
? (
await prisma.project.findUnique({
where: {
id: env.NEXT_PUBLIC_DEMO_PROJECT_ID,
},
})
)?.id
: undefined;
const newUser = await prisma.user.create({
data: {
email: email.toLowerCase(),
password: hashedPassword,
name,
// if demo project id is set grant user access to it
...(demoProjectId
? {
memberships: {
create: {
projectId: demoProjectId,
role: "VIEWER",
},
},
}
: undefined),
},
});
await createProjectMembershipsOnSignup(newUser);
return newUser.id;
}
@@ -0,0 +1,38 @@
import { useState, useEffect } from "react";
import { type ColumnDef, type VisibilityState } from "@tanstack/react-table";
function useColumnVisibility<TData>(
localStorageKey: string,
columns: ColumnDef<TData>[],
) {
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>(
() => {
const savedVisibility = localStorage.getItem(localStorageKey);
return savedVisibility
? (JSON.parse(savedVisibility) as VisibilityState)
: {};
},
);
useEffect(() => {
const localStorageItem = localStorage.getItem(localStorageKey);
if (!localStorageItem || localStorageItem === "{}") {
const initialVisibility: VisibilityState = {};
columns.forEach((column) => {
if ("accessorKey" in column && typeof column.accessorKey === "string") {
initialVisibility[column.accessorKey] = true;
}
});
setColumnVisibility(initialVisibility);
}
}, [columns, localStorageKey]);
useEffect(() => {
localStorage.setItem(localStorageKey, JSON.stringify(columnVisibility));
}, [columnVisibility, localStorageKey]);
return [columnVisibility, setColumnVisibility] as const;
}
export default useColumnVisibility;
@@ -69,7 +69,7 @@ export function ChartScores(props: {
<NoData noDataText="No data">
<DocPopup
description="Scores evaluate LLM quality and can be created manually or using the SDK."
link="https://langfuse.com/docs/scores"
href="https://langfuse.com/docs/scores"
/>
</NoData>
)}
@@ -79,7 +79,7 @@ export const MetricTable = ({
>
<DocPopup
description="Calculated multiplying the number of tokens with cost per token for each model."
link="https://langfuse.com/docs/token-usage"
href="https://langfuse.com/docs/token-usage"
/>
</TotalMetric>
</DashboardTable>
@@ -137,7 +137,7 @@ export const ScoresTable = ({
noDataChildren={
<DocPopup
description="Scores evaluate LLM quality and can be created manually or using the SDK."
link="https://langfuse.com/docs/scores"
href="https://langfuse.com/docs/scores"
/>
}
noDataClassName="mt-0"
@@ -82,7 +82,7 @@ export const TracesBarListChart = ({
<NoData noDataText="No data">
<DocPopup
description="Traces contain details about LLM applications and can be created using the SDK."
link="https://langfuse.com/docs/integrations/sdk#1-backend-tracing"
href="https://langfuse.com/docs/integrations/sdk#1-backend-tracing"
/>
</NoData>
)}
@@ -85,7 +85,7 @@ export const TracesTimeSeriesChart = ({
<NoData noDataText="No data available">
<DocPopup
description="Traces contain details about LLM applications and can be created using the SDK."
link="https://langfuse.com/docs/integrations/sdk#1-backend-tracing"
href="https://langfuse.com/docs/integrations/sdk#1-backend-tracing"
/>
</NoData>
)}
@@ -5,7 +5,6 @@ import { DashboardCard } from "@/src/features/dashboard/components/cards/Dashboa
import { compactNumberFormatter, usdFormatter } from "@/src/utils/numbers";
import { TabComponent } from "@/src/features/dashboard/components/TabsComponent";
import { BarList } from "@tremor/react";
import { isUndefinedOrNull } from "@/src/utils/types";
import { TotalMetric } from "@/src/features/dashboard/components/TotalMetric";
import { ExpandListButton } from "@/src/features/dashboard/components/cards/ChevronButton";
import { useState } from "react";
@@ -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(
@@ -151,7 +149,7 @@ export const UserChart = ({
<NoData noDataText="No data">
<DocPopup
description="Consumption per user is tracked by passing their ids on traces."
link="https://langfuse.com/docs/user-explorer"
href="https://langfuse.com/docs/user-explorer"
/>
</NoData>
)}
@@ -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
@@ -110,16 +110,16 @@ export function DatasetRunItemsTable(
runItems.isLoading
? { isLoading: true, isError: false }
: runItems.isError
? {
isLoading: false,
isError: true,
error: runItems.error.message,
}
: {
isLoading: false,
isError: false,
data: runItems.data?.map((t) => convertToTableRow(t)),
}
? {
isLoading: false,
isError: true,
error: runItems.error.message,
}
: {
isLoading: false,
isError: false,
data: runItems.data?.map((t) => convertToTableRow(t)),
}
}
/>
);
@@ -93,16 +93,16 @@ export function DatasetRunsTable(props: {
runs.isLoading
? { isLoading: true, isError: false }
: runs.isError
? {
isLoading: false,
isError: true,
error: runs.error.message,
}
: {
isLoading: false,
isError: false,
data: runs.data?.map((t) => convertToTableRow(t)),
}
? {
isLoading: false,
isError: true,
error: runs.error.message,
}
: {
isLoading: false,
isError: false,
data: runs.data?.map((t) => convertToTableRow(t)),
}
}
/>
);
@@ -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),
@@ -221,8 +221,8 @@ export const datasetRouter = createTRPCRouter({
input.expectedOutput === ""
? Prisma.DbNull
: input.expectedOutput !== undefined
? (JSON.parse(input.expectedOutput) as Prisma.InputJsonObject)
: undefined,
? (JSON.parse(input.expectedOutput) as Prisma.InputJsonObject)
: undefined,
sourceObservationId: input.sourceObservationId,
status: input.status,
},
@@ -302,8 +302,8 @@ export const datasetRouter = createTRPCRouter({
input.expectedOutput === ""
? Prisma.DbNull
: input.expectedOutput !== undefined
? (JSON.parse(input.expectedOutput) as Prisma.InputJsonObject)
: undefined,
? (JSON.parse(input.expectedOutput) as Prisma.InputJsonObject)
: undefined,
datasetId: input.datasetId,
sourceObservationId: input.sourceObservationId,
},
@@ -0,0 +1,56 @@
import { createTransport } from "nodemailer";
import { parseConnectionUrl } from "nodemailer/lib/shared/index.js";
import { render } from "@react-email/render";
import { env } from "@/src/env.mjs";
import ProjectInvitationTemplate from "@/src/features/email/templates/ProjectInvitation";
const langfuseUrls = {
US: "https://us.cloud.langfuse.com",
EU: "https://cloud.langfuse.com",
STAGING: "https://staging.langfuse.com",
};
const authUrl = env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION
? langfuseUrls[env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION]
: env.NEXTAUTH_URL;
export const sendProjectInvitation = async (
to: string,
inviterName: string,
inviterEmail: string,
projectName: string,
) => {
if (!env.EMAIL_FROM_ADDRESS || !env.SMTP_CONNECTION_URL) {
console.error(
"Missing environment variables for sending project invitation email.",
);
return;
}
try {
const mailer = createTransport(parseConnectionUrl(env.SMTP_CONNECTION_URL));
const htmlTemplate = render(
ProjectInvitationTemplate({
invitedByUsername: inviterName,
invitedByUserEmail: inviterEmail,
projectName: projectName,
recieverEmail: to,
inviteLink: authUrl,
}),
);
await mailer.sendMail({
to: to,
from: {
address: env.EMAIL_FROM_ADDRESS,
name: "Langfuse",
},
subject: `${inviterName} invited you to join "${projectName}"`,
html: htmlTemplate,
});
} catch (error) {
console.error(error);
}
};
@@ -0,0 +1,100 @@
import * as React from "react";
import {
Body,
Button,
Container,
Head,
Heading,
Hr,
Html,
Img,
Link,
Preview,
Section,
Tailwind,
Text,
} from "@react-email/components";
import { env } from "@/src/env.mjs";
interface ProjectInvitationTemplateProps {
invitedByUsername: string;
invitedByUserEmail: string;
projectName: string;
recieverEmail: string;
inviteLink: string;
}
export const ProjectInvitationTemplate = ({
invitedByUsername,
invitedByUserEmail,
projectName,
recieverEmail,
inviteLink,
}: ProjectInvitationTemplateProps) => {
const previewText = `Join ${invitedByUsername} on Langfuse`;
return (
<Html>
<Head />
<Preview>{previewText}</Preview>
<Tailwind>
<Body className="mx-auto my-auto bg-white font-sans">
<Container className="mx-auto my-10 w-[465px] rounded border border-solid border-[#eaeaea] p-5">
<Section className="mt-8">
<Img
src="https://static.langfuse.com/langfuse_logo_transactional_email.png"
width="40"
height="40"
alt="Langfuse"
className="mx-auto my-0"
/>
</Section>
<Heading className="mx-0 my-[30px] p-0 text-center text-2xl font-normal text-black">
Join <strong>{projectName}</strong> on <strong>Langfuse</strong>
</Heading>
<Text className="text-sm leading-6 text-black">Hello,</Text>
<Text className="text-sm leading-6 text-black">
<strong>{invitedByUsername}</strong> (
<Link
href={`mailto:${invitedByUserEmail}`}
className="text-blue-600 no-underline"
>
{invitedByUserEmail}
</Link>
) has invited you to the <strong>{projectName}</strong> project on
Langfuse.
</Text>
<Section className="mb-4 mt-8 text-center">
<Button
className="rounded bg-black px-5 py-3 text-center text-xs font-semibold text-white no-underline"
href={inviteLink}
>
Accept Invitation
</Button>
<Text className="mt-2 text-xs leading-3 text-gray-600">
(you need to create an account)
</Text>
</Section>
<Text className="text-sm leading-6 text-black">
or copy and paste this URL into your browser:{" "}
<Link href={inviteLink} className="text-blue-600 no-underline">
{inviteLink}
</Link>
</Text>
<Hr className="mx-0 my-[26px] w-full border border-solid border-[#eaeaea]" />
<Text className="text-xs leading-6 text-[#666666]">
This invitation was intended for{" "}
<span className="text-black">{recieverEmail}</span>. This invite
was sent from{" "}
<span className="text-black">{env.EMAIL_FROM_ADDRESS}</span>. If
you were not expecting this invitation, you can ignore this email.
</Text>
</Container>
</Body>
</Tailwind>
</Html>
);
};
export default ProjectInvitationTemplate;
@@ -58,11 +58,10 @@ export function FilterBuilder({
) => {
_setWipFilterState((prev) => {
const newState = state instanceof Function ? state(prev) : state;
onChange(
newState.filter(
(f) => singleFilter.safeParse(f).success,
) as FilterState,
);
const validFilters = newState.filter(
(f) => singleFilter.safeParse(f).success,
) as FilterState;
onChange(validFilters);
return newState;
});
};
@@ -97,11 +96,13 @@ export function FilterBuilder({
{filter.type === "datetime"
? new Date(filter.value).toLocaleDateString()
: filter.type === "stringOptions"
? filter.value.join(", ")
: filter.type === "number" ||
filter.type === "numberObject"
? filter.value
: `"${filter.value}"`}
? filter.value.join(", ")
: filter.type === "number" ||
filter.type === "numberObject"
? filter.value
: filter.type === "boolean"
? `${filter.value}`
: `"${filter.value}"`}
</span>
);
})
@@ -332,6 +333,33 @@ function FilterBuilderForm({
}
values={Array.isArray(filter.value) ? filter.value : []}
/>
) : filter.type === "boolean" ? (
<Select
onValueChange={(value) => {
handleFilterChange(
{
...filter,
value:
value !== undefined && value !== ""
? value === "true"
: undefined,
},
i,
);
}}
value={filter.value?.toString() ?? ""}
>
<SelectTrigger className="min-w-[60px]">
<SelectValue placeholder="" />
</SelectTrigger>
<SelectContent>
{["true", "false"].map((option) => (
<SelectItem key={option} value={option}>
{option}
</SelectItem>
))}
</SelectContent>
</Select>
) : (
<Input disabled />
)}
+10 -8
View File
@@ -22,8 +22,8 @@ const CommaArrayParam = {
f.type === "datetime"
? f.value.toISOString()
: f.type === "stringOptions"
? f.value.join("|")
: f.value
? f.value.join("|")
: f.value
}`;
if (DEBUG_QUERY_STATE) console.log("stringified", stringified);
return stringified;
@@ -42,12 +42,14 @@ const CommaArrayParam = {
value === undefined || type === undefined
? undefined
: type === "datetime"
? new Date(value)
: type === "number" || type === "numberObject"
? Number(value)
: type === "stringOptions"
? value.split("|")
: value;
? new Date(value)
: type === "number" || type === "numberObject"
? Number(value)
: type === "stringOptions"
? value.split("|")
: type === "boolean"
? value === "true"
: value;
if (DEBUG_QUERY_STATE) console.log("parsedValue", parsedValue);
const parsed = singleFilter.safeParse({
column,
+10 -2
View File
@@ -7,6 +7,7 @@ const operatorReplacements = {
"any of": "IN",
"none of": "NOT IN",
contains: "ILIKE",
"does not contain": "NOT ILIKE",
"starts with": "ILIKE",
"ends with": "ILIKE",
};
@@ -57,6 +58,9 @@ export function filterToPrismaSql(
filter.value.map((v) => Prisma.sql`${v}`),
)})`;
break;
case "boolean":
valuePrisma = Prisma.sql`${filter.value}`;
break;
}
const jsonKeyPrisma =
filter.type === "stringObject" || filter.type === "numberObject"
@@ -69,10 +73,14 @@ export function filterToPrismaSql(
const [valuePrefix, valueSuffix] =
filter.type === "string" || filter.type === "stringObject"
? [
["contains", "ends with"].includes(filter.operator)
["contains", "does not contain", "ends with"].includes(
filter.operator,
)
? Prisma.raw("'%' || ")
: Prisma.empty,
["contains", "starts with"].includes(filter.operator)
["contains", "does not contain", "starts with"].includes(
filter.operator,
)
? Prisma.raw(" || '%'")
: Prisma.empty,
]
+5 -5
View File
@@ -37,11 +37,11 @@ export function tokenCount(p: {
messages: p.text,
})
: isString(p.text)
? openAiStringTokenCount({ model: p.model, text: p.text })
: openAiStringTokenCount({
model: p.model,
text: JSON.stringify(p.text),
});
? openAiStringTokenCount({ model: p.model, text: p.text })
: openAiStringTokenCount({
model: p.model,
text: JSON.stringify(p.text),
});
} else if (isClaudeModel(p.model)) {
return isString(p.text)
? claudeStringTokenCount({ model: p.model, text: p.text })
@@ -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
@@ -32,7 +32,14 @@ export const DetailPageNav = (props: {
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
// don't trigger keyboard shortcuts if the user is typing in an input
if (document.activeElement instanceof HTMLInputElement) return;
if (
event.target instanceof HTMLInputElement ||
event.target instanceof HTMLTextAreaElement ||
(event.target instanceof HTMLElement &&
event.target.getAttribute("role") === "textbox")
) {
return;
}
if (event.key === "k" && previousPageId) {
void router.push(props.path(previousPageId));
@@ -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">
+4 -1
View File
@@ -125,6 +125,9 @@ async function findDbKeyOrThrow(publicKey: string) {
const dbKey = await prisma.apiKey.findUnique({
where: { publicKey },
});
if (!dbKey) throw new Error("Invalid public key");
if (!dbKey) {
console.log("No api key found for public key:", publicKey);
throw new Error("Invalid public key");
}
return dbKey;
}
@@ -10,6 +10,7 @@ export const TraceSchema = z.object({
metadata: jsonSchema.nullish(),
release: z.string().nullish(),
version: z.string().nullish(),
public: z.boolean().nullish(),
});
export const SpanPostSchema = z.object({
id: z.string().nullish(),
@@ -30,6 +31,7 @@ export const SpanPatchSchema = z.object({
spanId: z.string(),
traceId: z.string().nullish(),
name: z.string().nullish(),
startTime: z.string().datetime({ offset: true }).nullish(),
endTime: z.string().datetime({ offset: true }).nullish(),
metadata: jsonSchema.nullish(),
input: jsonSchema.nullish(),
@@ -72,6 +74,7 @@ export const GenerationPatchSchema = z.object({
generationId: z.string(),
traceId: z.string().nullish(),
name: z.string().nullish(),
startTime: z.string().datetime({ offset: true }).nullish(),
endTime: z.string().datetime({ offset: true }).nullish(),
completionStartTime: z.string().datetime({ offset: true }).nullish(),
model: z.string().nullish(),
@@ -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(),
});
@@ -1,9 +1,12 @@
import { throwIfNoAccess } from "@/src/features/rbac/utils/checkAccess";
import { createTRPCRouter, protectedProcedure } from "@/src/server/api/trpc";
import {
createTRPCRouter,
protectedProjectProcedure,
} from "@/src/server/api/trpc";
import * as z from "zod";
export const publishTracesRouter = createTRPCRouter({
update: protectedProcedure
update: protectedProjectProcedure
.input(
z.object({
projectId: z.string(),
@@ -51,9 +51,14 @@ 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(),
onError: (error) =>
form.setError("email", {
type: "manual",
message: error.message,
}),
});
const form = useForm<z.infer<typeof formSchema>>({
@@ -77,94 +82,93 @@ export function CreateProjectMemberButton(props: { projectId: string }) {
form.reset();
setOpen(false);
})
.catch(() => {
form.setError("email", {
type: "manual",
message: "User does not exist or already has access to this project",
});
.catch((error) => {
console.error(error);
});
}
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button variant="secondary" loading={mutCreateProjectMember.isLoading}>
<PlusIcon className="-ml-0.5 mr-1.5 h-5 w-5" aria-hidden="true" />
Add new member
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Add new member to project</DialogTitle>
</DialogHeader>
<Form {...form}>
<form
className="space-y-6"
// eslint-disable-next-line @typescript-eslint/no-misused-promises
onSubmit={form.handleSubmit(onSubmit)}
<>
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button
variant="secondary"
loading={mutCreateProjectMember.isLoading}
>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input placeholder="jsdoe@example.com" {...field} />
</FormControl>
<FormDescription>
User must already have an account
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="role"
render={({ field }) => (
<FormItem>
<FormLabel>Role</FormLabel>
<Select
defaultValue={field.value}
onValueChange={(value) =>
field.onChange(value as (typeof availableRoles)[number])
}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select a verified email to display" />
</SelectTrigger>
</FormControl>
<SelectContent>
{availableRoles.map((role) => (
<SelectItem value={role} key={role}>
{role}
</SelectItem>
))}
</SelectContent>
</Select>
<FormDescription>
Rights of role:{" "}
{roleAccessRights[field.value].length
? roleAccessRights[field.value].join(", ")
: "none"}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<Button
type="submit"
className="w-full"
loading={form.formState.isSubmitting}
<PlusIcon className="-ml-0.5 mr-1.5 h-5 w-5" aria-hidden="true" />
Add new member
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Add new member to project</DialogTitle>
</DialogHeader>
<Form {...form}>
<form
className="space-y-6"
// eslint-disable-next-line @typescript-eslint/no-misused-promises
onSubmit={form.handleSubmit(onSubmit)}
>
Grant access
</Button>
<FormMessage />
</form>
</Form>
</DialogContent>
</Dialog>
<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="role"
render={({ field }) => (
<FormItem>
<FormLabel>Role</FormLabel>
<Select
defaultValue={field.value}
onValueChange={(value) =>
field.onChange(value as (typeof availableRoles)[number])
}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select a verified email to display" />
</SelectTrigger>
</FormControl>
<SelectContent>
{availableRoles.map((role) => (
<SelectItem value={role} key={role}>
{role}
</SelectItem>
))}
</SelectContent>
</Select>
<FormDescription>
Rights of role:{" "}
{roleAccessRights[field.value].length
? roleAccessRights[field.value].join(", ")
: "none"}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<Button
type="submit"
className="w-full"
loading={form.formState.isSubmitting}
>
Grant access
</Button>
<FormMessage />
</form>
</Form>
</DialogContent>
</Dialog>
</>
);
}
@@ -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,8 +24,10 @@ export function ProjectMembersTable({ projectId }: { projectId: string }) {
scope: "members:delete",
});
const utils = api.useContext();
const memberships = api.projectMembers.get.useQuery(
const session = useSession();
const utils = api.useUtils();
const data = api.projectMembers.get.useQuery(
{
projectId: projectId,
},
@@ -32,9 +35,16 @@ export function ProjectMembersTable({ projectId }: { projectId: string }) {
enabled: hasReadAccess,
},
);
const memberships = data.data?.memberships ?? []; // Active Members
const invitations = data.data?.invitations ?? []; // Pending Members
const mutDeleteMembership = api.projectMembers.delete.useMutation({
onSuccess: () => utils.projectMembers.invalidate(),
});
const mutDeleteInvitation = api.projectMembers.deleteInvitation.useMutation({
onSuccess: () => utils.projectMembers.invalidate(),
});
if (!hasReadAccess) return null;
@@ -54,12 +64,14 @@ export function ProjectMembersTable({ projectId }: { projectId: string }) {
</TableRow>
</TableHeader>
<TableBody className="text-gray-500">
{memberships.data?.map((m) => (
{memberships.map((m) => (
<TableRow key={m.userId} className="hover:bg-transparent">
<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"
@@ -81,6 +93,53 @@ export function ProjectMembersTable({ projectId }: { projectId: string }) {
</TableBody>
</Table>
</Card>
{invitations.length > 0 ? (
<>
<h3 className="mb-3 text-sm font-semibold leading-4 text-gray-600">
Pending Invites
</h3>
<Card className="mb-4">
<Table className="text-sm">
<TableHeader>
<TableRow>
<TableHead className="text-gray-900">Email</TableHead>
<TableHead className="text-gray-900">Role</TableHead>
<TableHead className="text-gray-900">Sent by</TableHead>
{hasDeleteAccess ? <TableHead /> : null}
</TableRow>
</TableHeader>
<TableBody className="text-gray-500">
{invitations.map((invite) => (
<TableRow key={invite.id} className="hover:bg-transparent">
<TableCell>{invite.email}</TableCell>
<TableCell>{invite.role}</TableCell>
<TableCell>
{invite.sender ? invite.sender.name : ""}
</TableCell>
{hasDeleteAccess ? (
<TableCell>
<Button
variant="ghost"
size="xs"
loading={mutDeleteInvitation.isLoading}
onClick={() => {
mutDeleteInvitation.mutate({
id: invite.id,
projectId: projectId,
});
}}
>
<TrashIcon className="h-4 w-4" />
</Button>
</TableCell>
) : null}
</TableRow>
))}
</TableBody>
</Table>
</Card>
</>
) : null}
<CreateProjectMemberButton projectId={projectId} />
</div>
);
@@ -10,10 +10,13 @@ const scopes = [
"apiKeys:delete",
"traces:publish",
"traces:bookmark",
"traces:delete",
"scores:CUD",
"project:delete",
"project:transfer",
"datasets:CUD",
] as const;
@@ -30,8 +33,11 @@ export const roleAccessRights: Record<MembershipRole, Scope[]> = {
"apiKeys:create",
"apiKeys:delete",
"traces:publish",
"traces:bookmark",
"traces:delete",
"scores:CUD",
"project:delete",
"project:transfer",
"datasets:CUD",
],
ADMIN: [
@@ -42,10 +48,17 @@ export const roleAccessRights: Record<MembershipRole, Scope[]> = {
"apiKeys:create",
"apiKeys:delete",
"traces:publish",
"traces:bookmark",
"traces:delete",
"scores:CUD",
"datasets:CUD",
],
MEMBER: [
"members:read",
"traces:publish",
"traces:bookmark",
"scores:CUD",
"project:delete",
"datasets:CUD",
],
MEMBER: ["members:read", "traces:publish", "scores:CUD", "datasets:CUD"],
VIEWER: [],
};
@@ -1,10 +1,14 @@
import { sendProjectInvitation } from "@/src/features/email/lib/project-invitation";
import { throwIfNoAccess } from "@/src/features/rbac/utils/checkAccess";
import { createTRPCRouter, protectedProcedure } from "@/src/server/api/trpc";
import {
createTRPCRouter,
protectedProjectProcedure,
} from "@/src/server/api/trpc";
import { MembershipRole } from "@prisma/client";
import * as z from "zod";
export const projectMembersRouter = createTRPCRouter({
get: protectedProcedure
get: protectedProjectProcedure
.input(
z.object({
projectId: z.string(),
@@ -17,7 +21,7 @@ export const projectMembersRouter = createTRPCRouter({
scope: "members:read",
});
return await ctx.prisma.membership.findMany({
const memberships = await ctx.prisma.membership.findMany({
where: {
projectId: input.projectId,
project: {
@@ -38,8 +42,23 @@ export const projectMembersRouter = createTRPCRouter({
},
},
});
const invitations = await ctx.prisma.membershipInvitation.findMany({
where: {
projectId: input.projectId,
},
include: {
sender: {
select: {
name: true,
},
},
},
});
return { memberships: memberships, invitations: invitations };
}),
delete: protectedProcedure
delete: protectedProjectProcedure
.input(
z.object({
projectId: z.string(),
@@ -67,7 +86,28 @@ export const projectMembersRouter = createTRPCRouter({
},
});
}),
create: protectedProcedure
deleteInvitation: protectedProjectProcedure
.input(
z.object({
id: z.string(),
projectId: z.string(),
}),
)
.mutation(async ({ input, ctx }) => {
throwIfNoAccess({
session: ctx.session,
projectId: input.projectId,
scope: "members:delete",
});
return await ctx.prisma.membershipInvitation.delete({
where: {
id: input.id,
projectId: input.projectId,
},
});
}),
create: protectedProjectProcedure
.input(
z.object({
projectId: z.string(),
@@ -91,14 +131,40 @@ export const projectMembersRouter = createTRPCRouter({
email: input.email.toLowerCase(),
},
});
if (!user) throw new Error("User not found");
if (user) {
return await ctx.prisma.membership.create({
data: {
userId: user.id,
projectId: input.projectId,
role: input.role,
},
});
} else {
const invitation = await ctx.prisma.membershipInvitation.create({
data: {
projectId: input.projectId,
email: input.email.toLowerCase(),
role: input.role,
senderId: ctx.session.user.id,
},
});
return await ctx.prisma.membership.create({
data: {
userId: user.id,
projectId: input.projectId,
role: input.role,
},
});
const project = await ctx.prisma.project.findFirst({
where: {
id: input.projectId,
},
});
if (!project) throw new Error("Project not found");
await sendProjectInvitation(
input.email,
ctx.session.user.name!,
ctx.session.user.email!,
project.name,
);
return invitation;
}
}),
});

Some files were not shown because too many files have changed in this diff Show More