Compare commits

...
264 Commits
Author SHA1 Message Date
Marc Klingen 46b20f4107 chore: release v2.81.2
CI/CD / lint (push) Waiting to run
CI/CD / test-docker-build (push) Waiting to run
CI/CD / tests-web (node20, pg12) (push) Waiting to run
CI/CD / tests-web (node20, pg15) (push) Waiting to run
CI/CD / tests-worker (node20, pg12) (push) Waiting to run
CI/CD / tests-worker (node20, pg15) (push) Waiting to run
CI/CD / e2e-tests (push) Waiting to run
CI/CD / e2e-server-tests (push) Waiting to run
CI/CD / all-ci-passed (push) Blocked by required conditions
CI/CD / push-docker-image (push) Blocked by required conditions
release.yml / release (push) Waiting to run
2024-09-30 16:23:40 -07:00
Marc KlingenandGitHub fb8f26a00e feat(ee): add customizable default model adapter base urls (#3542) 2024-09-30 23:22:45 +00:00
Marc KlingenandGitHub 3d7ba475e2 chore(auth): reduce maxAge of password reset link to 10 minutes (#3541) 2024-09-30 21:40:18 +00:00
Marc KlingenandGitHub 03028a92b4 chore: upgrade fern openapi generator and cli (#3536) 2024-09-30 18:18:53 +02:00
Marc Klingen 8a96290f03 chore: release v2.81.1
CI/CD / lint (push) Waiting to run
CI/CD / test-docker-build (push) Waiting to run
CI/CD / tests-web (node20, pg12) (push) Waiting to run
CI/CD / tests-web (node20, pg15) (push) Waiting to run
CI/CD / tests-worker (node20, pg12) (push) Waiting to run
CI/CD / tests-worker (node20, pg15) (push) Waiting to run
CI/CD / e2e-tests (push) Waiting to run
CI/CD / e2e-server-tests (push) Waiting to run
CI/CD / all-ci-passed (push) Blocked by required conditions
CI/CD / push-docker-image (push) Blocked by required conditions
release.yml / release (push) Waiting to run
2024-09-27 17:05:22 +02:00
Marc KlingenandGitHub bca09bdefe fix(security): only allow redirect via target to internal urls (#3512) 2024-09-27 16:52:58 +02:00
Steffen SchmitzandGitHub 2d314250e9 chore: make credentials for export bucket optional (#3509) 2024-09-27 16:41:54 +02:00
Marc KlingenandGitHub 4a87f402ec fix: favicon reference when using custom basepath (#3506) 2024-09-27 13:10:21 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
800b2136b9 chore(deps-dev): bump @types/lodash from 4.17.7 to 4.17.9 in the lodash group (#3492)
chore(deps-dev): bump @types/lodash in the lodash group

Bumps the lodash group with 1 update: [@types/lodash](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/lodash).


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

---
updated-dependencies:
- dependency-name: "@types/lodash"
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: lodash
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-27 10:25:35 +02:00
Steffen SchmitzandGitHub 278df5ab3c feat: optionally upload all incoming events to S3 (#3488) 2024-09-27 06:58:34 +00:00
Marc KlingenandGitHub c7f9e18768 fix(ui): include non-generation warnings/error in rolled-up trace status (#3494) 2024-09-27 00:31:53 +00:00
Max DeichmannandGitHub 2121b40cca security: upgrade snyk (#3482) 2024-09-25 23:10:57 +02:00
Max DeichmannandGitHub 5ba440f2de chore: rate limit legacy endpoints (#3473) 2024-09-25 12:58:13 +00:00
Max DeichmannandGitHub 657a3275de chore: mark admin api as internal and cloud only (#3464) 2024-09-24 12:47:47 +00:00
Steffen SchmitzandGitHub 56bb9c8dcf chore: remove porter configs from CI (#3463) 2024-09-24 11:36:06 +00:00
Steffen SchmitzandGitHub 66e15bcf0d chore: drop redundant indexes on postgres (#3377) 2024-09-24 11:35:04 +02:00
Marc KlingenandGitHub 5f94ca8e82 fix: skip entitlement check for instance admins (#3452) 2024-09-23 16:55:06 +02:00
Steffen SchmitzandGitHub e8561fb105 chore: move web-ee logic from console.log to logger (#3400) 2024-09-23 16:54:45 +02:00
marliessophieandGitHub 18e84c6498 fix(ui_markdown): whitelist images with valid ipv6 or ipv4 hostname (#3444) 2024-09-23 14:12:37 +00:00
Marc KlingenandGitHub 05152ac8d6 fix(auth): email/password sign-up with custom base-path (#3450)
fix: email/password sign-up with custom base-path
2024-09-23 13:48:59 +00:00
marliessophieandGitHub 6cb9e24ba2 chore(ui): support larger step sizes in Lanfguse Trace Timeline View (#3449) 2024-09-23 12:53:33 +00:00
Marc Klingen 484fa424fb docs: update comments on stripe util functions 2024-09-23 14:18:37 +02:00
Hassieb Pakzad 555aebe8c7 chore: release v2.81.0
CI/CD / lint (push) Waiting to run
CI/CD / test-docker-build (push) Waiting to run
CI/CD / tests-web (node20, pg12) (push) Waiting to run
CI/CD / tests-web (node20, pg15) (push) Waiting to run
CI/CD / tests-worker (node20, pg12) (push) Waiting to run
CI/CD / tests-worker (node20, pg15) (push) Waiting to run
CI/CD / e2e-tests (push) Waiting to run
CI/CD / e2e-server-tests (push) Waiting to run
CI/CD / all-ci-passed (push) Blocked by required conditions
CI/CD / push-docker-image (push) Blocked by required conditions
release.yml / release (push) Waiting to run
2024-09-23 13:59:21 +02:00
Max DeichmannandGitHub af29424b48 feat: delete API keys via API (#3448) 2024-09-23 12:30:41 +02:00
Max DeichmannandGitHub 42f1c63bbf chore: set sentry config on build time (#3445) 2024-09-23 11:27:15 +02:00
Hassieb PakzadandGitHub 663ded0743 chore(evals): move to tool calling instead of function calling (#3430) 2024-09-23 11:08:23 +02:00
marliessophieandGitHub d7ced99d16 feat(ui): export generations async on cloud (#3368) 2024-09-23 06:03:03 +00:00
Max DeichmannandGitHub c979cae620 chore: add worker logs for tokenization (#3440) 2024-09-21 16:07:18 +00:00
marliessophieandGitHub c483293b10 style(prompts): display all prompt tags on versions, not just on hover (#3422) 2024-09-20 16:33:10 +00:00
Steffen SchmitzandGitHub 932913cd76 fix: use actual singletons for db clients (#3427) 2024-09-20 13:27:46 +02:00
Steffen SchmitzandGitHub 5875537132 fix: keep more failed jobs in bullmq (#3424) 2024-09-20 13:26:14 +02:00
Marc KlingenandGitHub 1a53981a7a docs: add comment on max aspect ratio of custom logo (#3426) 2024-09-20 09:48:25 +00:00
Hassieb PakzadandGitHub 0e6b676b19 fix(playground): parse complex messages when jumping to playground (#3425) 2024-09-20 11:03:49 +02:00
Marc KlingenandGitHub 955e25555f fix(ui): performance of optimistic updates of bookmarked traces (#3418) 2024-09-19 22:20:11 +00:00
Marc KlingenandGitHub 2a11f1e8fb fix(ui): truncate long model names in dashboard (#3417) 2024-09-19 21:40:54 +00:00
Marc KlingenandGitHub 143a461f0c chore(ui): add link to support and tracing faq to onboarding (#3416) 2024-09-19 19:58:53 +00:00
Max DeichmannandGitHub c8544690f7 chore: remove event io from dd traces (#3413) 2024-09-19 18:06:00 +00:00
Marc Klingen 05a533dd7e chore: release v2.80.0
CI/CD / lint (push) Waiting to run
CI/CD / test-docker-build (push) Waiting to run
CI/CD / tests-web (node20, pg12) (push) Waiting to run
CI/CD / tests-web (node20, pg15) (push) Waiting to run
CI/CD / tests-worker (node20, pg12) (push) Waiting to run
CI/CD / tests-worker (node20, pg15) (push) Waiting to run
CI/CD / e2e-tests (push) Waiting to run
CI/CD / e2e-server-tests (push) Waiting to run
CI/CD / all-ci-passed (push) Blocked by required conditions
CI/CD / push-docker-image (push) Blocked by required conditions
release.yml / release (push) Waiting to run
2024-09-19 16:37:23 +02:00
Steffen SchmitzandGitHub f3d15bb36e feat: optimize daily metrics query (#3371) 2024-09-19 14:10:10 +00:00
Hassieb PakzadandGitHub 24c321f632 fix(dataset-table): cost and latency queries (#3411) 2024-09-19 12:14:42 +00:00
Steffen SchmitzandGitHub 0e9cf548b2 chore: ecs production deploy automation (#3408) 2024-09-19 12:42:21 +02:00
Marc KlingenandGitHub 7158a41fe9 feat(ee): add additional logo (#3404) 2024-09-19 11:45:53 +02:00
Max DeichmannandGitHub bc384a525a chore: adjust sentry sample rate (#3403)
adjust autoscaling eval periods
2024-09-18 22:40:22 +00:00
Max DeichmannandGitHub 3f70828df6 chore: fix dev server speed (#3388) 2024-09-18 17:10:50 +00:00
marliessophieandGitHub 0988635427 fix: prompt tag menu disappears when out of focus (#3394) 2024-09-18 18:39:55 +02:00
Max DeichmannandGitHub 41ed6fed5e fix: allow to eval with azure models (#3387) 2024-09-17 16:54:22 +00:00
Hassieb PakzadandGitHub eb40659e44 fix(playground): save to prompt list (#3384) 2024-09-17 13:54:39 +00:00
Steffen SchmitzandGitHub a22bcb1188 build: add deploy config for eu (#3366) 2024-09-17 10:04:46 +00:00
Marc KlingenandGitHub 392fbe6c8c chore(ui): improve posthog api key error message (#3373)
* chore(ui): improve posthog api key error message

* Empty commit
2024-09-16 21:58:06 +00:00
marliessophieandGitHub 48d5a8f5a1 fix: cutoff filter for sessions export (#3370) 2024-09-16 15:11:32 +00:00
marliessophieandGitHub 44428870f7 style: adjust success toast for dark mode (#3362) 2024-09-16 07:46:10 +00:00
Hassieb PakzadandGitHub 724a4f5d9a fix(ui/prompts): variable parsing for chat prompts (#3360) 2024-09-15 13:55:01 +00:00
Marc KlingenandGitHub 2358264b66 fix(auth): unknown argument refresh_token_expires_in with Github connection (#3356)
fix(auth): unknown argument `refresh_token_expires_in` with Github connection

Fixes langfuse/langfuse#3355
2024-09-13 19:10:44 +00:00
Marc Klingen 65e3b4c5ca chore: release v2.79.0
CI/CD / lint (push) Waiting to run
CI/CD / test-docker-build (push) Waiting to run
CI/CD / tests-web (node20, pg12) (push) Waiting to run
CI/CD / tests-web (node20, pg15) (push) Waiting to run
CI/CD / tests-worker (node20, pg12) (push) Waiting to run
CI/CD / tests-worker (node20, pg15) (push) Waiting to run
CI/CD / e2e-tests (push) Waiting to run
CI/CD / e2e-server-tests (push) Waiting to run
CI/CD / all-ci-passed (push) Blocked by required conditions
CI/CD / push-docker-image (push) Blocked by required conditions
release.yml / release (push) Waiting to run
2024-09-13 20:37:58 +02:00
62c413faf5 fix(ui): ensure table footer is fixed on the bottom of the page (#3354)
* style(tables): ensure footer is fixed on the bottom

* fix

---------

Co-authored-by: Marc Klingen <git@marcklingen.com>
2024-09-13 20:13:36 +02:00
Marc KlingenandGitHub 8ea14ac537 feat(cloud): alert when free plan limit is approaching or reached (#3350)
* feat(cloud): alert when free plan limit is approaching or reached

* check entitlement
2024-09-13 20:08:32 +02:00
marliessophieandGitHub c32b84663c feat(ui): export traces (#3332) 2024-09-13 17:51:22 +00:00
Marc KlingenandGitHub 23ab79ba42 fix: custom base path nullable (#3353) 2024-09-13 17:17:13 +00:00
Hassieb PakzadandGitHub 6ffec3337f feat(playground): add support for OpenAI o1 beta models (#3349) 2024-09-13 18:47:57 +02:00
steffen911 34a2a9cddf chore: remove pipeline concurrency limiter 2024-09-13 16:24:24 +02:00
Steffen SchmitzandGitHub 0854429850 feat: add env variables to toggle queue consumers (#3339) 2024-09-13 14:16:48 +00:00
Steffen SchmitzandGitHub 05fd690486 chore: deploy to staging ECS environment (#3327) 2024-09-13 13:49:27 +00:00
marliessophieandGitHub bc3f39272b style(evals): apply sticky headers to eval logs and eval config tables (#3347) 2024-09-13 13:27:22 +00:00
Marc KlingenandGitHub 96ace71f9a feat(self-host): support for custom base path via NEXT_PUBLIC_BASE_PATH (#3346) 2024-09-13 13:02:36 +00:00
marliessophieandGitHub 70415dcab5 fix: Time to first token filter in Generations table (#3345) 2024-09-13 11:53:33 +00:00
Hassieb PakzadandGitHub 6a18f249aa feat(cost-tracking): add OpenAI o1 models (#3343) 2024-09-13 12:00:10 +02:00
Marc KlingenandGitHub cc60d506a7 feat(cloud): improve upgrade ui (#3331)
* add link to billing settings to project settings

* add plan label to org switcher

* add upgrade menu item on free cloud plans
2024-09-12 16:49:49 +00:00
Steffen SchmitzandGitHub a31a7f238e chore: remove db query from healthcheck (#3330) 2024-09-12 17:24:40 +02:00
Marc KlingenandGitHub 41ebc77f86 feat(cloud): improve crisp integration (#3329)
* chore: update crisp sdk

* feat(cloud): improve crisp integration
2024-09-12 14:33:51 +00:00
marliessophieandGitHub cdc41cf573 chore(ui_markdown): whitelist images with valid ipv6 or ipv4 hostname (#3318) 2024-09-12 08:04:44 +00:00
Max DeichmannandGitHub c86e32f756 chore: improve client side sentry (#3312) 2024-09-12 09:19:07 +02:00
Max DeichmannandGitHub 85576c162a chore: downgrade codegen (#3319) 2024-09-11 21:58:05 +02:00
Max DeichmannandGitHub 93ad1e6880 perf: get observation I/O only if required (#3316) 2024-09-11 18:47:31 +00:00
marliessophieandGitHub 97b17fc8bb style(ui): fix hover effect on groups in column reordering (#3317) 2024-09-11 18:22:23 +00:00
marliessophieandGitHub 04e2d9249f fix(ui): populate default prompt data for chat prompts with custom role (#3295) 2024-09-11 12:36:59 +00:00
marliessophieandGitHub cb94a54f0c feat(ui): type to find column name in filter builder (#3290) 2024-09-11 08:31:18 +00:00
marliessophieandGitHub 7c90bbed0c fix(ui): persist dateRange filter state of dashboard (#3298) 2024-09-11 08:11:21 +00:00
Max DeichmannandGitHub f133526ba6 chore: create dependabot group for express (#3303)
fix
2024-09-11 09:24:45 +02:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
dc014de54c chore(deps-dev): bump kysely-codegen from 0.11.0 to 0.16.6 (#3301)
Bumps [kysely-codegen](https://github.com/RobinBlomberg/kysely-codegen) from 0.11.0 to 0.16.6.
- [Release notes](https://github.com/RobinBlomberg/kysely-codegen/releases)
- [Commits](https://github.com/RobinBlomberg/kysely-codegen/commits)

---
updated-dependencies:
- dependency-name: kysely-codegen
  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>
2024-09-11 09:12:04 +02:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
c6beec1ad5 chore(deps): bump @aws-sdk/lib-storage from 3.614.0 to 3.645.0 (#3235)
Bumps [@aws-sdk/lib-storage](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/lib/lib-storage) from 3.614.0 to 3.645.0.
- [Release notes](https://github.com/aws/aws-sdk-js-v3/releases)
- [Changelog](https://github.com/aws/aws-sdk-js-v3/blob/main/lib/lib-storage/CHANGELOG.md)
- [Commits](https://github.com/aws/aws-sdk-js-v3/commits/v3.645.0/lib/lib-storage)

---
updated-dependencies:
- dependency-name: "@aws-sdk/lib-storage"
  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>
2024-09-10 23:17:43 +02:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
a810f7750f chore(deps): bump posthog-js from 1.154.6 to 1.161.3 (#3297)
Bumps [posthog-js](https://github.com/PostHog/posthog-js) from 1.154.6 to 1.161.3.
- [Release notes](https://github.com/PostHog/posthog-js/releases)
- [Changelog](https://github.com/PostHog/posthog-js/blob/main/CHANGELOG.md)
- [Commits](https://github.com/PostHog/posthog-js/compare/v1.154.6...v1.161.3)

---
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>
2024-09-10 20:02:47 +02:00
marliessophieandGitHub 2b34c82e98 feat(ui): render 0 latency as such in trace timeline view (#3288) 2024-09-10 13:10:24 +00:00
marliessophieandGitHub fe283284ef feat(ui): move columns via drag and drop (#3266) 2024-09-10 12:40:59 +00:00
Steffen SchmitzandGitHub 307f3d18ce chore: add trace info to web logs and context to queue events (#3282) 2024-09-10 12:26:07 +00:00
Hassieb PakzadandGitHub 46fff7148e fix(ui/prompts): parse prompt variables correctly (#3286) 2024-09-10 12:05:52 +02:00
Steffen SchmitzandGitHub 9cce111be6 chore: add worker entrypoint to handle alternative database config (#3283) 2024-09-10 10:58:50 +02:00
Max DeichmannandGitHub dbd7f810c1 chore: remove sentry tunnel (#3274) 2024-09-09 17:22:53 +02:00
Max DeichmannandGitHub abc3ea2b5a chore: remove explicit profiling settings worker (#3272)
fix
2024-09-09 14:44:51 +00:00
Steffen SchmitzandGitHub 3d8a4f9d75 chore: downgrade winston instrumentation and fix worker import (#3268) 2024-09-09 13:35:05 +02:00
Max DeichmannandGitHub 7b4bef7c2e fix: revert async zod (#3264) 2024-09-07 09:24:59 +02:00
Marc KlingenandGitHub b10353115b fix(ui): x axis of timeseries charts on dashboard for some time ranges (#3261) 2024-09-06 20:30:58 +00:00
Marc KlingenandGitHub f3235ceccb fix(ui): overflow of long strings in sessions view (#3258) 2024-09-06 17:20:11 +00:00
Marc KlingenandGitHub 92455b4e5a fix(ui): reset feedback form after submit (#3257)
fix(ui): reset feedback form after successfully sending feedback
2024-09-06 16:49:49 +00:00
Max DeichmannandGitHub 217b92d548 perf: parse zod in ingestion pipeline async (#3253) 2024-09-06 16:10:23 +02:00
Marc KlingenandGitHub c6ddd3822f feat(users): add search to users table (#3251) 2024-09-06 01:14:31 +00:00
Marc KlingenandGitHub 67d6c32ec5 chore: Update README.md 2024-09-06 01:50:29 +02:00
Marc KlingenandGitHub d89379e693 chore: Update README.md 2024-09-06 01:48:47 +02:00
Marc KlingenandGitHub 4b8d3a764e chore: Update README.md 2024-09-06 01:26:09 +02:00
Marc Klingen 5757fa6e38 chore: update readme 2024-09-06 00:58:47 +02:00
Marc KlingenandGitHub 34c3e5af08 chore: Update README.md 2024-09-06 00:56:16 +02:00
Steffen SchmitzandGitHub c68ec2fca0 chore: move additional logs to debug level (#3250) 2024-09-05 18:44:18 +00:00
Max DeichmannandGitHub 03dda153e8 chore: remove log line (#3249) 2024-09-05 16:28:33 +00:00
Marc Klingen 63641521d8 chore: add langfuse.com and langfuse.dev to csp headers 2024-09-05 16:13:37 +02:00
Marc Klingen 29d11642c4 chore: update csp headers to include ph.langfuse.dev 2024-09-05 15:52:29 +02:00
Steffen SchmitzandGitHub fb98a404d2 chore: adjust log level for queue infos (#3248) 2024-09-05 14:14:56 +02:00
Max DeichmannandGitHub 032fce56c4 chore: upgrade fern (#3247) 2024-09-05 11:30:10 +00:00
Max DeichmannandGitHub 518b8df2da fix: also track langfuse SDK req headers with underscores on span (#2972) 2024-09-05 11:00:13 +00:00
Max DeichmannandGitHub 70761f7ab0 chore: improve log level (#3244) 2024-09-05 09:01:22 +00:00
Max DeichmannandGitHub a85d4ccf07 chore: improve zod monitoring in ingestion pipeline (#3239) 2024-09-05 08:47:17 +00:00
Steffen SchmitzandGitHub 1b42eae7dd chore: move createAuthedApiRoute logging into metadata (#3238) 2024-09-05 09:09:07 +02:00
Marc Klingen 898cca56f9 chore: release v2.78.0
CI/CD / lint (push) Waiting to run
CI/CD / test-docker-build (push) Waiting to run
CI/CD / tests-web (node20, pg12) (push) Waiting to run
CI/CD / tests-web (node20, pg15) (push) Waiting to run
CI/CD / tests-worker (node20, pg12) (push) Waiting to run
CI/CD / tests-worker (node20, pg15) (push) Waiting to run
CI/CD / e2e-tests (push) Waiting to run
CI/CD / e2e-server-tests (push) Waiting to run
CI/CD / all-ci-passed (push) Blocked by required conditions
CI/CD / push-docker-image (push) Blocked by required conditions
release.yml / release (push) Waiting to run
2024-09-04 17:29:47 +02:00
Steffen SchmitzandGitHub 83e052e769 feat: migrate web api to new logger (#3230) 2024-09-04 17:02:26 +02:00
Steffen SchmitzandGitHub b5f19771fa feat: migrate shared package to new logger (#3229) 2024-09-04 16:43:36 +02:00
Marc KlingenandGitHub 644390183b fix: rename LANGFUSE_PROVISION_ to LANGFUSE_INIT_ ahead of changelog release (#3228) 2024-09-04 15:06:47 +02:00
Steffen SchmitzandGitHub 4d8e9003fe feat: add structured logging option (#3221) 2024-09-04 14:53:42 +02:00
Marc KlingenandGitHub 651c46a270 docs: add provisioning envs to .env.prod.example (#3227) 2024-09-04 14:24:53 +02:00
Max DeichmannandGitHub 6b127bced0 chore: add instrumentation to ingestion zod parsing (#3224) 2024-09-04 10:08:05 +00:00
Steffen SchmitzandGitHub e3a837b561 feat: add new /ready endpoint to separate liveness from readiness (#3222) 2024-09-04 11:53:41 +02:00
steffen911 11c04f02c8 chore: release v2.77.0
CI/CD / lint (push) Waiting to run
CI/CD / test-docker-build (push) Waiting to run
CI/CD / tests-web (node20, pg12) (push) Waiting to run
CI/CD / tests-web (node20, pg15) (push) Waiting to run
CI/CD / tests-worker (node20, pg12) (push) Waiting to run
CI/CD / tests-worker (node20, pg15) (push) Waiting to run
CI/CD / e2e-tests (push) Waiting to run
CI/CD / e2e-server-tests (push) Waiting to run
CI/CD / all-ci-passed (push) Blocked by required conditions
CI/CD / push-docker-image (push) Blocked by required conditions
release.yml / release (push) Waiting to run
2024-09-04 10:50:09 +02:00
Steffen SchmitzandGitHub 536aa138bc fix: reconnect workers to redis on errors (#3211) 2024-09-04 10:22:59 +02:00
Marc KlingenandGitHub 61b596c46a fix(docker): prevent web build oom by allocating more memory to build step in Dockerfile (#3217) 2024-09-04 00:18:15 +02:00
Marc KlingenandGitHub ddf43c0836 feat(self-host): provision instance with default org/project/user/keys via LANGFUSE_PROVISION_* envs (#3216) 2024-09-03 23:50:20 +02:00
Marc KlingenandGitHub d5ab03892a chore(ee): add EE to VersionLabel (#3214) 2024-09-03 14:03:57 +00:00
918b6643d2 chore(release): nextjs client side refresh after redeployment (#3116)
Co-authored-by: Marc Klingen <git@marcklingen.com>
Co-authored-by: Max Deichmann <m.deichmann@tum.de>
2024-09-03 12:42:57 +02:00
marliessophieandGitHub 937b4c0f0f feat(ui): deletion of dataset runs (#3209) 2024-09-03 12:26:02 +02:00
Max Deichmann 9b7f046e71 chore: release v2.76.0
CI/CD / lint (push) Waiting to run
CI/CD / test-docker-build (push) Waiting to run
CI/CD / tests-web (node20, pg12) (push) Waiting to run
CI/CD / tests-web (node20, pg15) (push) Waiting to run
CI/CD / tests-worker (node20, pg12) (push) Waiting to run
CI/CD / tests-worker (node20, pg15) (push) Waiting to run
CI/CD / e2e-tests (push) Waiting to run
CI/CD / e2e-server-tests (push) Waiting to run
CI/CD / all-ci-passed (push) Blocked by required conditions
CI/CD / push-docker-image (push) Blocked by required conditions
release.yml / release (push) Waiting to run
2024-09-03 10:48:10 +02:00
Steffen SchmitzandGitHub fe63704508 feat: fallback to synchronous ingestion if queue unavailable (#3196) 2024-09-03 10:42:59 +02:00
marliessophieandGitHub c39279f677 style(prompts): actions on hover (#3135) 2024-09-03 08:12:39 +00:00
Marc KlingenandGitHub 4206f5025d perf(cloud): pull usage data shown in settings from stripe if available (#3206) 2024-09-02 23:51:13 +00:00
marliessophieandGitHub 0260a4e861 style(dataset_items): make table vs item detail resizeable (#3139) 2024-09-02 08:01:43 +00:00
Max DeichmannandGitHub 244adf0dc3 feat: disable legacy ingestion queue if required (#3183) 2024-09-01 14:24:25 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
dc6b98d009 chore(deps-dev): bump tsx from 4.18.0 to 4.19.0 (#3172)
Bumps [tsx](https://github.com/privatenumber/tsx) from 4.18.0 to 4.19.0.
- [Release notes](https://github.com/privatenumber/tsx/releases)
- [Changelog](https://github.com/privatenumber/tsx/blob/master/release.config.cjs)
- [Commits](https://github.com/privatenumber/tsx/compare/v4.18.0...v4.19.0)

---
updated-dependencies:
- dependency-name: tsx
  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>
2024-09-01 10:07:44 +02:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
b20fc5a20b chore(deps-dev): bump tailwindcss from 3.4.4 to 3.4.10 (#3173)
Bumps [tailwindcss](https://github.com/tailwindlabs/tailwindcss) from 3.4.4 to 3.4.10.
- [Release notes](https://github.com/tailwindlabs/tailwindcss/releases)
- [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/v3.4.10/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/tailwindcss/compare/v3.4.4...v3.4.10)

---
updated-dependencies:
- dependency-name: tailwindcss
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-31 22:36:42 +02:00
Max DeichmannandGitHub f59ad5fd60 chore: upload sentry sourcemaps (#3179) 2024-08-31 18:50:48 +02:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
06ef41e372 chore(deps): bump pino-pretty from 10.3.1 to 11.2.2 (#3170)
Bumps [pino-pretty](https://github.com/pinojs/pino-pretty) from 10.3.1 to 11.2.2.
- [Release notes](https://github.com/pinojs/pino-pretty/releases)
- [Commits](https://github.com/pinojs/pino-pretty/compare/v10.3.1...v11.2.2)

---
updated-dependencies:
- dependency-name: pino-pretty
  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>
2024-08-31 17:04:49 +02:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
e215a2db99 chore(deps-dev): bump @types/lodash from 4.17.5 to 4.17.7 in the lodash group (#3169)
chore(deps-dev): bump @types/lodash in the lodash group

Bumps the lodash group with 1 update: [@types/lodash](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/lodash).


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

---
updated-dependencies:
- dependency-name: "@types/lodash"
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: lodash
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-31 17:00:36 +02:00
Max DeichmannandGitHub 5e247e88c9 fix: do nothing if invalidating org or project without api keys (#3167)
* also run on main branch

* also run on main branch

* also run on main branch

* also run on main branch

* also run on main branch
2024-08-30 22:57:41 +02:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
8e1e27c868 chore(deps-dev): bump msw from 2.3.1 to 2.4.1 (#3150)
Bumps [msw](https://github.com/mswjs/msw) from 2.3.1 to 2.4.1.
- [Release notes](https://github.com/mswjs/msw/releases)
- [Changelog](https://github.com/mswjs/msw/blob/main/CHANGELOG.md)
- [Commits](https://github.com/mswjs/msw/compare/v2.3.1...v2.4.1)

---
updated-dependencies:
- dependency-name: msw
  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>
2024-08-30 21:52:41 +02:00
dependabot[bot]andGitHub 4648a5be8b chore(deps): bump lucide-react from 0.429.0 to 0.436.0 (#3154) 2024-08-30 21:02:28 +02:00
Marc Klingen 4677cb10a9 chore: release v2.75.2
CI/CD / lint (push) Waiting to run
CI/CD / test-docker-build (push) Waiting to run
CI/CD / tests-web (node20, pg12) (push) Waiting to run
CI/CD / tests-web (node20, pg15) (push) Waiting to run
CI/CD / tests-worker (node20, pg12) (push) Waiting to run
CI/CD / tests-worker (node20, pg15) (push) Waiting to run
CI/CD / e2e-tests (push) Waiting to run
CI/CD / e2e-server-tests (push) Waiting to run
CI/CD / all-ci-passed (push) Blocked by required conditions
CI/CD / push-docker-image (push) Blocked by required conditions
release.yml / release (push) Waiting to run
2024-08-30 20:38:04 +02:00
Marc KlingenandGitHub 7ed03a0946 fix(playground): allow for creation of non-openai llm api keys (#3162) 2024-08-30 20:36:55 +02:00
Marc Klingen bbe40a0ba8 chore: release v2.75.1
CI/CD / lint (push) Waiting to run
CI/CD / test-docker-build (push) Waiting to run
CI/CD / tests-web (node20, pg12) (push) Waiting to run
CI/CD / tests-web (node20, pg15) (push) Waiting to run
CI/CD / tests-worker (node20, pg12) (push) Waiting to run
CI/CD / tests-worker (node20, pg15) (push) Waiting to run
CI/CD / e2e-tests (push) Waiting to run
CI/CD / e2e-server-tests (push) Waiting to run
CI/CD / all-ci-passed (push) Blocked by required conditions
CI/CD / push-docker-image (push) Blocked by required conditions
release.yml / release (push) Waiting to run
2024-08-30 20:25:38 +02:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2cf9a75fa5 chore(deps): bump @uiw/codemirror-theme-github from 4.21.25 to 4.23.0 (#3151)
Bumps [@uiw/codemirror-theme-github](https://github.com/uiwjs/react-codemirror) from 4.21.25 to 4.23.0.
- [Release notes](https://github.com/uiwjs/react-codemirror/releases)
- [Commits](https://github.com/uiwjs/react-codemirror/compare/v4.21.25...v4.23.0)

---
updated-dependencies:
- dependency-name: "@uiw/codemirror-theme-github"
  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>
2024-08-30 17:59:28 +02:00
Max DeichmannandGitHub 38acccbfac fix: make redis upsert queue backwards compatible (#3165) 2024-08-30 14:31:23 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
8327863176 chore(deps): bump the radix-ui group with 18 updates (#3159)
Bumps the radix-ui group with 18 updates:

| Package | From | To |
| --- | --- | --- |
| [@radix-ui/react-accordion](https://github.com/radix-ui/primitives) | `1.1.2` | `1.2.0` |
| [@radix-ui/react-alert-dialog](https://github.com/radix-ui/primitives) | `1.0.5` | `1.1.1` |
| [@radix-ui/react-avatar](https://github.com/radix-ui/primitives) | `1.0.4` | `1.1.0` |
| [@radix-ui/react-checkbox](https://github.com/radix-ui/primitives) | `1.0.4` | `1.1.1` |
| [@radix-ui/react-dialog](https://github.com/radix-ui/primitives) | `1.0.5` | `1.1.1` |
| [@radix-ui/react-dropdown-menu](https://github.com/radix-ui/primitives) | `2.0.6` | `2.1.1` |
| [@radix-ui/react-hover-card](https://github.com/radix-ui/primitives) | `1.0.7` | `1.1.1` |
| [@radix-ui/react-label](https://github.com/radix-ui/primitives) | `2.0.2` | `2.1.0` |
| [@radix-ui/react-popover](https://github.com/radix-ui/primitives) | `1.0.7` | `1.1.1` |
| [@radix-ui/react-scroll-area](https://github.com/radix-ui/primitives) | `1.0.5` | `1.1.0` |
| [@radix-ui/react-select](https://github.com/radix-ui/primitives) | `2.0.0` | `2.1.1` |
| [@radix-ui/react-separator](https://github.com/radix-ui/primitives) | `1.0.3` | `1.1.0` |
| [@radix-ui/react-slider](https://github.com/radix-ui/primitives) | `1.1.2` | `1.2.0` |
| [@radix-ui/react-slot](https://github.com/radix-ui/primitives) | `1.0.2` | `1.1.0` |
| [@radix-ui/react-switch](https://github.com/radix-ui/primitives) | `1.0.3` | `1.1.0` |
| [@radix-ui/react-tabs](https://github.com/radix-ui/primitives) | `1.0.4` | `1.1.0` |
| [@radix-ui/react-toggle](https://github.com/radix-ui/primitives) | `1.0.3` | `1.1.0` |
| [@radix-ui/react-tooltip](https://github.com/radix-ui/primitives) | `1.0.7` | `1.1.2` |


Updates `@radix-ui/react-accordion` from 1.1.2 to 1.2.0
- [Changelog](https://github.com/radix-ui/primitives/blob/main/release-process.md)
- [Commits](https://github.com/radix-ui/primitives/commits)

Updates `@radix-ui/react-alert-dialog` from 1.0.5 to 1.1.1
- [Changelog](https://github.com/radix-ui/primitives/blob/main/release-process.md)
- [Commits](https://github.com/radix-ui/primitives/commits)

Updates `@radix-ui/react-avatar` from 1.0.4 to 1.1.0
- [Changelog](https://github.com/radix-ui/primitives/blob/main/release-process.md)
- [Commits](https://github.com/radix-ui/primitives/commits)

Updates `@radix-ui/react-checkbox` from 1.0.4 to 1.1.1
- [Changelog](https://github.com/radix-ui/primitives/blob/main/release-process.md)
- [Commits](https://github.com/radix-ui/primitives/commits)

Updates `@radix-ui/react-dialog` from 1.0.5 to 1.1.1
- [Changelog](https://github.com/radix-ui/primitives/blob/main/release-process.md)
- [Commits](https://github.com/radix-ui/primitives/commits)

Updates `@radix-ui/react-dropdown-menu` from 2.0.6 to 2.1.1
- [Changelog](https://github.com/radix-ui/primitives/blob/main/release-process.md)
- [Commits](https://github.com/radix-ui/primitives/commits)

Updates `@radix-ui/react-hover-card` from 1.0.7 to 1.1.1
- [Changelog](https://github.com/radix-ui/primitives/blob/main/release-process.md)
- [Commits](https://github.com/radix-ui/primitives/commits)

Updates `@radix-ui/react-label` from 2.0.2 to 2.1.0
- [Changelog](https://github.com/radix-ui/primitives/blob/main/release-process.md)
- [Commits](https://github.com/radix-ui/primitives/commits)

Updates `@radix-ui/react-popover` from 1.0.7 to 1.1.1
- [Changelog](https://github.com/radix-ui/primitives/blob/main/release-process.md)
- [Commits](https://github.com/radix-ui/primitives/commits)

Updates `@radix-ui/react-scroll-area` from 1.0.5 to 1.1.0
- [Changelog](https://github.com/radix-ui/primitives/blob/main/release-process.md)
- [Commits](https://github.com/radix-ui/primitives/commits)

Updates `@radix-ui/react-select` from 2.0.0 to 2.1.1
- [Changelog](https://github.com/radix-ui/primitives/blob/main/release-process.md)
- [Commits](https://github.com/radix-ui/primitives/commits)

Updates `@radix-ui/react-separator` from 1.0.3 to 1.1.0
- [Changelog](https://github.com/radix-ui/primitives/blob/main/release-process.md)
- [Commits](https://github.com/radix-ui/primitives/commits)

Updates `@radix-ui/react-slider` from 1.1.2 to 1.2.0
- [Changelog](https://github.com/radix-ui/primitives/blob/main/release-process.md)
- [Commits](https://github.com/radix-ui/primitives/commits)

Updates `@radix-ui/react-slot` from 1.0.2 to 1.1.0
- [Changelog](https://github.com/radix-ui/primitives/blob/main/release-process.md)
- [Commits](https://github.com/radix-ui/primitives/commits)

Updates `@radix-ui/react-switch` from 1.0.3 to 1.1.0
- [Changelog](https://github.com/radix-ui/primitives/blob/main/release-process.md)
- [Commits](https://github.com/radix-ui/primitives/commits)

Updates `@radix-ui/react-tabs` from 1.0.4 to 1.1.0
- [Changelog](https://github.com/radix-ui/primitives/blob/main/release-process.md)
- [Commits](https://github.com/radix-ui/primitives/commits)

Updates `@radix-ui/react-toggle` from 1.0.3 to 1.1.0
- [Changelog](https://github.com/radix-ui/primitives/blob/main/release-process.md)
- [Commits](https://github.com/radix-ui/primitives/commits)

Updates `@radix-ui/react-tooltip` from 1.0.7 to 1.1.2
- [Changelog](https://github.com/radix-ui/primitives/blob/main/release-process.md)
- [Commits](https://github.com/radix-ui/primitives/commits)

---
updated-dependencies:
- dependency-name: "@radix-ui/react-accordion"
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: radix-ui
- dependency-name: "@radix-ui/react-alert-dialog"
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: radix-ui
- dependency-name: "@radix-ui/react-avatar"
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: radix-ui
- dependency-name: "@radix-ui/react-checkbox"
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: radix-ui
- dependency-name: "@radix-ui/react-dialog"
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: radix-ui
- dependency-name: "@radix-ui/react-dropdown-menu"
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: radix-ui
- dependency-name: "@radix-ui/react-hover-card"
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: radix-ui
- dependency-name: "@radix-ui/react-label"
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: radix-ui
- dependency-name: "@radix-ui/react-popover"
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: radix-ui
- dependency-name: "@radix-ui/react-scroll-area"
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: radix-ui
- dependency-name: "@radix-ui/react-select"
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: radix-ui
- dependency-name: "@radix-ui/react-separator"
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: radix-ui
- dependency-name: "@radix-ui/react-slider"
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: radix-ui
- dependency-name: "@radix-ui/react-slot"
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: radix-ui
- dependency-name: "@radix-ui/react-switch"
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: radix-ui
- dependency-name: "@radix-ui/react-tabs"
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: radix-ui
- dependency-name: "@radix-ui/react-toggle"
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: radix-ui
- dependency-name: "@radix-ui/react-tooltip"
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: radix-ui
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-30 16:04:22 +02:00
Max DeichmannandGitHub 6045324d2b fix: fix rate limits for orgs with stripe details (#3164) 2024-08-30 13:53:47 +00:00
Marc KlingenandGitHub 87923e5d6c fix(ui): add default llm base urls for reference in llm api key form (#3163)
* ui nit

* fix(ui): add default llm base urls for reference in llm api key form
2024-08-30 13:28:07 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
f584de7577 chore(deps): bump zod-to-json-schema from 3.22.5 to 3.23.2 (#3161)
Bumps [zod-to-json-schema](https://github.com/StefanTerdell/zod-to-json-schema) from 3.22.5 to 3.23.2.
- [Release notes](https://github.com/StefanTerdell/zod-to-json-schema/releases)
- [Changelog](https://github.com/StefanTerdell/zod-to-json-schema/blob/master/changelog.md)
- [Commits](https://github.com/StefanTerdell/zod-to-json-schema/commits)

---
updated-dependencies:
- dependency-name: zod-to-json-schema
  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>
2024-08-30 13:55:34 +02:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
d01c403f62 chore(deps): bump kysely from 0.27.3 to 0.27.4 (#3160)
Bumps [kysely](https://github.com/kysely-org/kysely) from 0.27.3 to 0.27.4.
- [Release notes](https://github.com/kysely-org/kysely/releases)
- [Commits](https://github.com/kysely-org/kysely/compare/0.27.3...0.27.4)

---
updated-dependencies:
- dependency-name: kysely
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-30 13:38:28 +02:00
Max DeichmannandGitHub a001e6072c chore: run snyk on pr pushes only (#3158) 2024-08-30 12:17:12 +02:00
Max DeichmannandGitHub d9a3f977bc chore: radix dependabot group (#3157) 2024-08-30 11:57:58 +02:00
Max DeichmannandGitHub 8fae4f3051 feat: add custom rate limits for organisations (#2976) 2024-08-30 11:57:31 +02:00
Max DeichmannandGitHub c36b1ff51d chore: dependabot gorup for lodash (#3155)
smaller fixes
2024-08-30 11:17:51 +02:00
Max DeichmannandGitHub d3cf604f4f chore: increase dependabot limit (#3148) 2024-08-30 10:51:41 +02:00
dependabot[bot]andGitHub cc886917b6 chore(deps): bump @radix-ui/react-toggle-group from 1.0.4 to 1.1.0 (#3146) 2024-08-30 08:51:05 +02:00
Marc KlingenandGitHub b924bf9e09 feat(auth): improve password reset error messages (#3143) 2024-08-29 22:02:57 +00:00
Marc Klingen 895825dd1c chore: release v2.75.0
CI/CD / lint (push) Waiting to run
CI/CD / test-docker-build (push) Waiting to run
CI/CD / tests-web (node20, pg12) (push) Waiting to run
CI/CD / tests-web (node20, pg15) (push) Waiting to run
CI/CD / tests-worker (node20, pg12) (push) Waiting to run
CI/CD / tests-worker (node20, pg15) (push) Waiting to run
CI/CD / e2e-tests (push) Waiting to run
CI/CD / e2e-server-tests (push) Waiting to run
CI/CD / all-ci-passed (push) Blocked by required conditions
CI/CD / push-docker-image (push) Blocked by required conditions
release.yml / release (push) Waiting to run
2024-08-29 18:52:21 +02:00
Max DeichmannandGitHub 6ceccb649f chore: fix worker tests (#3133) 2024-08-29 16:29:56 +02:00
Max DeichmannandGitHub 911021ac5d chore: ensure clickhouse is healthy before migrating (#3128) 2024-08-29 13:58:02 +00:00
marliessophieandGitHub f39778ef9f chore(ui): render cloudflare errors in more user readable format (#3118) 2024-08-29 10:26:55 +00:00
Marc KlingenandGitHub 4d00e39c6b fix(ui): reset to first table page when number of total pages is reduced (#3122) 2024-08-28 21:58:14 +00:00
Marc Klingen 7af49fd649 chore(ui): adjust padding in settings 2024-08-28 23:30:16 +02:00
Max DeichmannandGitHub 662f1917b6 fix: create fast hashed secret key upon key creation (#3117) 2024-08-28 20:14:12 +02:00
Marc KlingenandGitHub d8f047f2a1 chore(ui): improve trpc error types (#3097) 2024-08-28 19:03:27 +02:00
69815a51b9 feat: add trace tags to generations, sessions and score table (#3109)
* feat: Add query to retrieve tags for a specific project

* chore(api): Add "tags" column to scores table definition

* add nullable to score table config

* chore(api): Parallelize filter options queries

* chore(api): Update datetime filter function name to use raw SQL

* chore(api): Update datetime filter function name to use raw SQL

* chore: Update datetime filter function name to use raw SQL

* chore: Update scores router to include timestamp filter in query

* chore: Update datetime filter function name to use raw SQL

* chore: Update datetime filter function name to use raw SQL

* chore: Update datetime filter function name to use raw SQL

* refactor: Use datetimeFilterToPrisma function for start time filter

* refactor: Use datetimeFilterToPrisma function for start time filter

* refactor: Use datetimeFilterToPrisma function for timestamp filter

* refactor: Update datetime filter function name to use raw SQL

* refactor: Add viewOnly prop to TagList component

* refactor: Add viewOnly prop to TagButton component

* feat: Add trace tags to GenerationsTable

* refactor: Add traceTags column to ScoresTable

* feat: Add traceTags column to SessionsTable

* refactor: Add traceTags column to ScoresTable

* feat: Add traceTags to session metrics

* feat: Add traceTags to getAllGenerationsSqlQuery

* revert rename

* push

* add row height to sessions

* wrap if non-s row height

---------

Co-authored-by: Marc Klingen <git@marcklingen.com>
2024-08-28 19:01:49 +02:00
Marc KlingenandGitHub 2bb419213a feat(ui): reduce font size and white-space to shrink UI to fit more content (#3001) 2024-08-28 19:00:55 +02:00
Richard KrümmelandGitHub a330035c40 feat: add tags filter to scores table (#3071) 2024-08-28 17:11:25 +02:00
Max DeichmannandGitHub dd4434fe25 chore: set keepalive on web docker container (#3113) 2024-08-28 15:00:34 +00:00
Max DeichmannandGitHub f898f57bff chore: upgrade dd-trace (#3114)
fix
2024-08-28 14:15:32 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
eda52c7b77 chore(deps-dev): bump prettier-plugin-tailwindcss from 0.5.14 to 0.6.6 (#3103)
Bumps [prettier-plugin-tailwindcss](https://github.com/tailwindlabs/prettier-plugin-tailwindcss) from 0.5.14 to 0.6.6.
- [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.14...v0.6.6)

---
updated-dependencies:
- dependency-name: prettier-plugin-tailwindcss
  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>
2024-08-28 16:01:30 +02:00
marliessophieandGitHub e5c7686bc7 fix(comments): reference object check for comment creation (#3108) 2024-08-28 08:57:48 +00:00
marliessophieandGitHub 251be1a240 fix(comments): reintroduce old procedure name for backwards compatibility (#3107) 2024-08-28 08:15:42 +00:00
marliessophieandGitHub 66c810e4a7 chore(comments): fetch comment counts by type in trace tree (#3098) 2024-08-27 16:30:02 +00:00
marliessophieandGitHub c3c8a741e4 chore(ui): style TRPC error toast to differentiate between errors and warnings (#3095) 2024-08-27 17:28:32 +02:00
Marc KlingenandGitHub 81c898b4ed feat(ui): jump to page x in table via input in pagination (#3090) 2024-08-27 17:04:46 +02:00
834d0bcc7a feat: add tag filter to generations table (#3022)
* feat(api): add tag filtering to generation filter options query

* chore: add Trace Tags column to observations table

* feat(api): optimize filterOptionsQuery by removing count property

* chore(api): optimize filterOptionsQuery for better performance

* feat: add tags filter to sessions table (#3067)



---------

Co-authored-by: Marc Klingen <git@marcklingen.com>

* refactor: use DISTINCT keyword in filterOptionsQuery

---------

Co-authored-by: Marc Klingen <git@marcklingen.com>
2024-08-27 15:19:10 +02:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
40953d7732 chore(deps): bump pino-http from 9.0.0 to 10.2.0 (#3091)
Bumps [pino-http](https://github.com/pinojs/pino-http) from 9.0.0 to 10.2.0.
- [Release notes](https://github.com/pinojs/pino-http/releases)
- [Commits](https://github.com/pinojs/pino-http/compare/v9.0.0...v10.2.0)

---
updated-dependencies:
- dependency-name: pino-http
  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>
2024-08-27 15:05:05 +02:00
Max DeichmannandGitHub d471be8783 fix: fix async instrumentation (#3094) 2024-08-27 12:03:47 +00:00
marliessophieandGitHub 0ca38d8070 chore(evals): display eval config sampling rate in UI (#3093) 2024-08-27 11:13:19 +00:00
Max DeichmannandGitHub 12458e3d71 chore: add instrumentation to auth apis (#3069) 2024-08-27 09:44:32 +00:00
Max DeichmannandGitHub 51575b5491 chore: fix dependabot observability group (#3086)
fix
2024-08-27 10:47:30 +02:00
Max DeichmannandGitHub a9c11f3323 chore: create dependabot observability group (#3085) 2024-08-27 10:42:42 +02:00
Max DeichmannandGitHub 066140c903 fix: fix grouping of 3 months dashboard (#3080) 2024-08-27 08:40:32 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
0b7de157ad chore(deps): bump tailwind-merge from 2.2.2 to 2.5.2 (#3060)
Bumps [tailwind-merge](https://github.com/dcastil/tailwind-merge) from 2.2.2 to 2.5.2.
- [Release notes](https://github.com/dcastil/tailwind-merge/releases)
- [Commits](https://github.com/dcastil/tailwind-merge/compare/v2.2.2...v2.5.2)

---
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>
2024-08-27 10:20:55 +02:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
7dcc718c77 chore(deps-dev): bump tsx from 4.7.2 to 4.18.0 (#3066)
Bumps [tsx](https://github.com/privatenumber/tsx) from 4.7.2 to 4.18.0.
- [Release notes](https://github.com/privatenumber/tsx/releases)
- [Changelog](https://github.com/privatenumber/tsx/blob/master/release.config.cjs)
- [Commits](https://github.com/privatenumber/tsx/compare/v4.7.2...v4.18.0)

---
updated-dependencies:
- dependency-name: tsx
  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>
2024-08-26 22:08:43 +02:00
marliessophieandGitHub e098618614 chore(filters): improve filter table type safety (#3078) 2024-08-26 14:45:28 +00:00
marliessophieandGitHub 3e8ba54aa0 chore(tables): persist date range in session storage (#3076) 2024-08-26 15:44:05 +02:00
Marc KlingenandGitHub ea36cd58a4 chore(ui): disable jump to last page while count pages is loading (#3077) 2024-08-26 14:38:05 +02:00
Marc KlingenandGitHub c7b726a73a perf(ui): split totalCount from scores.all to reduce loading time of scores table (#3075) 2024-08-26 14:11:10 +02:00
Marc KlingenandGitHub 112e7a2dcf perf(ui): split totalCount from sessions.all to reduce loading time of sessions table (#3073) 2024-08-26 14:03:14 +02:00
Marc Klingen c1d98aba6a fix(ui): remove artificial wait time used in testing 2024-08-26 13:41:31 +02:00
Marc KlingenandGitHub 75fd1cbd3f perf(ui): split totalCount from generations.all to reduce loading time of generations table (#3072) 2024-08-26 13:40:29 +02:00
Marc KlingenandGitHub 75ec734a45 perf(ui): split total count query on trace table to reduce loading time (#3070) 2024-08-26 13:24:54 +02:00
marliessophieandGitHub 98e951423c chore(filters): persist table and dashboard filter state in session storage (#3036) 2024-08-26 11:05:58 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
8e1dc3ee99 chore(deps): bump core-js from 3.36.1 to 3.38.1 (#3058)
Bumps [core-js](https://github.com/zloirock/core-js/tree/HEAD/packages/core-js) from 3.36.1 to 3.38.1.
- [Release notes](https://github.com/zloirock/core-js/releases)
- [Changelog](https://github.com/zloirock/core-js/blob/master/CHANGELOG.md)
- [Commits](https://github.com/zloirock/core-js/commits/v3.38.1/packages/core-js)

---
updated-dependencies:
- dependency-name: core-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>
2024-08-26 12:35:47 +02:00
Marc KlingenandGitHub 7668995ec9 perf(ui): increase speed of tables, no nulls first/last modifier when sorting by non-nullable attributes (#3068) 2024-08-26 11:30:31 +02:00
marliessophieandGitHub 06233f878c chore(comments): disable comments count on public traces (#3065) 2024-08-26 09:13:58 +00:00
Max DeichmannandGitHub dae3d4e4e2 fix: remove dd profiling presets (#3063)
fix
2024-08-24 17:36:50 +02:00
Max DeichmannandGitHub 04dca2ad6b perf: use dedicated redis clients for bullmq (#3052) 2024-08-23 16:54:39 +00:00
Marc KlingenandGitHub 48c61ad77f perf(ui): load metrics in trace table independently to reduce initial table load time (#3051) 2024-08-23 17:39:32 +02:00
Marc KlingenandGitHub 34140302a6 perf(ui): load metrics in session table independently to reduce initial table load time (#3048) 2024-08-23 13:28:32 +02:00
Marc KlingenandGitHub 93cc36ecdc fix(cloud): catch crisp chat exception (#3042) 2024-08-23 13:22:36 +02:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
ace30782a0 chore(deps): bump @radix-ui/react-collapsible from 1.0.3 to 1.1.0 (#3031)
Bumps [@radix-ui/react-collapsible](https://github.com/radix-ui/primitives) from 1.0.3 to 1.1.0.
- [Changelog](https://github.com/radix-ui/primitives/blob/main/release-process.md)
- [Commits](https://github.com/radix-ui/primitives/commits)

---
updated-dependencies:
- dependency-name: "@radix-ui/react-collapsible"
  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>
2024-08-23 13:04:49 +02:00
Marc KlingenandGitHub 3c94a0cda9 perf(ui): parallelize db query for sessions.totalCount (#3044) 2024-08-23 09:58:58 +00:00
Max DeichmannandGitHub b0fea44556 feat: sentry capture console.error in frontend (#3043)
fix
2024-08-23 08:30:42 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
ed91330c37 chore(deps): bump lucide-react from 0.364.0 to 0.429.0 (#3037)
Bumps [lucide-react](https://github.com/lucide-icons/lucide/tree/HEAD/packages/lucide-react) from 0.364.0 to 0.429.0.
- [Release notes](https://github.com/lucide-icons/lucide/releases)
- [Commits](https://github.com/lucide-icons/lucide/commits/0.429.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>
2024-08-23 10:16:58 +02:00
Max DeichmannandGitHub b8ed9d681b fix: free up tokenizer at worker shutdown (#3041) 2024-08-23 08:02:57 +00:00
Max DeichmannandGitHub ea2e1b4873 fix: security policy for posthog (#3040)
fix
2024-08-23 07:32:55 +00:00
Max DeichmannandGitHub 3ddef14e75 fix: add sentry frontend support (#3012) 2024-08-23 06:41:27 +00:00
Marc KlingenandGitHub 4658942701 perf(ui): parallelize db queries in traces.all (#3038) 2024-08-23 01:32:33 +02:00
Marc KlingenandGitHub 51be8f8fb3 perf(ui): parallelize users, sessions, models, score config, scores queries in trpc routes (#3039)
* prompts

* models

* score config

* scores

* sessions
2024-08-22 23:26:59 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
6ede3b7690 chore(deps): bump the next group with 2 updates (#3030)
Bumps the next group with 2 updates: [next](https://github.com/vercel/next.js) and [eslint-config-next](https://github.com/vercel/next.js/tree/HEAD/packages/eslint-config-next).


Updates `next` from 14.2.3 to 14.2.6
- [Release notes](https://github.com/vercel/next.js/releases)
- [Changelog](https://github.com/vercel/next.js/blob/canary/release.js)
- [Commits](https://github.com/vercel/next.js/compare/v14.2.3...v14.2.6)

Updates `eslint-config-next` from 14.2.3 to 14.2.6
- [Release notes](https://github.com/vercel/next.js/releases)
- [Changelog](https://github.com/vercel/next.js/blob/canary/release.js)
- [Commits](https://github.com/vercel/next.js/commits/v14.2.6/packages/eslint-config-next)

---
updated-dependencies:
- dependency-name: next
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: next
- dependency-name: eslint-config-next
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: next
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-22 22:51:08 +00:00
Max DeichmannandGitHub dee36bbe29 chore: remove instrumentation from prisma dependabot group (#3033) 2024-08-23 00:31:28 +02:00
Max DeichmannandGitHub f71cf2f894 fix: remove dependabot patch group (#3027) 2024-08-22 23:52:35 +02:00
Marc KlingenandGitHub 1a707fe147 fix(ui): layout entitlements (#3026)
* fix(ui): layout entitlements

* push
2024-08-22 21:51:09 +00:00
Marc KlingenandGitHub 8da1640777 perf(ui): parallelize db queries in users.all route (#3018) 2024-08-22 23:24:14 +02:00
Max DeichmannandGitHub 1e56499f00 chore: upgrade bull (#3025)
fix
2024-08-22 21:23:18 +00:00
Max DeichmannandGitHub 2524fb9648 fix: otel error formatting (#3024) 2024-08-22 19:29:31 +00:00
marliessophieandGitHub 91cd86a286 chore(ui): Add free text option to dynamically populated filter options (#3020) 2024-08-22 18:23:42 +02:00
Lucas MagnumandGitHub e6b9d23615 fix(auth): Cannot read properties of undefined, GoogleProfile.hd (#3019) 2024-08-22 17:36:20 +02:00
Marc KlingenandGitHub 8b76feeffa perf(ui): parallelize db queries in filterOptions routes (#3016) 2024-08-22 15:27:35 +00:00
88a53dc72d perf(tables): drop filter option counts to reduce latency (#3013)
* perf(filters): rm counts from prompt filter options

* perf: drop counts from generations filter options

* perf: drop counts from trace filter options

---------

Co-authored-by: Marc Klingen <git@marcklingen.com>
2024-08-22 17:06:15 +02:00
Marc KlingenandGitHub 649a9af91c perf(ui): simplify default traces.byId route to not load observations and scores for table use cases (#3010) 2024-08-22 16:19:02 +02:00
Marc KlingenandGitHub 876917d112 perf(ui): remove unused scores from observations.byId (#3011) 2024-08-22 16:17:51 +02:00
Max DeichmannandGitHub bb6bc79bdc fix: use serverComponentsExternalPackages for dd and otel (#3009) 2024-08-22 13:21:14 +00:00
Marc KlingenandGitHub 7aff2b2e61 perf(ui): parallelize db queries in byId trpc routes (#3006) 2024-08-22 14:45:36 +02:00
Max DeichmannandGitHub 8f742f660a fix: await closure of bull queues (#3007)
fix
2024-08-22 11:15:54 +00:00
Marc Klingen 8245cf8ae7 chore: release v2.74.0
CI/CD / lint (push) Waiting to run
CI/CD / test-docker-build (push) Waiting to run
CI/CD / tests-web (node20, pg12) (push) Waiting to run
CI/CD / tests-web (node20, pg15) (push) Waiting to run
CI/CD / tests-worker (node20, pg12) (push) Waiting to run
CI/CD / tests-worker (node20, pg15) (push) Waiting to run
CI/CD / e2e-tests (push) Waiting to run
CI/CD / e2e-server-tests (push) Waiting to run
CI/CD / all-ci-passed (push) Blocked by required conditions
CI/CD / push-docker-image (push) Blocked by required conditions
release.yml / release (push) Waiting to run
2024-08-22 12:38:18 +02:00
Marc Klingen 05b3021bff fix(ee): remove model-based-evaluations entitlement as it is not available in v2 2024-08-22 11:59:32 +02:00
Marc KlingenandGitHub 44bcebdaec feat(ui): add markdown support to comments (#3002)
* feat(ui): add markdown support to comments

* Apply suggestions from code review
2024-08-21 23:50:35 +00:00
Marc Klingen 2ac15e8714 chore: release v2.73.0
CI/CD / lint (push) Waiting to run
CI/CD / test-docker-build (push) Waiting to run
CI/CD / tests-web (node20, pg12) (push) Waiting to run
CI/CD / tests-web (node20, pg15) (push) Waiting to run
CI/CD / tests-worker (node20, pg12) (push) Waiting to run
CI/CD / tests-worker (node20, pg15) (push) Waiting to run
CI/CD / e2e-tests (push) Waiting to run
CI/CD / e2e-server-tests (push) Waiting to run
CI/CD / all-ci-passed (push) Blocked by required conditions
CI/CD / push-docker-image (push) Blocked by required conditions
release.yml / release (push) Waiting to run
2024-08-21 23:50:09 +02:00
marliessophieandGitHub 13b027cd3c fix(ui): handle JSON assistant output correctly in ChatML Schema (#3000) 2024-08-21 23:49:05 +02:00
Marc KlingenandGitHub 4a89cf4e45 perf(ui): disable trpc batching (#2999) 2024-08-21 23:24:56 +02:00
Marc Klingen e776af544f perf(cloud): report usage to stripe only for orgs with usage in timeframe 2024-08-21 19:28:25 +02:00
Marc KlingenandGitHub 0d781f12f3 chore(ui): add note that prompts from playground can only be saved to chat prompts (#2996) 2024-08-21 15:28:33 +00:00
marliessophieandGitHub 5d81505d84 feat(ui): support OpenAI's image content part format via markdown on trace/observation preview (#2984) 2024-08-21 17:18:04 +02:00
Marc KlingenandGitHub 8471b7f7b3 security: disable x-powered-by header (#2995) 2024-08-21 15:14:23 +02:00
Max DeichmannandGitHub 78a90afdab fix: fix api key link (#2994) 2024-08-21 11:39:45 +00:00
Max DeichmannandGitHub 9e304ad873 infra: improve eval logs (#2993) 2024-08-21 10:10:13 +00:00
Marc Klingen 5457053fdf chore(ui): change prompt label icon 2024-08-21 11:47:13 +02:00
Max DeichmannandGitHub e91716c6a2 infra: improve eval observability (#2992) 2024-08-21 09:45:38 +00:00
Max DeichmannandGitHub 5148cfa690 chore: remove log line (#2991) 2024-08-21 09:24:19 +00:00
Marlies Mayerhofer 86816e569d chore: release v2.72.0
CI/CD / lint (push) Waiting to run
CI/CD / test-docker-build (push) Waiting to run
CI/CD / tests-web (node20, pg12) (push) Waiting to run
CI/CD / tests-web (node20, pg15) (push) Waiting to run
CI/CD / tests-worker (node20, pg12) (push) Waiting to run
CI/CD / tests-worker (node20, pg15) (push) Waiting to run
CI/CD / e2e-tests (push) Waiting to run
CI/CD / e2e-server-tests (push) Waiting to run
CI/CD / all-ci-passed (push) Blocked by required conditions
CI/CD / push-docker-image (push) Blocked by required conditions
release.yml / release (push) Waiting to run
2024-08-21 09:54:34 +02:00
marliessophieandGitHub 0fe2176930 style(ui): align styling of comment button on sessions page and adjust dashboard and session overflow for mobile (#2985)
* style(ui): align styling of comment button on sessions page

* style(dashboard): score analytics charts full width for mobile

* style(markdown): fix overflow behavior on sessions page for mobile
2024-08-21 09:29:17 +02:00
Max DeichmannandGitHub 43f7c524dc fix: drop failed eval jobs (#2986) 2024-08-20 23:07:51 +00:00
Marc Klingen 2bcfab130d chore(cloud): update stripe product ids 2024-08-20 19:10:56 +02:00
Marc Klingen 51bb1c3374 chore(cloud): add more details to upgrade page 2024-08-20 16:58:10 +02:00
Marc KlingenandGitHub 302bb065b7 feat(cloud): add billing period and next invoiced amount to billing settings (#2982) 2024-08-20 16:35:13 +02:00
Max DeichmannandGitHub e6a8fb84f0 fix: remove last backend sentry logic (#2981) 2024-08-20 14:07:43 +00:00
Marc KlingenandGitHub b12f3df933 fix: admin role (#2980) 2024-08-20 15:40:32 +02:00
marliessophieandGitHub 159b5f87fe feat(comments): add comments on objects (#2971) 2024-08-20 15:39:23 +02:00
Marc Klingen 73fe2dc1a7 fix: use test mode stripe products also in staging 2024-08-19 22:52:03 +02:00
Marc KlingenandGitHub 9daf348d61 feat(cloud): add stripe checkout, customer portal, and usage metering (#2962) 2024-08-19 20:35:46 +00:00
Marc KlingenandGitHub 8a9d0b01af chore(cloud): reduce posthog event count (#2973)
optimize how often we use identify with posthog
2024-08-19 16:49:01 +02:00
Marc Klingen 315d8c7619 chore(ui): include NEXT_PUBLIC_DEMO_ORG_ID in demo banner check 2024-08-19 12:18:04 +02:00
Max DeichmannandGitHub b23eaf3cd4 infra: record exceptions of health endpoint (#2970) 2024-08-19 11:54:43 +02:00
Max DeichmannandGitHub eea1e8d054 fix: only openai adapter for evals (#2960) 2024-08-19 08:00:54 +00:00
Max DeichmannandGitHub 06a7a4bfb2 infra: add ingestion api attributes to span (#2968) 2024-08-19 07:46:43 +00:00
Max DeichmannandGitHub 7c8a30d48f infra: add ingestion pipeline processing time (#2965) 2024-08-17 21:13:41 +00:00
Max DeichmannandGitHub 1971adda18 feat: improve dd ressource names for http requests (#2964) 2024-08-17 21:57:33 +02:00
Max DeichmannandGitHub d9b4e3a7dd feat: add dd instrumentation to web container (#2727) 2024-08-17 20:21:37 +02:00
ClemoandGitHub f1aa884cbc docs: update readme 2024-08-17 14:33:46 +02:00
ClemoandGitHub 0ed9acddb6 docs: update careers 2024-08-17 14:28:58 +02:00
Marc KlingenandGitHub 61e3470a93 feat(datasets): add duplicate dataset button (#2957)
* feat(datasets): add duplicate dataset button

* named export
2024-08-17 01:12:32 +02:00
420 changed files with 18258 additions and 8865 deletions
-14
View File
@@ -1,14 +0,0 @@
{
"name": "langfuse dev",
"image": "mcr.microsoft.com/devcontainers/javascript-node:20-bookworm",
"features": {
"ghcr.io/devcontainers/features/docker-in-docker:2": {
"version": "latest",
"moby": true
}
},
"forwardPorts": [3000],
"onCreateCommand": "npm install -g pnpm@9.5.0",
"postCreateCommand": "cp .env.dev.example .env && pnpm run dx-f"
}
+3
View File
@@ -51,3 +51,6 @@ REDIS_AUTH="myredissecret"
LANGFUSE_WORKER_PASSWORD=mybasicauthsecret
# openssl rand -hex 32 used only here
ENCRYPTION_KEY=0000000000000000000000000000000000000000000000000000000000000000
# speeds up local development by not executing init scripts on server startup
NEXT_PUBLIC_LANGFUSE_RUN_NEXT_INIT="false"
+47 -4
View File
@@ -28,6 +28,9 @@ ENCRYPTION_KEY="0000000000000000000000000000000000000000000000000000000000000000
# Use CSP headers to enforce HTTPS, optional
# LANGFUSE_CSP_ENFORCE_HTTPS="true"
# Configure base path for self-hosting, optional
# Note: You need to build the docker image with the base path set and cannot use the pre-built docker image if you set this.
# NEXT_PUBLIC_BASE_PATH="/app"
# Docker only, optional
# PORT=3000
@@ -39,6 +42,10 @@ ENCRYPTION_KEY="0000000000000000000000000000000000000000000000000000000000000000
# LANGFUSE_DEFAULT_PROJECT_ID=
# LANGFUSE_DEFAULT_PROJECT_ROLE=
# Logging, optional
# LANGFUSE_LOG_LEVEL=info
# LANGFUSE_LOG_FORMAT=text
# Enable experimental features, optional
# LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES=true
@@ -93,10 +100,33 @@ ENCRYPTION_KEY="0000000000000000000000000000000000000000000000000000000000000000
# S3_REGION=
# BATCH_EXPORT_DOWNLOAD_LINK_EXPIRATION_HOURS=
# S3 storage for events, optional, used to persist all incoming events
# LANGFUSE_S3_EVENT_UPLOAD_ENABLED="true"
# LANGFUSE_S3_EVENT_UPLOAD_BUCKET=
# Optional prefix to be used within the bucket. Must end with `/` if set
# LANGFUSE_S3_EVENT_UPLOAD_PREFIX=events/
# The following four options are optional and fallback to the normal SDK credential provider chain if omitted
# See https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/setting-credentials-node.html
# LANGFUSE_S3_EVENT_UPLOAD_ENDPOINT=
# LANGFUSE_S3_EVENT_UPLOAD_REGION=
# LANGFUSE_S3_EVENT_UPLOAD_ACCESS_KEY_ID=
# LANGFUSE_S3_EVENT_UPLOAD_SECRET_ACCESS_KEY=
# Exports are streamed to S3 in pages to avoid memory issues
# The page size can be adjusted if needed to optimize performance
# DB_EXPORT_PAGE_SIZE=1000
# Automated provisioning of default resources
# LANGFUSE_INIT_ORG_ID=org-id
# LANGFUSE_INIT_ORG_NAME=org-name
# LANGFUSE_INIT_PROJECT_ID=project-id
# LANGFUSE_INIT_PROJECT_NAME=project-name
# LANGFUSE_INIT_PROJECT_PUBLIC_KEY=pk-1234567890
# LANGFUSE_INIT_PROJECT_SECRET_KEY=sk-1234567890
# LANGFUSE_INIT_USER_EMAIL=user@example.com
# LANGFUSE_INIT_USER_NAME=User Name
# LANGFUSE_INIT_USER_PASSWORD=password
### START Enterprise Edition Configuration
@@ -109,6 +139,12 @@ ENCRYPTION_KEY="0000000000000000000000000000000000000000000000000000000000000000
# LANGFUSE_UI_DOCUMENTATION_HREF=https://docs.example.com
# LANGFUSE_UI_SUPPORT_HREF=https://support.example.com
# LANGFUSE_UI_FEEDBACK_HREF=https://feedback.example.com
# LANGFUSE_UI_LOGO_LIGHT_MODE_HREF=https://static.langfuse.com/langfuse-dev/example-logo-light-mode.png
# LANGFUSE_UI_LOGO_DARK_MODE_HREF=https://static.langfuse.com/langfuse-dev/example-logo-dark-mode.png
# LANGFUSE_UI_DEFAULT_MODEL_ADAPTER=Anthropic # OpenAI, Anthropic, Azure
# LANGFUSE_UI_DEFAULT_BASE_URL_OPENAI=https://api.openai.com/v1
# LANGFUSE_UI_DEFAULT_BASE_URL_ANTHROPIC=https://api.anthropic.com
# LANGFUSE_UI_DEFAULT_BASE_URL_AZURE_OPENAI=https://{instanceName}.openai.azure.com/openai/deployments
### END Enterprise Edition Configuration
@@ -139,13 +175,12 @@ ENCRYPTION_KEY="0000000000000000000000000000000000000000000000000000000000000000
# NEXT_PUBLIC_POSTHOG_KEY=
# Sentry
# NEXT_PUBLIC_LANGFUSE_TRACING_SAMPLE_RATE
# NEXT_PUBLIC_SENTRY_DSN=
# NEXT_SENTRY_ORG=
# NEXT_SENTRY_PROJECT=
# SENTRY_AUTH_TOKEN=
# Used to determine the Sentry sample rate
# LANGFUSE_TRACING_SAMPLE_RATE=
# LANGFUSE_WORKER_BETTERSTACK_TOKEN=
# SENTRY_CSP_REPORT_URI=
# Cloudflare Turnstile
@@ -167,6 +202,7 @@ ENCRYPTION_KEY="0000000000000000000000000000000000000000000000000000000000000000
# REDIS_PORT=
# REDIS_AUTH=
# REDIS_CONNECTION_STRING=
# REDIS_ENABLE_AUTO_PIPELINING=
# Cache configuration
# LANGFUSE_CACHE_API_KEY_ENABLED=
@@ -174,6 +210,13 @@ ENCRYPTION_KEY="0000000000000000000000000000000000000000000000000000000000000000
# LANGFUSE_CACHE_PROMPT_ENABLED=
# LANGFUSE_CACHE_PROMPT_TTL_SECONDS=
# Rate limiting
# LANGFUSE_RATE_LIMITS_ENABLED=
# Stripe
# STRIPE_SECRET_KEY=
# STRIPE_WEBHOOK_SIGNING_SECRET=
### END Langfuse Cloud Config
@@ -193,8 +236,8 @@ ENCRYPTION_KEY="0000000000000000000000000000000000000000000000000000000000000000
# LANGFUSE_INGESTION_CLICKHOUSE_WRITE_BATCH_SIZE=
# LANGFUSE_INGESTION_CLICKHOUSE_WRITE_INTERVAL_MS=
# LANGFUSE_INGESTION_CLICKHOUSE_MAX_ATTEMPTS=
# LANGFUSE_LOG_LEVEL=
# LANGFUSE_LEGACY_INGESTION_WORKER_CONCURRENCY=
# LANGFUSE_ASYNC_INGESTION_PROCESSING="true"
# QUEUE_CONSUMER_LEGACY_INGESTION_QUEUE_IS_ENABLED="true"
## END Langfuse V3 Ingestion
+20 -7
View File
@@ -8,6 +8,7 @@ updates:
- package-ecosystem: npm
directory: "/" # Location of package manifests
rebase-strategy: "disabled" # use dependabot-rebase-stale
open-pull-requests-limit: 10
schedule:
interval: "daily"
versioning-strategy: "increase"
@@ -19,17 +20,29 @@ updates:
- dependency-name: "@types/node"
- dependency-name: "@trpc/*"
groups:
sentry:
patterns:
- "@sentry/*"
prisma:
patterns:
- "prisma"
- "@prisma/*"
- "@prisma/client"
next:
patterns:
- "eslint-config-next"
- "next"
patches:
update-types:
- "patch"
lodash:
patterns:
- "lodash"
- "@types/lodash"
express:
patterns:
- "express"
- "@types/express"
observability:
patterns:
- "dd-trace"
- "@opentelemetry/*"
- "@appsignal/opentelemetry-instrumentation-bullmq"
- "@prisma/instrumentation"
- "@sentry/*"
radix-ui:
patterns:
- "@radix-ui/*"
+75
View File
@@ -0,0 +1,75 @@
name: WorkflowCall - Deploy to ECS Service
on:
workflow_call:
inputs:
environment:
type: string
description: Deployment environment
required: true
service:
type: string
description: Name of the service to be deployed, e.g. web-ingestion, web, or worker.
required: true
jobs:
ecs-deploy:
runs-on: ubuntu-latest
environment: ${{ inputs.environment }}
steps:
- name: Get app name
uses: winterjung/split@v2
id: split
with:
msg: ${{ inputs.service }}
separator: "-"
- name: Checkout code
uses: actions/checkout@v4
- name: Authenticate with AWS
# GitHub/AWS recommend to use OIDC here: https://github.com/aws-actions/configure-aws-credentials?tab=readme-ov-file#oidc
# Probably more painful to configure, but would remove all long-lived credentials.
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: ${{ vars.AWS_REGION }}
- name: Login to AWS ECR
id: login-ecr
uses: aws-actions/amazon-ecr-login@v2
- name: Build, tag, and push Docker image
env:
REGISTRY: ${{ steps.login-ecr.outputs.registry }}
REPOSITORY: ${{ steps.split.outputs._0 }}
IMAGE_TAG: ${{ github.sha }}
run: |
docker build \
-t $REGISTRY/$REPOSITORY:$IMAGE_TAG \
-f ./${{ steps.split.outputs._0 }}/Dockerfile \
--build-arg NEXT_PUBLIC_LANGFUSE_CLOUD_REGION=${{ vars.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION }} \
--build-arg NEXT_LANGFUSE_TRACING_SAMPLE_RATE=${{ vars.NEXT_LANGFUSE_TRACING_SAMPLE_RATE }} \
--build-arg NEXT_PUBLIC_SENTRY_ENVIRONMENT=${{ vars.NEXT_PUBLIC_SENTRY_ENVIRONMENT }} \
--build-arg NEXT_PUBLIC_DEMO_ORG_ID=${{ vars.NEXT_PUBLIC_DEMO_ORG_ID }} \
--build-arg NEXT_PUBLIC_DEMO_PROJECT_ID=${{ vars.NEXT_PUBLIC_DEMO_PROJECT_ID }} \
--build-arg NEXT_PUBLIC_SENTRY_DSN=${{ vars.NEXT_PUBLIC_SENTRY_DSN }} \
--build-arg NEXT_PUBLIC_BUILD_ID=${{ github.sha }} \
--build-arg NEXT_PUBLIC_POSTHOG_KEY=${{ vars.NEXT_PUBLIC_POSTHOG_KEY }} \
--build-arg NEXT_PUBLIC_POSTHOG_HOST=${{ vars.NEXT_PUBLIC_POSTHOG_HOST }} \
--build-arg NEXT_PUBLIC_CRISP_WEBSITE_ID=${{ vars.NEXT_PUBLIC_CRISP_WEBSITE_ID }} \
--build-arg SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }} \
--build-arg SENTRY_ORG=${{ vars.SENTRY_ORG }} \
--build-arg SENTRY_PROJECT=${{ vars.SENTRY_PROJECT }} \
.
docker push $REGISTRY/$REPOSITORY:$IMAGE_TAG
- name: Render AWS ECS Task Definition
id: render-task-definition
uses: aws-actions/amazon-ecs-render-task-definition@v1
with:
container-name: ${{ inputs.service }}
image: ${{ steps.login-ecr.outputs.registry }}/${{ steps.split.outputs._0 }}:${{ github.sha }}
task-definition-family: ${{ inputs.environment }}-${{ inputs.service }}
- name: Update AWS ECS Service
uses: aws-actions/amazon-ecs-deploy-task-definition@v2
with:
task-definition: ${{ steps.render-task-definition.outputs.task-definition }}
service: ${{ inputs.environment }}-${{ inputs.service }}
cluster: ${{ inputs.environment }}-cluster
wait-for-service-stability: true
+105
View File
@@ -0,0 +1,105 @@
on:
push:
branches:
- main
- production
workflow_dispatch:
inputs:
service:
description: 'Service to be deployed'
type: choice
options:
- all
- web
- web-ingestion
- worker
required: true
environment:
description: 'Environment to deploy to'
type: choice
options:
- staging
- prod-eu-temp
- prod-us
required: true
concurrency:
# Support concurrent `push` and `workflow_dispatch`` actions
group: deploy-${{ github.event_name }}-${{ github.ref }}
cancel-in-progress: true
name: Deploy to ECS
jobs:
affected-services:
runs-on: ubuntu-latest
outputs:
services: ${{ steps.affected-services.outputs.result }}
steps:
- name: Get affected services
id: affected-services
uses: actions/github-script@v7
with:
script: |
if (context.eventName === "workflow_dispatch") {
if (context.payload.inputs.service === "all") {
return `["web", "web-ingestion", "worker"]`
}
return `["${context.payload.inputs.service}"]`
}
if (context.eventName === "push") {
return `["web", "web-ingestion", "worker"]`
}
return "[]"
result-encoding: string
- name: Print services to build
uses: actions/github-script@v7
env:
services: ${{ steps.affected-services.outputs.result }}
with:
result-encoding: string
script: |
console.log('Services', `${process.env.services}` ?? 'n/a');
affected-environments:
runs-on: ubuntu-latest
outputs:
environments: ${{ steps.affected-environments.outputs.result }}
steps:
- name: Get affected environments
id: affected-environments
uses: actions/github-script@v7
with:
script: |
if (context.eventName === "workflow_dispatch") {
return `["${context.payload.inputs.environment}"]`
}
if (context.eventName === "push") {
if (context.ref === "refs/heads/main") {
return `["staging"]`
}
if (context.ref === "refs/heads/production") {
return `["prod-eu-temp", "prod-us"]`
}
}
return "[]"
result-encoding: string
- name: Print environments to build
uses: actions/github-script@v7
env:
environments: ${{ steps.affected-environments.outputs.result }}
with:
result-encoding: string
script: |
console.log('Environments', `${process.env.environments}` ?? 'n/a');
ecs-deploy:
uses: ./.github/workflows/_deploy_ecs_service.yml
needs: [affected-services, affected-environments]
secrets: inherit
strategy:
matrix:
service: ${{ fromJson(needs.affected-services.outputs.services) }}
environment: ${{ fromJson(needs.affected-environments.outputs.environments) }}
with:
service: ${{ matrix.service }}
environment: ${{ matrix.environment }}
+28 -4
View File
@@ -40,6 +40,9 @@ jobs:
- name: Checkout
uses: actions/checkout@v3
- name: Set NEXT_PUBLIC_BUILD_ID
run: echo "NEXT_PUBLIC_BUILD_ID=$(git rev-parse --short HEAD)" >> $GITHUB_ENV
- name: Build and run both images from compose
run: |
docker compose -f docker-compose.build.yml up -d
@@ -94,7 +97,7 @@ jobs:
- name: Load default env
run: |
cp .env.dev.example .env
grep -v -e '^REDIS_HOST=' .env.dev.example > .env
grep -v -e '^REDIS_HOST=' -e '^NEXT_PUBLIC_LANGFUSE_RUN_NEXT_INIT=' .env.dev.example > .env
- name: Run + migrate
run: |
@@ -107,13 +110,22 @@ jobs:
- name: Seed DB
run: |
pnpm run db:migrate
pnpm run db:seed
- name: Build
run: pnpm run build
- name: Start Langfuse
run: (pnpm run start&)
env:
LANGFUSE_INIT_ORG_ID: "seed-org-id"
LANGFUSE_INIT_ORG_NAME: "Seed Org"
LANGFUSE_INIT_PROJECT_ID: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a"
LANGFUSE_INIT_PROJECT_NAME: "Seed Project"
LANGFUSE_INIT_PROJECT_PUBLIC_KEY: "pk-lf-1234567890"
LANGFUSE_INIT_PROJECT_SECRET_KEY: "sk-lf-1234567890"
LANGFUSE_INIT_USER_EMAIL: "demo@langfuse.com"
LANGFUSE_INIT_USER_NAME: "Demo User"
LANGFUSE_INIT_USER_PASSWORD: "password"
- name: run tests
run: pnpm --filter=web run test
@@ -136,6 +148,7 @@ jobs:
- uses: pnpm/action-setup@v3
with:
version: 9.5.0
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with:
@@ -165,18 +178,26 @@ jobs:
sleep 5 # Wait for PostgreSQL to accept connections
docker compose ps
- name: Ensure no unhealthy status
run: |
if docker compose ps | grep "(unhealthy)"; then
echo "One or more services are unhealthy"
exit 1
else
echo "All services are healthy"
fi
- name: Seed DB
run: |
pnpm run --filter=shared ch:up
pnpm run db:migrate
pnpm run db:seed
pnpm run --filter=shared ch:up
- name: Build
run: pnpm --filter=worker... run build
- name: run tests
run: pnpm --filter=worker run test
e2e-tests:
runs-on: ubuntu-latest
steps:
@@ -308,6 +329,9 @@ jobs:
- name: Checkout
uses: actions/checkout@v3
- name: Set NEXT_PUBLIC_BUILD_ID
run: echo "NEXT_PUBLIC_BUILD_ID=$(git rev-parse --short HEAD)" >> $GITHUB_ENV
- name: Log in to the GitHub Container registry
uses: docker/login-action@v2
with:
@@ -1,29 +0,0 @@
"on":
push:
branches:
- production
name: Deploy to web
jobs:
porter-deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Set Github tag
id: vars
run: echo "sha_short=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
- name: Setup porter
uses: porter-dev/setup-porter@v0.1.0
- name: Deploy stack
timeout-minutes: 30
run: exec porter apply
env:
PORTER_CLUSTER: "4060"
PORTER_DEPLOYMENT_TARGET_ID: 3be075c0-b121-4123-aec1-5dc571626b0c
PORTER_HOST: https://dashboard.porter.run
PORTER_PR_NUMBER: ${{ github.event.number }}
PORTER_PROJECT: "12565"
PORTER_REPO_NAME: ${{ github.event.repository.name }}
PORTER_STACK_NAME: web
PORTER_TAG: ${{ steps.vars.outputs.sha_short }}
PORTER_TOKEN: ${{ secrets.PORTER_STACK_12565_4060 }}
@@ -1,29 +0,0 @@
"on":
push:
branches:
- main
name: Deploy to web (Staging)
jobs:
porter-deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Set Github tag
id: vars
run: echo "sha_short=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
- name: Setup porter
uses: porter-dev/setup-porter@v0.1.0
- name: Deploy stack
timeout-minutes: 30
run: exec porter apply
env:
PORTER_CLUSTER: "4037"
PORTER_DEPLOYMENT_TARGET_ID: c22d8fa2-f97c-4e1f-9cae-83cc6bfbad35
PORTER_HOST: https://dashboard.getporter.dev
PORTER_PR_NUMBER: ${{ github.event.number }}
PORTER_PROJECT: "12565"
PORTER_REPO_NAME: ${{ github.event.repository.name }}
PORTER_STACK_NAME: web
PORTER_TAG: ${{ steps.vars.outputs.sha_short }}
PORTER_TOKEN: ${{ secrets.PORTER_STACK_12565_4037 }}
@@ -1,29 +0,0 @@
"on":
push:
branches:
- production
name: Deploy to web (US temp)
jobs:
porter-deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Set Github tag
id: vars
run: echo "sha_short=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
- name: Setup porter
uses: porter-dev/setup-porter@v0.1.0
- name: Deploy stack
timeout-minutes: 30
run: exec porter apply
env:
PORTER_CLUSTER: "4054"
PORTER_DEPLOYMENT_TARGET_ID: cddc0f2a-bb19-401a-9819-29f59f76b101
PORTER_HOST: https://dashboard.porter.run
PORTER_PR_NUMBER: ${{ github.event.number }}
PORTER_PROJECT: "12565"
PORTER_REPO_NAME: ${{ github.event.repository.name }}
PORTER_STACK_NAME: web
PORTER_TAG: ${{ steps.vars.outputs.sha_short }}
PORTER_TOKEN: ${{ secrets.PORTER_STACK_12565_4054 }}
@@ -1,29 +0,0 @@
"on":
push:
branches:
- production
name: Deploy to worker (EU temp)
jobs:
porter-deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Set Github tag
id: vars
run: echo "sha_short=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
- name: Setup porter
uses: porter-dev/setup-porter@v0.1.0
- name: Deploy stack
timeout-minutes: 30
run: exec porter apply
env:
PORTER_CLUSTER: "4060"
PORTER_DEPLOYMENT_TARGET_ID: 3be075c0-b121-4123-aec1-5dc571626b0c
PORTER_HOST: https://dashboard.porter.run
PORTER_PR_NUMBER: ${{ github.event.number }}
PORTER_PROJECT: "12565"
PORTER_REPO_NAME: ${{ github.event.repository.name }}
PORTER_STACK_NAME: worker
PORTER_TAG: ${{ steps.vars.outputs.sha_short }}
PORTER_TOKEN: ${{ secrets.PORTER_STACK_12565_4060 }}
@@ -1,29 +0,0 @@
"on":
push:
branches:
- main
name: Deploy to worker (staging)
jobs:
porter-deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Set Github tag
id: vars
run: echo "sha_short=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
- name: Setup porter
uses: porter-dev/setup-porter@v0.1.0
- name: Deploy stack
timeout-minutes: 30
run: exec porter apply
env:
PORTER_CLUSTER: "4037"
PORTER_DEPLOYMENT_TARGET_ID: c22d8fa2-f97c-4e1f-9cae-83cc6bfbad35
PORTER_HOST: https://dashboard.getporter.dev
PORTER_PR_NUMBER: ${{ github.event.number }}
PORTER_PROJECT: "12565"
PORTER_REPO_NAME: ${{ github.event.repository.name }}
PORTER_STACK_NAME: worker
PORTER_TAG: ${{ steps.vars.outputs.sha_short }}
PORTER_TOKEN: ${{ secrets.PORTER_STACK_12565_4037 }}
@@ -1,29 +0,0 @@
"on":
push:
branches:
- production
name: Deploy to worker (US temp)
jobs:
porter-deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Set Github tag
id: vars
run: echo "sha_short=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
- name: Setup porter
uses: porter-dev/setup-porter@v0.1.0
- name: Deploy stack
timeout-minutes: 30
run: exec porter apply
env:
PORTER_CLUSTER: "4054"
PORTER_DEPLOYMENT_TARGET_ID: cddc0f2a-bb19-401a-9819-29f59f76b101
PORTER_HOST: https://dashboard.porter.run
PORTER_PR_NUMBER: ${{ github.event.number }}
PORTER_PROJECT: "12565"
PORTER_REPO_NAME: ${{ github.event.repository.name }}
PORTER_STACK_NAME: worker
PORTER_TAG: ${{ steps.vars.outputs.sha_short }}
PORTER_TOKEN: ${{ secrets.PORTER_STACK_12565_4054 }}
+3 -3
View File
@@ -1,11 +1,11 @@
name: Snyk Container
on:
push:
branches-ignore:
- "dependabot/**"
pull_request:
branches:
- "**"
push:
branches:
- main
jobs:
snyk:
+2 -2
View File
@@ -2,10 +2,10 @@
"version": "0.2.0",
"configurations": [
{
"name": "Next.js: debug server-side",
"name": "Web and Worker: server side",
"type": "node-terminal",
"request": "launch",
"command": "npm run dev"
"command": "pnpm run dev"
},
{
"name": "Next.js: debug client-side",
+29 -30
View File
@@ -1,8 +1,7 @@
![langfuse_logo_1024](https://github.com/user-attachments/assets/f17d8fc1-8206-4afa-a5ec-774826be88a8)
![Langfuse GitHub Banner](https://github.com/langfuse/langfuse/assets/121163007/6035f0f3-d691-4963-b5d0-10cf506e9d42)
<div align="center"><h1> Langfuse: Open Source LLM Engineering Platform</h4></div>
<div align="center"><h2> Debug and improve your LLM app</h4></div>
<div align="center"><h3> LLM Observability, Prompt Management, LLM Evaluations, Datasets, LLM Metrics and Prompt Playground</h4></div>
<div align="center"><h1>Langfuse: Open Source LLM Engineering Platform</h1></div>
<div align="center"><h4>LLM Observability, Prompt Management, LLM Evaluations,<br/>Datasets, LLM Metrics, and Prompt Playground</h4></div>
<div align="center">
<div>
@@ -28,15 +27,15 @@
</div>
<span>Langfuse uses <a href="https://github.com/orgs/langfuse/discussions"><strong>Github Discussions</strong></a> for Support and Feature Requests.</span>
<br/>
<span>We're hiring. <a href="https://langfuse.com/careers"><strong>Join us</strong></a> in Backend Engineering, Product Engineering, and Developer Relations.</span>
<span>We're hiring. <a href="https://langfuse.com/careers"><strong>Join us</strong></a> in Product Engineering and Developer Relations.</span>
<br/>
<br/>
<div>
<a href="https://github.com/langfuse/langfuse/blob/main/LICENSE"><img src="https://img.shields.io/badge/License-MIT-red.svg?style=flat-square" alt="MIT License"></a>
<a href="https://www.ycombinator.com/companies/langfuse"><img src="https://img.shields.io/badge/Y%20Combinator-W23-orange?style=flat-square" alt="Y Combinator W23"></a>
<a href="https://github.com/langfuse/langfuse/pkgs/container/langfuse"><img alt="Docker Image" src="https://img.shields.io/badge/docker-langfuse-blue?logo=Docker&logoColor=white&style=flat-square"></a>
<a href="https://www.npmjs.com/package/langfuse"><img src="https://img.shields.io/npm/v/langfuse?style=flat-square&label=npm+langfuse" alt="langfuse npm package"></a>
<a href="https://pypi.python.org/pypi/langfuse"><img src="https://img.shields.io/pypi/v/langfuse.svg?style=flat-square&label=pypi+langfuse" alt="langfuse Python package on PyPi"></a>
<a href="https://pypi.python.org/pypi/langfuse"><img src="https://img.shields.io/pypi/dm/langfuse?style=flat-square&logo=python&logoColor=white&label=pypi%20langfuse&color=blue" alt="langfuse Python package on PyPi"></a>
<a href="https://www.npmjs.com/package/langfuse"><img src="https://img.shields.io/npm/dm/langfuse?style=flat-square&logo=npm&logoColor=white&label=npm%20langfuse&color=blue" alt="langfuse npm package"></a>
</div>
</div>
</br>
@@ -93,10 +92,10 @@ docker compose up -d
### Self-host (docker)
Langfuse is simple to self-host and keep updated. It currently requires only a single docker container.
Langfuse is simple to self-host and keep updated. It currently requires only a single docker container and a postgres database.
[→ Self Hosting Instructions](https://langfuse.com/docs/deployment/self-host)
Templated deployments: [Railway, GCP Cloud Run, AWS Fargate, Kubernetes and others](https://langfuse.com/docs/deployment/self-host#platform-specific-information)
Templated deployments: [Railway, GCP, AWS, Azure, Kubernetes and others](https://langfuse.com/docs/deployment/self-host#platform-specific-information)
## Get Started
@@ -108,31 +107,31 @@ You need a Langfuse public and secret key to get started. Sign up [here](https:/
Note: We recommend using our fully async, typed [SDKs](https://langfuse.com/docs/sdk) that allow you to instrument any LLM application with any underlying model. They are available in [Python (Decorators)](https://langfuse.com/docs/sdk/python) & [JS/TS](https://langfuse.com/docs/sdk/typescript). The SDKs will always be the most fully featured and stable way to ingest data into Langfuse.
You may want to use another integration to get started quickly or implement a use case that we do not yet support. However, we recommend to migrate to the Langfuse SDKs over time to ensure performance and stability.
See the [→ Quickstart](https://langfuse.com/docs/get-started) to integrate Langfuse.
### LLM Observability Integrations
| Integration | Supports | Description |
| -------------------------------------------------------- | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| [SDK](/docs/sdk) | Python, JS/TS | Manual instrumentation using the SDKs for full flexibility. |
| [OpenAI](/docs/integrations/openai) | Python, JS/TS | Automated instrumentation using drop-in replacement of OpenAI SDK. |
| [Langchain](/docs/integrations/langchain) | Python, JS/TS | Automated instrumentation by passing callback handler to Langchain application. |
| [LlamaIndex](/docs/integrations/llama-index/get-started) | Python | Automated instrumentation via LlamaIndex callback system. |
| [Haystack](/docs/integrations/haystack) | Python | Automated instrumentation via Haystack content tracing system. |
| [LiteLLM](/docs/integrations/litellm) | Python, JS/TS (proxy only) | Use any LLM as a drop in replacement for GPT. Use Azure, OpenAI, Cohere, Anthropic, Ollama, VLLM, Sagemaker, HuggingFace, Replicate (100+ LLMs). |
| [API](/docs/api) | | Directly call the public API. OpenAPI spec available. |
| Integration | Supports | Description |
| ---------------------------------------------------------------------------- | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| [SDK](https://langfuse.com/docs/sdk) | Python, JS/TS | Manual instrumentation using the SDKs for full flexibility. |
| [OpenAI](https://langfuse.com/docs/integrations/openai) | Python, JS/TS | Automated instrumentation using drop-in replacement of OpenAI SDK. |
| [Langchain](https://langfuse.com/docs/integrations/langchain) | Python, JS/TS | Automated instrumentation by passing callback handler to Langchain application. |
| [LlamaIndex](https://langfuse.com/docs/integrations/llama-index/get-started) | Python | Automated instrumentation via LlamaIndex callback system. |
| [Haystack](https://langfuse.com/docs/integrations/haystack) | Python | Automated instrumentation via Haystack content tracing system. |
| [LiteLLM](https://langfuse.com/docs/integrations/litellm) | Python, JS/TS (proxy only) | Use any LLM as a drop in replacement for GPT. Use Azure, OpenAI, Cohere, Anthropic, Ollama, VLLM, Sagemaker, HuggingFace, Replicate (100+ LLMs). |
| [Vercel AI SDK](https://langfuse.com/docs/integrations/vercel-ai-sdk) | JS/TS | TypeScript toolkit designed to help developers build AI-powered applications with React, Next.js, Vue, Svelte, Node.js. |
| [API](https://langfuse.com/docs/api) | | Directly call the public API. OpenAPI spec available. |
Packages that integrate with Langfuse:
Packages integrated with Langfuse:
| Name | Description |
| ---------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| [Instructor](/docs/integrations/instructor) | Library to get structured LLM outputs (JSON, Pydantic) |
| [Mirascope](/docs/integrations/mirascope) | Python toolkit for building LLM applications. |
| [AI SDK by Vercel](/docs/sdk/typescript/example-vercel-ai) | Typescript SDK that makes streaming LLM outputs super easy. |
| [Flowise](/docs/integrations/flowise) | JS/TS no-code builder for customized LLM flows. |
| [Langflow](/docs/integrations/langflow) | Python-based UI for LangChain, designed with react-flow to provide an effortless way to experiment and prototype flows. |
| Name | Description |
| --------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| [Instructor](https://langfuse.com/docs/integrations/instructor) | Library to get structured LLM outputs (JSON, Pydantic) |
| [Dify](https://langfuse.com/docs/integrations/dify) | Open source LLM app development platform with no-code builder. |
| [Ollama](https://langfuse.com/docs/integrations/ollama) | Easily run open source LLMs on your own machine. |
| [Mirascope](https://langfuse.com/docs/integrations/mirascope) | Python toolkit for building LLM applications. |
| [Flowise](https://langfuse.com/docs/integrations/flowise) | JS/TS no-code builder for customized LLM flows. |
| [Langflow](https://langfuse.com/docs/integrations/langflow) | Python-based UI for LangChain, designed with react-flow to provide an effortless way to experiment and prototype flows. |
## Questions and feedback
@@ -140,13 +139,13 @@ Packages that integrate with Langfuse:
- [Roadmap](https://langfuse.com/roadmap)
- [GitHub Discussions](https://github.com/orgs/langfuse/discussions)
- [Feature Requests](https://langfuse.com/idea
- [Feature Requests](https://langfuse.com/ideas)
### Support and feedback
In order of preference the best way to communicate with us:
- [GitHub Discussions](https://github.com/orgs/langfuse/discussions): Contribute [ideas](https://langfuse.com/idea) [support requests](https://github.com/orgs/langfuse/discussions/categories/support) and [report bugs](https://github.com/langfuse/langfuse/issues/new?labels=%F0%9F%90%9E%E2%9D%94+unconfirmed+bug&projects=&template=bug_report.yml&title=bug%3A+) (preferred as we create a permanent, indexed artifact for other community members)
- [GitHub Discussions](https://github.com/orgs/langfuse/discussions) (preferred): Contribute [ideas](https://langfuse.com/ideas), [support requests](https://langfuse.com/gh-support) and [report bugs](https://langfuse.com/issues)
- [Discord](https://langfuse.com/discord): community support
- Privately: contact at langfuse dot com
-1
View File
@@ -3,7 +3,6 @@ Join us in scaling Langfuse in Berlin, Germany. We are an open source company, w
_Open Roles_
- Backend Engineer, 70-130k EUR, 0.25-0.75% Equity, https://www.ycombinator.com/companies/langfuse/jobs/mnrdwla-backend-engineer
- Product Engineer, 70-130k EUR, 0.25-0.75% Equity, https://www.ycombinator.com/companies/langfuse/jobs/aAvmoFB-product-engineer
- Developer Advocate, 60-110k EUR, 0.25-0.5% Equity, https://www.ycombinator.com/companies/langfuse/jobs/uHysbKH-developer-advocate-devrel
+2 -1
View File
@@ -25,7 +25,8 @@
},
"dependencies": {
"@langfuse/shared": "workspace:*",
"next": "^14.2.3",
"@opentelemetry/api": ">=1.0.0 <1.9.0",
"next": "^14.2.6",
"next-auth": "^4.24.7",
"zod": "^3.23.8"
},
+2 -2
View File
@@ -31,10 +31,10 @@ service:
docs: Optional filter for metrics where traces include all of these tags
fromTimestamp:
type: optional<datetime>
docs: Optional filter to only include traces on or after a certain datetime (ISO 8601)
docs: Optional filter to only include traces and observations on or after a certain datetime (ISO 8601)
toTimestamp:
type: optional<datetime>
docs: Optional filter to only include traces before a certain datetime (ISO 8601)
docs: Optional filter to only include traces and observations before a certain datetime (ISO 8601)
response: DailyMetrics
types:
DailyMetrics:
+3 -13
View File
@@ -3,32 +3,22 @@ groups:
local:
generators:
- name: fernapi/fern-openapi
version: 0.0.28
version: 0.0.31
output:
location: local-file-system
path: ../../../web/public/generated/api
config:
namespaceExport: Langfuse
allowCustomFetcher: true
- name: fernapi/fern-python-sdk
version: 1.1.0-rc2
version: 2.16.0
output:
location: local-file-system
path: ../../../generated/python
config:
client_class_name: FernLangfuse
improved_imports: false
inline_request_params: false
pydantic_config:
require_optional_fields: false
use_str_enums: false
- name: fernapi/fern-typescript-node-sdk
version: 0.7.1
output:
location: local-file-system
path: ../../../generated/typescript-server
config:
namespaceExport: Langfuse
allowCustomFetcher: true
- name: fernapi/fern-postman
version: 0.0.45
output:
+1 -1
View File
@@ -1,4 +1,4 @@
{
"organization": "finto",
"version": "0.30.3"
"version": "0.43.7"
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "langfuse",
"version": "2.71.0",
"version": "2.81.2",
"author": "engineering@langfuse.com",
"license": "MIT",
"private": true,
+1 -1
View File
@@ -11,7 +11,7 @@
"@typescript-eslint/eslint-plugin": "^7.1.0",
"@typescript-eslint/parser": "^7.12.0",
"@vercel/style-guide": "^6.0.0",
"eslint-config-next": "^14.2.3",
"eslint-config-next": "^14.2.6",
"eslint-config-prettier": "^9.1.0",
"eslint-config-turbo": "^1.13.4",
"eslint-plugin-only-warn": "^1.1.0",
+1
View File
@@ -18,5 +18,6 @@ if [ "$CLICKHOUSE_MIGRATION_SSL" = true ] ; then
else
DATABASE_URL="${CLICKHOUSE_MIGRATION_URL}?username=${CLICKHOUSE_USER}&password=${CLICKHOUSE_PASSWORD}&database=default&x-multi-statement=true&x-migrations-table-engine=MergeTree"
fi
# Execute the up command
migrate -source file://clickhouse/migrations -database "$DATABASE_URL" up
+21 -13
View File
@@ -21,6 +21,10 @@
"import": "./dist/src/server/index.js",
"require": "./dist/src/server/index.js"
},
"./src/server/auth/apiKeys": {
"import": "./dist/src/server/auth/apiKeys.js",
"require": "./dist/src/server/auth/apiKeys.js"
},
"./encryption": {
"import": "./dist/src/encryption/index.js",
"require": "./dist/src/encryption/index.js"
@@ -51,35 +55,39 @@
"dependencies": {
"@anthropic-ai/tokenizer": "^0.0.4",
"@aws-sdk/client-s3": "^3.627.0",
"@aws-sdk/lib-storage": "^3.568.0",
"@aws-sdk/lib-storage": "^3.645.0",
"@aws-sdk/s3-request-presigner": "^3.554.0",
"@clickhouse/client": "^1.4.0",
"@langchain/anthropic": "^0.1.21",
"@langchain/core": "^0.1.61",
"@langchain/openai": "^0.0.33",
"@prisma/client": "^5.13.0",
"@langchain/anthropic": "^0.3.1",
"@langchain/core": "^0.3.3",
"@langchain/openai": "^0.3.0",
"@opentelemetry/api": ">=1.0.0 <1.9.0",
"@prisma/client": "^5.17.0",
"@react-email/components": "^0.0.19",
"@react-email/render": "^0.0.15",
"@types/bcryptjs": "^2.4.6",
"bcryptjs": "^2.4.3",
"bullmq": "^5.4.2",
"bullmq": "^5.12.10",
"dd-trace": "^5.19.0",
"decimal.js": "^10.4.3",
"exponential-backoff": "^3.1.1",
"ioredis": "^5.4.1",
"kysely": "^0.27.3",
"langchain": "^0.2.6",
"kysely": "^0.27.4",
"langchain": "^0.3.2",
"lodash": "^4.17.21",
"next-auth": "^4.24.7",
"nodemailer": "^6.9.13",
"prisma-extension-kysely": "^2.1.0",
"uuid": "^9.0.1",
"winston": "^3.14.2",
"winston-transport": "^4.7.1",
"zod": "^3.23.8",
"zod-to-json-schema": "^3.22.5"
"zod-to-json-schema": "^3.23.2"
},
"devDependencies": {
"@repo/eslint-config": "workspace:*",
"@repo/typescript-config": "workspace:*",
"@types/lodash": "^4.17.5",
"@types/lodash": "^4.17.9",
"@types/node": "^20.11.29",
"@types/nodemailer": "^6.4.15",
"@types/pg": "^8.11.6",
@@ -92,7 +100,7 @@
"kysely-codegen": "^0.11.0",
"nodemon": "^3.1.3",
"prettier": "^3.2.5",
"prisma": "^5.13.0",
"prisma": "^5.17.0",
"prisma-erd-generator": "^1.11.2",
"prisma-kysely": "^1.8.0",
"ts-node": "^10.9.2",
@@ -101,7 +109,7 @@
"vitest": "^1.5.3"
},
"peerDependencies": {
"@types/react": "^18.2.79",
"react": "^18.0.0"
"@types/react": "~18.2.79",
"react": "~18.2.0"
}
}
+19
View File
@@ -42,6 +42,13 @@ export const DatasetStatus = {
ARCHIVED: "ARCHIVED"
} as const;
export type DatasetStatus = (typeof DatasetStatus)[keyof typeof DatasetStatus];
export const CommentObjectType = {
TRACE: "TRACE",
OBSERVATION: "OBSERVATION",
SESSION: "SESSION",
PROMPT: "PROMPT"
} as const;
export type CommentObjectType = (typeof CommentObjectType)[keyof typeof CommentObjectType];
export const JobType = {
EVAL: "EVAL"
} as const;
@@ -73,6 +80,7 @@ export type Account = {
scope: string | null;
id_token: string | null;
session_state: string | null;
refresh_token_expires_in: number | null;
};
export type ApiKey = {
id: string;
@@ -116,6 +124,16 @@ export type BatchExport = {
url: string | null;
log: string | null;
};
export type Comment = {
id: string;
project_id: string;
object_type: CommentObjectType;
object_id: string;
created_at: Generated<Timestamp>;
updated_at: Generated<Timestamp>;
content: string;
author_user_id: string | null;
};
export type CronJobs = {
name: string;
last_run: Timestamp | null;
@@ -491,6 +509,7 @@ export type DB = {
api_keys: ApiKey;
audit_logs: AuditLog;
batch_exports: BatchExport;
comments: Comment;
cron_jobs: CronJobs;
dataset_items: DatasetItem;
dataset_run_items: DatasetRunItems;
@@ -0,0 +1,22 @@
-- CreateEnum
CREATE TYPE "CommentObjectType" AS ENUM ('TRACE', 'OBSERVATION', 'SESSION', 'PROMPT');
-- CreateTable
CREATE TABLE "comments" (
"id" TEXT NOT NULL,
"project_id" TEXT NOT NULL,
"object_type" "CommentObjectType" NOT NULL,
"object_id" TEXT NOT NULL,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"content" TEXT NOT NULL,
"author_user_id" TEXT,
CONSTRAINT "comments_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE INDEX "comments_project_id_object_type_object_id_idx" ON "comments"("project_id", "object_type", "object_id");
-- AddForeignKey
ALTER TABLE "comments" ADD CONSTRAINT "comments_project_id_fkey" FOREIGN KEY ("project_id") REFERENCES "projects"("id") ON DELETE CASCADE ON UPDATE CASCADE;
@@ -0,0 +1,30 @@
-- For OpenAI reasoning models, manually tokenizing output string does not account for reasoning tokens, thus would lead to incorrect cost
-- So no manual tokenizer config is provided
-- Reference: https://platform.openai.com/docs/guides/reasoning/how-reasoning-works
INSERT INTO models (
id,
project_id,
model_name,
match_pattern,
start_date,
input_price,
output_price,
total_price,
unit,
tokenizer_id,
tokenizer_config
)
VALUES
-- o1-preview
('cm10ivcdp0000gix7lelmbw80', NULL, 'o1-preview', '(?i)^(o1-preview)$', NULL, 0.000015, 0.00006, NULL, 'TOKENS', NULL, NULL),
-- o1-preview-2024-09-12
('cm10ivo130000n8x7qopcjjcg', NULL, 'o1-preview-2024-09-12', '(?i)^(o1-preview-2024-09-12)$', NULL, 0.000015, 0.00006, NULL, 'TOKENS', NULL, NULL),
-- o1-mini
('cm10ivwo40000r1x7gg3syjq0', NULL, 'o1-mini', '(?i)^(o1-mini)$', NULL, 0.000003, 0.000012, NULL, 'TOKENS', NULL, NULL),
-- o1-mini-2024-09-12
('cm10iw6p20000wgx7it1hlb22', NULL, 'o1-mini-2024-09-12', '(?i)^(o1-mini-2024-09-12)$', NULL, 0.000003, 0.000012, NULL, 'TOKENS', NULL, NULL)
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Account" ADD COLUMN "refresh_token_expires_in" INTEGER;
@@ -0,0 +1,3 @@
-- DropIndex
-- Removes indexes that are covered by other indexes on the same table, e.g. (project_id) if (project_id, timestamp) exists.
DROP INDEX CONCURRENTLY IF EXISTS "traces_project_id_idx"; -- covered by traces_project_id_timestamp_idx
@@ -0,0 +1,3 @@
-- DropIndex
-- Removes indexes that are covered by other indexes on the same table, e.g. (project_id) if (project_id, timestamp) exists.
DROP INDEX CONCURRENTLY IF EXISTS "datasets_project_id_idx"; -- covered by datasets_project_id_name_key (unique)
@@ -0,0 +1,3 @@
-- DropIndex
-- Removes indexes that are covered by other indexes on the same table, e.g. (project_id) if (project_id, timestamp) exists.
DROP INDEX CONCURRENTLY IF EXISTS "eval_templates_project_id_idx"; -- covered by eval_templates_project_id_id_idx
@@ -0,0 +1,3 @@
-- DropIndex
-- Removes indexes that are covered by other indexes on the same table, e.g. (project_id) if (project_id, timestamp) exists.
DROP INDEX CONCURRENTLY IF EXISTS "job_configurations_project_id_idx"; -- covered by job_configurations_project_id_id_idx
@@ -0,0 +1,3 @@
-- DropIndex
-- Removes indexes that are covered by other indexes on the same table, e.g. (project_id) if (project_id, timestamp) exists.
DROP INDEX CONCURRENTLY IF EXISTS "job_executions_project_id_idx"; -- covered by job_executions_project_id_id_idx
@@ -0,0 +1,3 @@
-- DropIndex
-- Removes indexes that are covered by other indexes on the same table, e.g. (project_id) if (project_id, timestamp) exists.
DROP INDEX CONCURRENTLY IF EXISTS "llm_api_keys_project_id_provider_idx"; -- covered by llm_api_keys_project_id_provider_key (unique)
@@ -0,0 +1,3 @@
-- DropIndex
-- Removes indexes that are covered by other indexes on the same table, e.g. (project_id) if (project_id, timestamp) exists.
DROP INDEX CONCURRENTLY IF EXISTS "models_project_id_model_name_idx"; -- covered by models_project_id_model_name_start_date_unit_key (unique)
@@ -0,0 +1,3 @@
-- DropIndex
-- Removes indexes that are covered by other indexes on the same table, e.g. (project_id) if (project_id, timestamp) exists.
DROP INDEX CONCURRENTLY IF EXISTS "models_project_id_model_name_start_date_unit_idx"; -- covered by models_project_id_model_name_start_date_unit_key (unique)
@@ -0,0 +1,3 @@
-- DropIndex
-- Removes indexes that are covered by other indexes on the same table, e.g. (project_id) if (project_id, timestamp) exists.
DROP INDEX CONCURRENTLY IF EXISTS "observations_project_id_idx"; -- covered by observations_project_id_start_time_type_idx
@@ -0,0 +1,3 @@
-- DropIndex
-- Removes indexes that are covered by other indexes on the same table, e.g. (project_id) if (project_id, timestamp) exists.
DROP INDEX CONCURRENTLY IF EXISTS "observations_trace_id_idx"; -- covered by observations_trace_id_project_id_start_time_idx
@@ -0,0 +1,3 @@
-- DropIndex
-- Removes indexes that are covered by other indexes on the same table, e.g. (project_id) if (project_id, timestamp) exists.
DROP INDEX CONCURRENTLY IF EXISTS "observations_trace_id_project_id_idx"; -- covered by observations_trace_id_project_id_start_time_idx
@@ -0,0 +1,3 @@
-- DropIndex
-- Removes indexes that are covered by other indexes on the same table, e.g. (project_id) if (project_id, timestamp) exists.
DROP INDEX CONCURRENTLY IF EXISTS "organization_memberships_org_id_idx"; -- covered by organization_memberships_org_id_user_id_key (unique)
@@ -0,0 +1,3 @@
-- DropIndex
-- Removes indexes that are covered by other indexes on the same table, e.g. (project_id) if (project_id, timestamp) exists.
DROP INDEX CONCURRENTLY IF EXISTS "posthog_integrations_project_id_idx"; -- covered by posthog_integrations_pkey (unique)
@@ -0,0 +1,3 @@
-- DropIndex
-- Removes indexes that are covered by other indexes on the same table, e.g. (project_id) if (project_id, timestamp) exists.
DROP INDEX CONCURRENTLY IF EXISTS "prompts_project_id_idx"; -- covered by prompts_project_id_name_version_key (unique)
@@ -0,0 +1,3 @@
-- DropIndex
-- Removes indexes that are covered by other indexes on the same table, e.g. (project_id) if (project_id, timestamp) exists.
DROP INDEX CONCURRENTLY IF EXISTS "prompts_project_id_name_version_idx"; -- covered by prompts_project_id_name_version_key (unique)
@@ -0,0 +1,3 @@
-- DropIndex
-- Removes indexes that are covered by other indexes on the same table, e.g. (project_id) if (project_id, timestamp) exists.
DROP INDEX CONCURRENTLY IF EXISTS "scores_project_id_idx"; -- covered by scores_project_id_name_idx
File diff suppressed because it is too large Load Diff
+168 -80
View File
@@ -12,7 +12,7 @@ import { parseArgs } from "node:util";
import { chunk } from "lodash";
import { v4 } from "uuid";
import { ModelUsageUnit } from "../src";
import { getDisplaySecretKey, hashSecretKey } from "../src/server";
import { getDisplaySecretKey, hashSecretKey, logger } from "../src/server";
import { encrypt } from "../src/encryption";
import { redis } from "../src/server/redis/redis";
@@ -257,22 +257,30 @@ async function main() {
const traceVolume = environment === "load" ? LOAD_TRACE_VOLUME : 100;
const { traces, observations, scores, sessions, events } = createObjects(
traceVolume,
envTags,
colorTags,
project1,
project2,
promptIds,
configIdsAndNames
const { traces, observations, scores, sessions, events, comments } =
createObjects(
traceVolume,
envTags,
colorTags,
project1,
project2,
promptIds,
configIdsAndNames,
);
logger.info(
`Seeding ${traces.length} traces, ${observations.length} observations, and ${scores.length} scores`,
);
console.log(
`Seeding ${traces.length} traces, ${observations.length} observations, and ${scores.length} scores`
await uploadObjects(
traces,
observations,
scores,
sessions,
events,
comments,
);
await uploadObjects(traces, observations, scores, sessions, events);
// If openai key is in environment, add it to the projects LLM API keys
const OPENAI_API_KEY = process.env.OPENAI_API_KEY;
@@ -287,8 +295,8 @@ async function main() {
},
});
} else {
console.warn(
"No OPENAI_API_KEY found in environment. Skipping seeding LLM API key."
logger.warn(
"No OPENAI_API_KEY found in environment. Skipping seeding LLM API key.",
);
}
@@ -423,7 +431,7 @@ async function main() {
for (const datasetItemId of datasetItemIds) {
const relevantObservations = observations.filter(
(o) => o.projectId === project2.id
(o) => o.projectId === project2.id,
);
const observation =
relevantObservations[
@@ -449,13 +457,13 @@ main()
.then(async () => {
await prisma.$disconnect();
redis?.disconnect();
console.log("Disconnected from postgres and redis");
logger.info("Disconnected from postgres and redis");
})
.catch(async (e) => {
console.error(e);
logger.error(e);
await prisma.$disconnect();
redis?.disconnect();
console.log("Disconnected from postgres and redis");
logger.info("Disconnected from postgres and redis");
process.exit(1);
});
@@ -464,7 +472,8 @@ async function uploadObjects(
observations: Prisma.ObservationCreateManyInput[],
scores: Prisma.ScoreCreateManyInput[],
sessions: Prisma.TraceSessionCreateManyInput[],
events: Prisma.ObservationCreateManyInput[]
events: Prisma.ObservationCreateManyInput[],
comments: Prisma.CommentCreateManyInput[],
) {
let promises: Prisma.PrismaPromise<unknown>[] = [];
@@ -478,14 +487,14 @@ async function uploadObjects(
},
create: chunk[0]!,
update: {},
})
}),
);
});
for (let i = 0; i < promises.length; i++) {
if (i + 1 >= promises.length || i % Math.ceil(promises.length / 10) === 0)
console.log(
`Seeding of Sessions ${((i + 1) / promises.length) * 100}% complete`
logger.info(
`Seeding of Sessions ${((i + 1) / promises.length) * 100}% complete`,
);
await promises[i];
}
@@ -496,13 +505,13 @@ async function uploadObjects(
promises.push(
prisma.trace.createMany({
data: chunk,
})
}),
);
});
for (let i = 0; i < promises.length; i++) {
if (i + 1 >= promises.length || i % Math.ceil(promises.length / 10) === 0)
console.log(
`Seeding of Traces ${((i + 1) / promises.length) * 100}% complete`
logger.info(
`Seeding of Traces ${((i + 1) / promises.length) * 100}% complete`,
);
await promises[i];
}
@@ -512,14 +521,14 @@ async function uploadObjects(
promises.push(
prisma.observation.createMany({
data: chunk,
})
}),
);
});
for (let i = 0; i < promises.length; i++) {
if (i + 1 >= promises.length || i % Math.ceil(promises.length / 10) === 0)
console.log(
`Seeding of Observations ${((i + 1) / promises.length) * 100}% complete`
logger.info(
`Seeding of Observations ${((i + 1) / promises.length) * 100}% complete`,
);
await promises[i];
}
@@ -529,14 +538,14 @@ async function uploadObjects(
promises.push(
prisma.observation.createMany({
data: chunk,
})
}),
);
});
for (let i = 0; i < promises.length; i++) {
if (i + 1 >= promises.length || i % Math.ceil(promises.length / 10) === 0)
console.log(
`Seeding of Events ${((i + 1) / promises.length) * 100}% complete`
logger.info(
`Seeding of Events ${((i + 1) / promises.length) * 100}% complete`,
);
await promises[i];
}
@@ -546,13 +555,29 @@ async function uploadObjects(
promises.push(
prisma.score.createMany({
data: chunk,
})
}),
);
});
for (let i = 0; i < promises.length; i++) {
if (i + 1 >= promises.length || i % Math.ceil(promises.length / 10) === 0)
console.log(
`Seeding of Scores ${((i + 1) / promises.length) * 100}% complete`
logger.info(
`Seeding of Scores ${((i + 1) / promises.length) * 100}% complete`,
);
await promises[i];
}
promises = [];
chunk(comments, chunkSize).forEach((chunk) => {
promises.push(
prisma.comment.createMany({
data: chunk,
}),
);
});
for (let i = 0; i < promises.length; i++) {
if (i + 1 >= promises.length || i % Math.ceil(promises.length / 10) === 0)
logger.info(
`Seeding of Comments ${((i + 1) / promises.length) * 100}% complete`,
);
await promises[i];
}
@@ -573,7 +598,7 @@ function createObjects(
dataType: ScoreDataType;
categories: ConfigCategory[] | null;
}[]
>
>,
) {
const traces: Prisma.TraceCreateManyInput[] = [];
const observations: Prisma.ObservationCreateManyInput[] = [];
@@ -581,12 +606,13 @@ function createObjects(
const sessions: Prisma.TraceSessionCreateManyInput[] = [];
const events: Prisma.ObservationCreateManyInput[] = [];
const configs: Prisma.ScoreConfigCreateManyInput[] = [];
const comments: Prisma.CommentCreateManyInput[] = [];
for (let i = 0; i < traceVolume; i++) {
// print progress to console with a progress bar that refreshes every 10 iterations
// random date within last 90 days, with a linear bias towards more recent dates
const traceTs = new Date(
Date.now() - Math.floor(Math.random() ** 1.5 * 90 * 24 * 60 * 60 * 1000)
Date.now() - Math.floor(Math.random() ** 1.5 * 90 * 24 * 60 * 60 * 1000),
);
const envTag = envTags[Math.floor(Math.random() * envTags.length)];
@@ -611,6 +637,7 @@ function createObjects(
const trace = {
id: `trace-${v4()}`,
timestamp: traceTs,
createdAt: traceTs,
projectId: projectId,
name: ["generate-outreach", "label-inbound", "draft-response"][
i % 3
@@ -669,6 +696,7 @@ function createObjects(
traceId: trace.id,
name: annotationScoreName,
timestamp: traceTs,
createdAt: traceTs,
source: ScoreSource.ANNOTATION,
projectId,
authorUserId: `user-${i}`,
@@ -685,6 +713,7 @@ function createObjects(
name: "sentiment",
value: Math.floor(Math.random() * 10) - 5,
timestamp: traceTs,
createdAt: traceTs,
source: ScoreSource.API,
projectId,
dataType: ScoreDataType.NUMERIC,
@@ -697,6 +726,7 @@ function createObjects(
traceId: trace.id,
name: "Completeness",
timestamp: traceTs,
createdAt: traceTs,
source: ScoreSource.API,
projectId,
dataType: ScoreDataType.CATEGORICAL,
@@ -707,6 +737,15 @@ function createObjects(
: []),
];
if (Math.random() > 0.9)
comments.push({
projectId: trace.projectId,
objectId: trace.id,
objectType: "TRACE",
content: "Trace comment content",
...(Math.random() > 0.5 ? { authorUserId: `user-${i}` } : {}),
});
scores.push(...traceScores);
const existingSpanIds: string[] = [];
@@ -714,17 +753,18 @@ function createObjects(
for (let j = 0; j < Math.floor(Math.random() * 10) + 1; j++) {
// add between 1 and 30 ms to trace timestamp
const spanTsStart = new Date(
traceTs.getTime() + Math.floor(Math.random() * 30)
traceTs.getTime() + Math.floor(Math.random() * 30),
);
// random duration of upto 5000ms
const spanTsEnd = new Date(
spanTsStart.getTime() + Math.floor(Math.random() * 5000)
spanTsStart.getTime() + Math.floor(Math.random() * 5000),
);
const span = {
type: ObservationType.SPAN,
id: `span-${v4()}`,
startTime: spanTsStart,
createdAt: spanTsStart,
endTime: spanTsEnd,
name: `span-${i}-${j}`,
metadata: {
@@ -752,22 +792,22 @@ function createObjects(
const generationTsStart = new Date(
spanTsStart.getTime() +
Math.floor(
Math.random() * (spanTsEnd.getTime() - spanTsStart.getTime())
)
Math.random() * (spanTsEnd.getTime() - spanTsStart.getTime()),
),
);
const generationTsEnd = new Date(
generationTsStart.getTime() +
Math.floor(
Math.random() *
(spanTsEnd.getTime() - generationTsStart.getTime())
)
(spanTsEnd.getTime() - generationTsStart.getTime()),
),
);
// somewhere in the middle
const generationTsCompletionStart = new Date(
generationTsStart.getTime() +
Math.floor(
(generationTsEnd.getTime() - generationTsStart.getTime()) / 3
)
(generationTsEnd.getTime() - generationTsStart.getTime()) / 3,
),
);
const promptTokens = Math.floor(Math.random() * 1000) + 300;
@@ -788,49 +828,25 @@ function createObjects(
const promptId =
promptIds.get(projectId)![
Math.floor(
Math.random() * Math.floor(promptIds.get(projectId)!.length / 2)
Math.random() * Math.floor(promptIds.get(projectId)!.length / 2),
)
];
const { input, output } = getGenerationInputOutput();
const generation = {
type: ObservationType.GENERATION,
id: `generation-${v4()}`,
startTime: generationTsStart,
createdAt: generationTsStart,
endTime: generationTsEnd,
completionStartTime:
Math.random() > 0.5 ? generationTsCompletionStart : undefined,
name: `generation-${i}-${j}-${k}`,
projectId: trace.projectId,
promptId: promptId,
input:
Math.random() > 0.5
? [
{
role: "system",
content: "Be a helpful assistant",
},
{
role: "user",
content: "How can i create a *React* component?",
},
]
: {
input: "How can i create a React component?",
retrievedDocuments: [
{
title: "How to create a React component",
url: "https://www.google.com",
description: "A guide to creating React components",
},
{
title: "React component creation",
url: "https://www.google.com",
description: "A guide to creating React components",
},
],
},
output:
"Creating a React component can be done in two ways: as a functional component or as a class component. Let's start with a basic example of both.\n\n**Image**\n\n![Languse Example Image](https://static.langfuse.com/langfuse-dev/langfuse-example-image.jpeg)\n\n1. **Functional Component**:\n\nA functional component is just a plain JavaScript function that accepts props as an argument, and returns a React element. Here's how you can create one:\n\n```javascript\nimport React from 'react';\nfunction Greeting(props) {\n return <h1>Hello, {props.name}</h1>;\n}\nexport default Greeting;\n```\n\nTo use this component in another file, you can do:\n\n```javascript\nimport Greeting from './Greeting';\nfunction App() {\n return (\n <div>\n <Greeting name=\"John\" />\n </div>\n );\n}\nexport default App;\n```\n\n2. **Class Component**:\n\nYou can also define components as classes in React. These have some additional features compared to functional components:\n\n```javascript\nimport React, { Component } from 'react';\nclass Greeting extends Component {\n render() {\n return <h1>Hello, {this.props.name}</h1>;\n }\n}\nexport default Greeting;\n```\n\nAnd here's how to use this component:\n\n```javascript\nimport Greeting from './Greeting';\nclass App extends Component {\n render() {\n return (\n <div>\n <Greeting name=\"John\" />\n </div>\n );\n }\n}\nexport default App;\n```\n\nWith the advent of hooks in React, functional components can do everything that class components can do and hence, the community has been favoring functional components over class components.\n\nRemember to import React at the top of your file whenever you're creating a component, because JSX transpiles to `React.createElement` calls under the hood.",
input,
output,
model: model,
internalModel: model,
modelParameters: {
@@ -867,6 +883,7 @@ function createObjects(
source: ScoreSource.API,
projectId: trace.projectId,
timestamp: generationTsEnd,
createdAt: traceTs,
});
if (Math.random() > 0.6)
scores.push({
@@ -877,6 +894,15 @@ function createObjects(
source: ScoreSource.API,
projectId: trace.projectId,
timestamp: generationTsEnd,
createdAt: traceTs,
});
if (Math.random() > 0.8)
comments.push({
projectId: trace.projectId,
objectId: generation.id,
objectType: "OBSERVATION",
content: "Observation comment content",
});
for (let l = 0; l < Math.floor(Math.random() * 2); l++) {
@@ -884,14 +910,15 @@ function createObjects(
const eventTs = new Date(
spanTsStart.getTime() +
Math.floor(
Math.random() * (spanTsEnd.getTime() - spanTsStart.getTime())
)
Math.random() * (spanTsEnd.getTime() - spanTsStart.getTime()),
),
);
events.push({
type: ObservationType.EVENT,
id: `event-${v4()}`,
startTime: eventTs,
createdAt: eventTs,
name: `event-${i}-${j}-${k}-${l}`,
metadata: {
user: `user-${i}@langfuse.com`,
@@ -906,7 +933,7 @@ function createObjects(
}
// find unique sessions by id and projectid
const uniqueSessions: Prisma.TraceSessionCreateManyInput[] = Array.from(
new Set(sessions.map((session) => JSON.stringify(session)))
new Set(sessions.map((session) => JSON.stringify(session))),
).map((session) => JSON.parse(session) as Prisma.TraceSessionCreateManyInput);
return {
@@ -916,6 +943,7 @@ function createObjects(
configs,
sessions: uniqueSessions,
events,
comments,
};
}
@@ -926,7 +954,7 @@ async function generatePromptsForProject(projects: Project[]) {
projects.map(async (project) => {
const promptIdsForProject = await generatePrompts(project);
promptIds.set(project.id, promptIdsForProject);
})
}),
);
return promptIds;
}
@@ -1112,7 +1140,7 @@ async function generateConfigsForProject(projects: Project[]) {
projects.map(async (project) => {
const configNameAndId = await generateConfigs(project);
projectIdsToConfigs.set(project.id, configNameAndId);
})
}),
);
return projectIdsToConfigs;
}
@@ -1190,3 +1218,63 @@ async function generateConfigs(project: Project) {
return configNameAndId;
}
function getGenerationInputOutput(): {
input: Prisma.InputJsonValue;
output: Prisma.InputJsonValue;
} {
if (Math.random() > 0.9) {
const input = [
{
role: "user",
content: [
{ text: "Whats depicted in this image?", type: "text" },
{
type: "image_url",
image_url: {
url: "https://upload.wikimedia.org/wikipedia/commons/thumb/d/dd/Gfp-wisconsin-madison-the-nature-boardwalk.jpg/2560px-Gfp-wisconsin-madison-the-nature-boardwalk.jpg",
},
},
{ text: "Describe the scene in detail.", type: "text" },
],
},
];
const output =
"The image depicts a serene landscape featuring a wooden pathway or boardwalk that winds through a lush green field. The field is filled with tall grass and surrounded by trees and shrubs. Above, the sky is bright with scattered clouds, suggesting a clear and pleasant day. The scene conveys a sense of tranquility and natural beauty.";
return { input, output };
}
const input =
Math.random() > 0.5
? [
{
role: "system",
content: "Be a helpful assistant",
},
{
role: "user",
content: "How can i create a *React* component?",
},
]
: {
input: "How can i create a React component?",
retrievedDocuments: [
{
title: "How to create a React component",
url: "https://www.google.com",
description: "A guide to creating React components",
},
{
title: "React component creation",
url: "https://www.google.com",
description: "A guide to creating React components",
},
],
};
const output =
"Creating a React component can be done in two ways: as a functional component or as a class component. Let's start with a basic example of both.\n\n**Image**\n\n![Languse Example Image](https://static.langfuse.com/langfuse-dev/langfuse-example-image.jpeg)\n\n1. **Functional Component**:\n\nA functional component is just a plain JavaScript function that accepts props as an argument, and returns a React element. Here's how you can create one:\n\n```javascript\nimport React from 'react';\nfunction Greeting(props) {\n return <h1>Hello, {props.name}</h1>;\n}\nexport default Greeting;\n```\n\nTo use this component in another file, you can do:\n\n```javascript\nimport Greeting from './Greeting';\nfunction App() {\n return (\n <div>\n <Greeting name=\"John\" />\n </div>\n );\n}\nexport default App;\n```\n\n2. **Class Component**:\n\nYou can also define components as classes in React. These have some additional features compared to functional components:\n\n```javascript\nimport React, { Component } from 'react';\nclass Greeting extends Component {\n render() {\n return <h1>Hello, {this.props.name}</h1>;\n }\n}\nexport default Greeting;\n```\n\nAnd here's how to use this component:\n\n```javascript\nimport Greeting from './Greeting';\nclass App extends Component {\n render() {\n return (\n <div>\n <Greeting name=\"John\" />\n </div>\n );\n }\n}\nexport default App;\n```\n\nWith the advent of hooks in React, functional components can do everything that class components can do and hence, the community has been favoring functional components over class components.\n\nRemember to import React at the top of your file whenever you're creating a component, because JSX transpiles to `React.createElement` calls under the hood.";
return { input, output };
}
+67 -36
View File
@@ -1,7 +1,7 @@
// This file exports the prisma db connection, the Prisma Object, and the Typescript types.
// This is not imported in the index.ts file of this package, as we must not import this into FE code.
import { PrismaClient } from "@prisma/client";
import { Prisma, PrismaClient } from "@prisma/client";
import { env } from "process";
import kyselyExtension from "prisma-extension-kysely";
import {
@@ -11,45 +11,76 @@ import {
PostgresQueryCompiler,
} from "kysely";
import { DB } from ".";
import { logger } from "./server";
// Instantiated according to the Prisma documentation
// https://www.prisma.io/docs/orm/more/help-and-troubleshooting/help-articles/nextjs-prisma-client-dev-practices
export class PrismaClientSingleton {
private static instance: PrismaClient;
const prismaClientSingleton = () => {
return new PrismaClient({
log:
env.NODE_ENV === "development"
? ["query", "error", "warn"]
: ["error", "warn"],
});
};
public static getInstance(): PrismaClient {
if (PrismaClientSingleton.instance) {
return PrismaClientSingleton.instance;
}
const kyselySingleton = (prismaClient: PrismaClient) => {
return prismaClient.$extends(
kyselyExtension({
kysely: (driver) =>
new Kysely<DB>({
dialect: {
// This is where the magic happens!
createDriver: () => driver,
// Don't forget to customize these to match your database!
createAdapter: () => new PostgresAdapter(),
createIntrospector: (db) => new PostgresIntrospector(db),
createQueryCompiler: () => new PostgresQueryCompiler(),
},
}),
})
);
};
declare global {
// eslint-disable-next-line no-var
var prisma: undefined | ReturnType<typeof prismaClientSingleton>;
var kyselyPrisma: undefined | ReturnType<typeof kyselySingleton>;
const client = new PrismaClient<
Prisma.PrismaClientOptions,
"warn" | "error" | "query"
>({
log: [
{ emit: "event", level: "query" },
{ emit: "event", level: "error" },
{ emit: "event", level: "warn" },
],
});
if (env.NODE_ENV === "development") {
client.$on("query", (event) => {
logger.info(`prisma:query ${event.query}, ${event.duration}ms`);
});
}
client.$on("warn", (event) => {
logger.warn(`prisma:warn ${event.message}`);
});
client.$on("error", (event) => {
logger.error(`prisma:error ${event.message}`);
});
PrismaClientSingleton.instance = client;
return PrismaClientSingleton.instance;
}
}
export const prisma = globalThis.prisma ?? prismaClientSingleton();
export const kyselyPrisma = globalThis.kyselyPrisma ?? kyselySingleton(prisma);
export class KyselySingleton {
private static instance: { $kysely: Kysely<DB> };
public static getInstance() {
if (KyselySingleton.instance) {
return KyselySingleton.instance;
}
KyselySingleton.instance = PrismaClientSingleton.getInstance().$extends(
kyselyExtension({
kysely: (driver) =>
new Kysely<DB>({
dialect: {
// This is where the magic happens!
createDriver: () => driver,
// Don't forget to customize these to match your database!
createAdapter: () => new PostgresAdapter(),
createIntrospector: (db) => new PostgresIntrospector(db),
createQueryCompiler: () => new PostgresQueryCompiler(),
},
}),
}),
);
return KyselySingleton.instance;
}
}
export const prisma = PrismaClientSingleton.getInstance();
export const kyselyPrisma = KyselySingleton.getInstance();
export * from "@prisma/client";
if (process.env.NODE_ENV !== "production") globalThis.prisma = prisma;
+7 -1
View File
@@ -16,11 +16,12 @@ const EnvSchema = z.object({
.nullable(),
REDIS_AUTH: z.string().nullish(),
REDIS_CONNECTION_STRING: z.string().nullish(),
REDIS_ENABLE_AUTO_PIPELINING: z.enum(["true", "false"]).default("true"),
ENCRYPTION_KEY: z
.string()
.length(
64,
"ENCRYPTION_KEY must be 256 bits, 64 string characters in hex format, generate via: openssl rand -hex 32"
"ENCRYPTION_KEY must be 256 bits, 64 string characters in hex format, generate via: openssl rand -hex 32",
)
.optional(),
LANGFUSE_CACHE_PROMPT_ENABLED: z.enum(["true", "false"]).default("false"),
@@ -37,6 +38,11 @@ const EnvSchema = z.object({
.number()
.positive()
.default(60 * 10),
SALT: z.string().optional(), // used by components imported by web package
LANGFUSE_LOG_LEVEL: z
.enum(["trace", "debug", "info", "warn", "error", "fatal"])
.optional(),
LANGFUSE_LOG_FORMAT: z.enum(["text", "json"]).default("text"),
});
export const env = EnvSchema.parse(process.env);
@@ -19,6 +19,8 @@ export enum BatchExportFileFormat {
export enum BatchExportTableName {
Sessions = "sessions",
Traces = "traces",
Generations = "generations",
}
export const exportOptions: Record<
@@ -0,0 +1,22 @@
import { z } from "zod";
const COMMENT_OBJECT_TYPES = [
"TRACE",
"OBSERVATION",
"SESSION",
"PROMPT",
] as const;
export const CreateCommentData = z.object({
projectId: z.string(),
content: z.string().trim().min(1).max(500),
objectId: z.string(),
objectType: z.enum(COMMENT_OBJECT_TYPES),
});
export const DeleteCommentData = z.object({
projectId: z.string(),
commentId: z.string(),
objectId: z.string(),
objectType: z.enum(COMMENT_OBJECT_TYPES),
});
@@ -0,0 +1,17 @@
export const planLabels = {
oss: "OSS",
"cloud:hobby": "Hobby",
"cloud:pro": "Pro",
"cloud:team": "Team",
"self-hosted:enterprise": "Enterprise",
} as const;
export type Plan = keyof typeof planLabels;
export const plans = Object.keys(planLabels) as Plan[];
// This function is kept here to ensure consistency when updating plan names in the future.
export const isCloudPlan = (plan: Plan) => plan.startsWith("cloud");
export const isPlan = (value: string): value is Plan =>
plans.includes(value as Plan);
@@ -144,7 +144,7 @@ const ValidatedScoreConfigSchema = z
/**
* Use this function when pulling a list of score configs from the database before using in the application to ensure type safety.
* All score configs are expected to pass the validation. If a score fails validation, it will be logged to Sentry.
* All score configs are expected to pass the validation. If a score fails validation, it will be logged to Otel.
* @param scoreConfigs
* @returns list of validated score configs
*/
@@ -83,13 +83,13 @@ export const ScoreBodyWithoutConfig = z.discriminatedUnion("dataType", [
z.object({
value: z.number(),
dataType: z.literal("NUMERIC"),
})
}),
),
BaseScoreBody.merge(
z.object({
value: z.string(),
dataType: z.literal("CATEGORICAL"),
})
}),
),
BaseScoreBody.merge(
z.object({
@@ -97,7 +97,7 @@ export const ScoreBodyWithoutConfig = z.discriminatedUnion("dataType", [
message: "Value must be either 0 or 1",
}),
dataType: z.literal("BOOLEAN"),
})
}),
),
]);
@@ -155,13 +155,13 @@ export const ScorePropsAgainstConfig = z.union([
/**
* Use this function when pulling a list of scores from the database before using in the application to ensure type safety.
* All scores are expected to pass the validation. If a score fails validation, it will be logged to Sentry.
* All scores are expected to pass the validation. If a score fails validation, it will be logged to Otel.
* @param scores
* @returns list of validated scores
*/
export const filterAndValidateDbScoreList = (
scores: Score[],
onParseError?: (error: z.ZodError) => void
onParseError?: (error: z.ZodError) => void,
): APIScore[] =>
scores.reduce((acc, ts) => {
const result = APIScoreSchema.safeParse(ts);
@@ -198,14 +198,14 @@ export const PostScoresBody = z.discriminatedUnion("dataType", [
value: z.number(),
dataType: z.literal("NUMERIC"),
configId: z.string().nullish(),
})
}),
),
BaseScoreBody.merge(
z.object({
value: z.string(),
dataType: z.literal("CATEGORICAL"),
configId: z.string().nullish(),
})
}),
),
BaseScoreBody.merge(
z.object({
@@ -215,14 +215,14 @@ export const PostScoresBody = z.discriminatedUnion("dataType", [
}),
dataType: z.literal("BOOLEAN"),
configId: z.string().nullish(),
})
}),
),
BaseScoreBody.merge(
z.object({
value: z.union([z.string(), z.number()]),
dataType: z.undefined(),
configId: z.string().nullish(),
})
}),
),
]);
@@ -256,7 +256,7 @@ const LegacyGetScoreResponseDataV1 = z.intersection(
trace: z.object({
userId: z.string().nullish(),
}),
})
}),
);
export const GetScoresResponse = z.object({
data: z.array(LegacyGetScoreResponseDataV1),
@@ -265,7 +265,7 @@ export const GetScoresResponse = z.object({
export const legacyFilterAndValidateV1GetScoreList = (
scores: unknown[],
onParseError?: (error: z.ZodError) => void
onParseError?: (error: z.ZodError) => void,
): z.infer<typeof LegacyGetScoreResponseDataV1>[] =>
scores.reduce(
(acc: z.infer<typeof LegacyGetScoreResponseDataV1>[], ts) => {
@@ -278,7 +278,7 @@ export const legacyFilterAndValidateV1GetScoreList = (
}
return acc;
},
[] as z.infer<typeof LegacyGetScoreResponseDataV1>[]
[] as z.infer<typeof LegacyGetScoreResponseDataV1>[],
);
// GET /scores/{scoreId}
+8 -4
View File
@@ -1,18 +1,19 @@
export * from "./constants";
export * from "./queries";
export * from "./interfaces/filters";
export * from "./interfaces/orderBy";
export * from "./interfaces/cloudConfigSchema";
export * from "./interfaces/parseDbOrg";
export * from "./tableDefinitions";
export * from "./types";
export * from "./filterToPrisma";
export * from "./orderByToPrisma";
export * from "./tracesTable";
export * from "./server/auth/auth";
export * from "./server/auth/apiKeys";
export * from "./observationsTable";
export * from "./utils/zod";
export * from "./utils/json";
export * from "./utils/objects";
export * from "./utils/typeChecks";
export * from "./features/entitlements/plans";
export * from "./interfaces/rate-limits";
export { env } from "./env";
// llm api
@@ -30,6 +31,9 @@ export * from "./features/annotation/types";
export * from "./features/scores/scoreConfigTypes";
export * from "./features/scores/scoreTypes";
// comments
export * from "./features/comments/types";
// export db types only
export * from "@prisma/client";
export { type DB } from "../prisma/generated/types";
@@ -0,0 +1,22 @@
import { z } from "zod";
import { CloudConfigRateLimit } from "./rate-limits";
export const CloudConfigSchema = z.object({
plan: z.enum(["Hobby", "Pro", "Team", "Enterprise"]).optional(),
monthlyObservationLimit: z.number().int().positive().optional(),
// used for table and dashboard queries
defaultLookBackDays: z.number().int().positive().optional(),
// need to update stripe webhook if you change this, it fetches from db via these fields
stripe: z
.object({
customerId: z.string().optional(),
activeSubscriptionId: z.string().optional(),
activeProductId: z.string().optional(),
})
.optional(),
// custom rate limits for an organization
rateLimitOverrides: CloudConfigRateLimit.optional(),
});
export type CloudConfigSchema = z.infer<typeof CloudConfigSchema>;
@@ -1,5 +1,5 @@
import { CloudConfigSchema } from "@/src/features/organizations/utils/cloudConfigSchema";
import { type Organization } from "@langfuse/shared";
import { type Organization } from "@prisma/client";
import { CloudConfigSchema } from "./cloudConfigSchema";
type parsedOrg = Omit<Organization, "cloudConfig"> & {
cloudConfig: CloudConfigSchema | null;
@@ -0,0 +1,31 @@
import z from "zod";
import { ApiAccessScope } from "../server";
export const RateLimitResource = z.enum([
"ingestion",
"public-api",
"public-api-metrics",
"prompts",
"legacy-ingestion",
]);
// result of the rate limit check.
export type RateLimitResult = {
resource: z.infer<typeof RateLimitResource>;
points: number;
scope: ApiAccessScope;
// from rate-limiter-flexible
remainingPoints: number;
msBeforeNext: number;
consumedPoints: number;
isFirstInDuration: boolean;
};
export const RateLimitConfig = z.object({
points: z.number().nullish(),
durationInSec: z.number().nullish(),
resource: RateLimitResource,
});
export const CloudConfigRateLimit = z.array(RateLimitConfig);
+35 -2
View File
@@ -18,6 +18,7 @@ export const observationsTableCols: ColumnDefinition[] = [
type: "stringOptions",
internal: 'o."name"',
options: [], // to be added at runtime
nullable: true,
},
{ name: "Trace ID", id: "traceId", type: "string", internal: 't."id"' },
{
@@ -26,8 +27,15 @@ export const observationsTableCols: ColumnDefinition[] = [
type: "stringOptions",
internal: 't."name"',
options: [], // to be added at runtime
nullable: true,
},
{
name: "User ID",
id: "userId",
type: "string",
internal: 't."user_id"',
nullable: true,
},
{ name: "User ID", id: "userId", type: "string", internal: 't."user_id"' },
{
name: "Start Time",
id: "startTime",
@@ -44,7 +52,9 @@ export const observationsTableCols: ColumnDefinition[] = [
name: "Time To First Token (s)",
id: "timeToFirstToken",
type: "number",
internal: 'o."completion_start_time" - o."start_time"',
internal:
'EXTRACT(EPOCH FROM (o."completion_start_time" - o."start_time"))',
nullable: true,
},
{
name: "Latency (s)",
@@ -57,24 +67,28 @@ export const observationsTableCols: ColumnDefinition[] = [
id: "timePerOutputToken",
type: "number",
internal: '"latency" / o."completion_tokens"',
nullable: true,
},
{
name: "Input Cost ($)",
id: "inputCost",
type: "number",
internal: 'o."calculated_input_cost"',
nullable: true,
},
{
name: "Output Cost ($)",
id: "outputCost",
type: "number",
internal: 'o."calculated_output_cost"',
nullable: true,
},
{
name: "Total Cost ($)",
id: "totalCost",
type: "number",
internal: 'o."calculated_total_cost"',
nullable: true,
},
{
name: "Level",
@@ -88,6 +102,7 @@ export const observationsTableCols: ColumnDefinition[] = [
id: "statusMessage",
type: "string",
internal: 'o."status_message"',
nullable: true,
},
{
name: "Model",
@@ -95,24 +110,28 @@ export const observationsTableCols: ColumnDefinition[] = [
type: "stringOptions",
internal: 'o."model"',
options: [], // to be added at runtime
nullable: true,
},
{
name: "Input Tokens",
id: "inputTokens",
type: "number",
internal: 'o."prompt_tokens"',
nullable: true,
},
{
name: "Output Tokens",
id: "outputTokens",
type: "number",
internal: 'o."completion_tokens"',
nullable: true,
},
{
name: "Total Tokens",
id: "totalTokens",
type: "number",
internal: 'o."total_tokens"',
nullable: true,
},
{
name: "Usage",
@@ -137,6 +156,7 @@ export const observationsTableCols: ColumnDefinition[] = [
id: "version",
type: "string",
internal: 'o."version"',
nullable: true,
},
{
name: "Prompt Name",
@@ -144,12 +164,21 @@ export const observationsTableCols: ColumnDefinition[] = [
type: "stringOptions",
internal: "p.name",
options: [], // to be added at runtime
nullable: true,
},
{
name: "Prompt Version",
id: "promptVersion",
type: "number",
internal: "p.version",
nullable: true,
},
{
name: "Trace Tags",
id: "tags",
type: "arrayOptions",
internal: "t.tags",
options: [], // to be added at runtime
},
];
@@ -161,6 +190,7 @@ export type ObservationOptions = {
traceName: Array<OptionsDefinition>;
scores_avg: Array<string>;
promptName: Array<OptionsDefinition>;
tags: Array<OptionsDefinition>;
};
export function observationsTableColsWithOptions(
@@ -182,6 +212,9 @@ export function observationsTableColsWithOptions(
if (col.id === "promptName") {
return { ...col, options: options?.promptName ?? [] };
}
if (col.id === "tags") {
return { ...col, options: options?.tags ?? [] };
}
return col;
});
}
-1
View File
@@ -1 +0,0 @@
export { getSessionTableSQL } from "./getSessionTableSQL";
@@ -0,0 +1,81 @@
import { PrismaClient } from "@prisma/client";
import { compare, hash } from "bcryptjs";
import { randomUUID } from "crypto";
import * as crypto from "crypto";
import { env } from "../../env";
export function getDisplaySecretKey(secretKey: string) {
return secretKey.slice(0, 6) + "..." + secretKey.slice(-4);
}
export async function hashSecretKey(key: string) {
// legacy, uses bcrypt, transformed into hashed key upon first use
const hashedKey = await hash(key, 11);
return hashedKey;
}
async function generateKeySet() {
return {
pk: `pk-lf-${randomUUID()}`,
sk: `sk-lf-${randomUUID()}`,
};
}
export async function verifySecretKey(key: string, hashedKey: string) {
const isValid = await compare(key, hashedKey);
return isValid;
}
export function createShaHash(privateKey: string, salt: string): string {
const hash = crypto
.createHash("sha256")
.update(privateKey)
.update(crypto.createHash("sha256").update(salt, "utf8").digest("hex"))
.digest("hex");
return hash;
}
export async function createAndAddApiKeysToDb(p: {
prisma: PrismaClient;
projectId: string;
note?: string;
predefinedKeys?: {
secretKey: string;
publicKey: string;
};
}) {
const salt = env.SALT;
if (!salt) {
throw new Error("SALT is not set");
}
const { pk, sk } = p.predefinedKeys
? { pk: p.predefinedKeys.publicKey, sk: p.predefinedKeys.secretKey }
: await generateKeySet();
const hashedSk = await hashSecretKey(sk);
const displaySk = getDisplaySecretKey(sk);
const hashFromProvidedKey = createShaHash(sk, salt);
const apiKey = await p.prisma.apiKey.create({
data: {
projectId: p.projectId,
publicKey: pk,
hashedSecretKey: hashedSk,
displaySecretKey: displaySk,
fastHashedSecretKey: hashFromProvidedKey,
note: p.note,
},
});
return {
id: apiKey.id,
createdAt: apiKey.createdAt,
note: apiKey.note,
publicKey: apiKey.publicKey,
secretKey: sk,
displaySecretKey: displaySk,
};
}
-81
View File
@@ -1,81 +0,0 @@
import { compare, hash } from "bcryptjs";
import { randomUUID } from "crypto";
import * as crypto from "crypto";
import type { OAuthConfig, OAuthUserConfig } from "next-auth/providers/oauth";
export function generateSecretKey() {
return `sk-lf-${randomUUID()}`;
}
export function generatePublicKey() {
return `pk-lf-${randomUUID()}`;
}
export function getDisplaySecretKey(secretKey: string) {
return secretKey.slice(0, 6) + "..." + secretKey.slice(-4);
}
export async function hashSecretKey(key: string) {
// legacy, uses bcrypt, transformed into hashed key upon first use
const hashedKey = await hash(key, 11);
return hashedKey;
}
export async function generateKeySet() {
const pk = generatePublicKey();
const sk = generateSecretKey();
const hashedSk = await hashSecretKey(sk);
const displaySk = getDisplaySecretKey(sk);
return {
pk,
sk,
hashedSk,
displaySk,
};
}
export async function verifySecretKey(key: string, hashedKey: string) {
const isValid = await compare(key, hashedKey);
return isValid;
}
export function createShaHash(privateKey: string, salt: string): string {
const hash = crypto
.createHash("sha256")
.update(privateKey)
.update(crypto.createHash("sha256").update(salt, "utf8").digest("hex"))
.digest("hex");
return hash;
}
export interface CustomSSOUser extends Record<string, any> {
email: string;
id: string;
name: string;
verified: boolean;
}
export function CustomSSOProvider<P extends CustomSSOUser>(
options: OAuthUserConfig<P>
): OAuthConfig<P> {
return {
id: "custom",
name: "CustomSSOProvider",
type: "oauth",
wellKnown: `${options.issuer}/.well-known/openid-configuration`,
authorization: { params: { scope: "openid email profile" } }, // overridden by options.authorization to be able to set custom scopes, deep merged with this default
checks: ["pkce", "state"],
idToken: true,
profile(profile) {
return {
id: profile.sub,
name: profile.name,
email: profile.email,
image: null,
};
},
options,
};
}
@@ -0,0 +1,31 @@
import type { OAuthConfig, OAuthUserConfig } from "next-auth/providers/oauth";
interface CustomSSOUser extends Record<string, any> {
email: string;
id: string;
name: string;
verified: boolean;
}
export function CustomSSOProvider<P extends CustomSSOUser>(
options: OAuthUserConfig<P>
): OAuthConfig<P> {
return {
id: "custom",
name: "CustomSSOProvider",
type: "oauth",
wellKnown: `${options.issuer}/.well-known/openid-configuration`,
authorization: { params: { scope: "openid email profile" } }, // overridden by options.authorization to be able to set custom scopes, deep merged with this default
checks: ["pkce", "state"],
idToken: true,
profile(profile) {
return {
id: profile.sub,
name: profile.name,
email: profile.email,
image: null,
};
},
options,
};
}
+30
View File
@@ -1,3 +1,30 @@
import z from "zod";
import { Plan, plans } from "../../features/entitlements/plans";
import { CloudConfigRateLimit } from "../../interfaces/rate-limits";
export const OrgEnrichedApiKey = z.object({
id: z.string(),
note: z.string().nullable(),
publicKey: z.string(),
displaySecretKey: z.string(),
createdAt: z.string().datetime().nullable(),
lastUsedAt: z.string().datetime().nullable(),
expiresAt: z.string().datetime().nullable(),
projectId: z.string(),
fastHashedSecretKey: z.string(),
hashedSecretKey: z.string(),
orgId: z.string(),
plan: z.enum(plans as unknown as [string, ...string[]]),
rateLimitOverrides: CloudConfigRateLimit.nullish(),
});
export const API_KEY_NON_EXISTENT = "api-key-non-existent";
export const CachedApiKey = z.union([
OrgEnrichedApiKey,
z.literal(API_KEY_NON_EXISTENT),
]);
export type AuthHeaderVerificationResult =
| AuthHeaderValidVerificationResult
| {
@@ -13,4 +40,7 @@ export type AuthHeaderValidVerificationResult = {
export type ApiAccessScope = {
projectId: string;
accessLevel: "all" | "scores";
orgId: string;
plan: Plan;
rateLimitOverrides: z.infer<typeof CloudConfigRateLimit>;
};
@@ -1,7 +1,9 @@
import { Prisma } from "@prisma/client";
import { ColumnDefinition, type TableNames } from "./tableDefinitions";
import { FilterState } from "./types";
import { filterOperators } from "./interfaces/filters";
import { ColumnDefinition, type TableNames } from "../tableDefinitions";
import { FilterState } from "../types";
import { filterOperators, timeFilter } from "../interfaces/filters";
import { z } from "zod";
import { logger } from "./index";
const operatorReplacements = {
"any of": "IN",
@@ -50,7 +52,7 @@ export function tableColumnsToSqlFilter(
c.name === filter.column || c.id === filter.column
);
if (!col) {
console.error("Invalid filter column", filter.column);
logger.error("Invalid filter column", filter.column);
throw new Error("Invalid filter column: " + filter.column);
}
const colPrisma = Prisma.raw(col.internal);
@@ -179,3 +181,19 @@ export const datetimeFilterToPrismaSql = (
operator
)} ${value}::timestamp with time zone at time zone 'UTC'`;
};
export const datetimeFilterToPrisma = (
timestampFilter: z.infer<typeof timeFilter>
) => {
const prismaTimestampFilter =
timestampFilter.operator === ">="
? { gte: timestampFilter.value }
: timestampFilter.operator === ">"
? { gt: timestampFilter.value }
: timestampFilter.operator === "<="
? { lte: timestampFilter.value }
: timestampFilter.operator === "<"
? { lt: timestampFilter.value }
: {};
return prismaTimestampFilter;
};
+7 -1
View File
@@ -3,7 +3,8 @@ export * from "./services/email/organizationInvitation/sendMembershipInvitationE
export * from "./services/email/batchExportSuccess/sendBatchExportSuccessEmail";
export * from "./services/email/passwordReset/sendResetPasswordVerificationRequest";
export * from "./services/PromptService";
export * from "./auth/auth";
export * from "./auth/apiKeys";
export * from "./auth/customSsoProvider";
export * from "./llm/fetchLLMCompletion";
export * from "./llm/types";
export * from "./utils/DatabaseReadStream";
@@ -24,3 +25,8 @@ export * from "./auth/types";
export * from "./ingestion/legacy/index";
export * from "./queues";
export * from "./ingestion/legacy/EventProcessor";
export * from "./orderByToPrisma";
export * from "./filterToPrisma";
export * from "./instrumentation";
export * from "./logger";
export * from "./queries";
@@ -12,18 +12,20 @@ import {
sdkLogEvent,
} from "../types";
import { validateAndInflateScore } from "../validateAndInflateScore";
import { ApiAccessScope } from "../../auth/types";
import { Trace, Observation, Score, Prisma, Model } from "@prisma/client";
import { ForbiddenError, LangfuseNotFoundError } from "../../../errors";
import { mergeJson } from "../../../utils/json";
import { jsonSchema } from "../../../utils/zod";
import { prisma } from "../../../db";
import { LegacyIngestionAccessScope } from ".";
import { logger } from "../../logger";
import { ApiAccessScope } from "../../auth/types";
export interface EventProcessor {
auth(apiScope: ApiAccessScope): void;
auth(apiScope: LegacyIngestionAccessScope): void;
process(
apiScope: ApiAccessScope
apiScope: LegacyIngestionAccessScope
): Promise<Trace | Observation | Score> | undefined;
}
@@ -46,8 +48,8 @@ export class ObservationProcessor implements EventProcessor {
}
async convertToObservation(
apiScope: ApiAccessScope,
existingObservation: Observation | null
apiScope: LegacyIngestionAccessScope,
existingObservation: Omit<Observation, "input" | "output"> | null
): Promise<{
id: string;
create: Prisma.ObservationUncheckedCreateInput;
@@ -119,7 +121,8 @@ export class ObservationProcessor implements EventProcessor {
// Token counts
const [newInputCount, newOutputCount] =
"usage" in this.event.body
? this.calculateTokenCounts(
? await this.calculateTokenCounts(
apiScope.projectId,
this.event.body,
this.calculateTokenDelegate,
internalModel ?? undefined,
@@ -187,8 +190,9 @@ export class ObservationProcessor implements EventProcessor {
: undefined;
// Only null if promptName and promptVersion are set but prompt is not found
if (prompt === null)
console.warn("Prompt not found for observation", this.event.body);
if (prompt === null) {
logger.warn("Prompt not found for observation", this.event.body);
}
const observationId = this.event.body.id ?? v4();
@@ -309,7 +313,8 @@ export class ObservationProcessor implements EventProcessor {
};
}
calculateTokenCounts(
async calculateTokenCounts(
projectId: string,
body:
| z.infer<typeof legacyObservationCreateEvent>["body"]
| z.infer<typeof generationCreateEvent>["body"],
@@ -318,29 +323,59 @@ export class ObservationProcessor implements EventProcessor {
text: unknown;
}) => number | undefined,
model?: Model,
existingObservation?: Observation
existingObservation?: Omit<Observation, "input" | "output">
) {
const newPromptTokens =
body.usage?.input ??
((body.input || existingObservation?.input) && model && model.tokenizerId
? calculateTokenDelegate({
model: model,
text: body.input ?? existingObservation?.input,
})
: undefined);
let newPromptTokens = body.usage?.input;
if (newPromptTokens === undefined && model && model.tokenizerId) {
if (body.input) {
newPromptTokens = calculateTokenDelegate({
model: model,
text: body.input,
});
} else {
logger.info(
`No input provided, trying to calculate for id: ${existingObservation?.id}`
);
const observationInput = await prisma.observation.findFirst({
where: { id: existingObservation?.id, projectId: projectId },
select: {
input: true,
},
});
const newCompletionTokens =
body.usage?.output ??
((body.output || existingObservation?.output) &&
model &&
model.tokenizerId
? calculateTokenDelegate({
model: model,
text: body.output ?? existingObservation?.output,
})
: undefined);
newPromptTokens = calculateTokenDelegate({
model: model,
text: observationInput?.input,
});
}
}
return [newPromptTokens, newCompletionTokens];
let newCompletionTokens = body.usage?.output;
if (newCompletionTokens === undefined && model && model.tokenizerId) {
if (body.output) {
newCompletionTokens = calculateTokenDelegate({
model: model,
text: body.output,
});
} else {
logger.info(
`No output provided, trying to calculate for id: ${existingObservation?.id}`
);
const observationOutput = await prisma.observation.findFirst({
where: { id: existingObservation?.id, projectId: projectId },
select: {
output: true,
},
});
newCompletionTokens = calculateTokenDelegate({
model: model,
text: observationOutput?.output,
});
}
}
return [newPromptTokens ?? undefined, newCompletionTokens ?? undefined];
}
static calculateTokenCosts(
@@ -398,17 +433,53 @@ export class ObservationProcessor implements EventProcessor {
};
}
auth(apiScope: ApiAccessScope): void {
auth(apiScope: LegacyIngestionAccessScope): void {
if (apiScope.accessLevel !== "all")
throw new ForbiddenError("Access denied for observation creation");
}
async process(apiScope: ApiAccessScope): Promise<Observation> {
async process(apiScope: LegacyIngestionAccessScope): Promise<Observation> {
this.auth(apiScope);
const existingObservation = this.event.body.id
? await prisma.observation.findFirst({
where: { id: this.event.body.id },
select: {
// do not select I/O to spare our db
input: false,
output: false,
id: true,
traceId: true,
projectId: true,
type: true,
startTime: true,
endTime: true,
name: true,
metadata: true,
parentObservationId: true,
level: true,
statusMessage: true,
version: true,
createdAt: true,
updatedAt: true,
model: true,
internalModelId: true,
modelParameters: true,
promptTokens: true,
completionTokens: true,
totalTokens: true,
unit: true,
inputCost: true,
outputCost: true,
totalCost: true,
calculatedInputCost: true,
calculatedOutputCost: true,
calculatedTotalCost: true,
completionStartTime: true,
promptId: true,
internalModel: true,
},
where: { id: this.event.body.id, projectId: apiScope.projectId },
})
: null;
@@ -441,13 +512,13 @@ export class TraceProcessor implements EventProcessor {
this.event = event;
}
auth(apiScope: ApiAccessScope): void {
auth(apiScope: LegacyIngestionAccessScope): void {
if (apiScope.accessLevel !== "all")
throw new ForbiddenError("Access denied for trace creation");
}
async process(
apiScope: ApiAccessScope
apiScope: LegacyIngestionAccessScope
): Promise<Trace | Observation | Score> {
const { body } = this.event;
@@ -455,11 +526,8 @@ export class TraceProcessor implements EventProcessor {
const internalId = body.id ?? v4();
console.log(
"Trying to create trace, project ",
apiScope.projectId,
", id:",
internalId
logger.debug(
`Trying to create trace, project ${apiScope.projectId}, id: ${internalId}`
);
const existingTrace = await prisma.trace.findFirst({
@@ -553,7 +621,7 @@ export class ScoreProcessor implements EventProcessor {
this.event = event;
}
auth(apiScope: ApiAccessScope) {
auth(apiScope: LegacyIngestionAccessScope) {
if (apiScope.accessLevel !== "scores" && apiScope.accessLevel !== "all")
throw new ForbiddenError(
`Access denied for score creation, ${apiScope.accessLevel}`
@@ -561,7 +629,7 @@ export class ScoreProcessor implements EventProcessor {
}
async process(
apiScope: ApiAccessScope
apiScope: LegacyIngestionAccessScope
): Promise<Trace | Observation | Score> {
const { body } = this.event;
@@ -613,13 +681,13 @@ export class SdkLogProcessor implements EventProcessor {
this.event = event;
}
auth(apiScope: ApiAccessScope) {
auth(apiScope: LegacyIngestionAccessScope) {
return;
}
process() {
try {
console.log("SDK Log", this.event);
logger.info("SDK Log", this.event);
return undefined;
} catch (error) {
return undefined;
@@ -8,10 +8,11 @@ import { IngestionUtils } from "../IngestionUtils";
import { IngestionEventType } from "../types";
import { redis } from "../../redis/redis";
import { env } from "../../../env";
import { logger } from "../../logger";
export async function enqueueIngestionEvents(
projectId: string,
events: IngestionEventType[]
events: IngestionEventType[],
) {
const ingestionFlushQueue = getIngestionFlushQueue();
@@ -31,8 +32,8 @@ export async function enqueueIngestionEvents(
event,
redis,
ingestionFlushQueue,
batchTimestamp
)
batchTimestamp,
),
);
}
@@ -44,11 +45,11 @@ async function enqueueSingleIngestionEvent(
event: IngestionEventType,
redis: Redis,
ingestionFlushQueue: IngestionFlushQueue,
batchTimestamp: string
batchTimestamp: string,
): Promise<void> {
if (!("id" in event.body && event.body.id)) {
console.warn(
`Received ingestion event without id: ${JSON.stringify(event)}`
logger.warn(
`Received ingestion event without id: ${JSON.stringify(event)}`,
);
return;
@@ -14,11 +14,12 @@ import {
convertTraceUpsertEventsToRedisEvents,
getTraceUpsertQueue,
} from "../../redis/trace-upsert";
import { ApiAccessScope, AuthHeaderVerificationResult } from "../../auth/types";
import { ApiAccessScope } from "../../auth/types";
import { redis } from "../../redis/redis";
import { backOff } from "exponential-backoff";
import { Model } from "../../..";
import { enqueueIngestionEvents } from "./enqueueIngestionEvents";
import { logger } from "../../logger";
export type BatchResult = {
result: unknown;
@@ -31,12 +32,27 @@ type TokenCountInput = {
text: unknown;
};
export type LegacyIngestionAccessScope = Omit<
ApiAccessScope,
"orgId" | "plan" | "rateLimitOverrides"
>;
type LegacyIngestionAuthHeaderVerificationResult =
| {
validKey: true;
scope: LegacyIngestionAccessScope;
}
| {
validKey: false;
error: string;
};
export const handleBatch = async (
events: z.infer<typeof ingestionApiSchema>["batch"],
authCheck: AuthHeaderVerificationResult,
calculateTokenDelegate: (p: TokenCountInput) => number | undefined
authCheck: LegacyIngestionAuthHeaderVerificationResult,
calculateTokenDelegate: (p: TokenCountInput) => number | undefined,
) => {
console.log(`handling ingestion ${events.length} events`);
logger.debug(`handling ingestion ${events.length} events`);
if (!authCheck.validKey) throw new UnauthorizedError(authCheck.error);
@@ -54,7 +70,7 @@ export const handleBatch = async (
return await handleSingleEvent(
singleEvent,
authCheck.scope,
calculateTokenDelegate
calculateTokenDelegate,
);
});
results.push({
@@ -64,7 +80,7 @@ export const handleBatch = async (
}); // Push each result into the array
} catch (error) {
// Handle or log the error if `handleSingleEvent` fails
console.error("Error handling event:", error);
logger.error("Error handling event:", error);
// Decide how to handle the error: rethrow, continue, or push an error object to results
// For example, push an error object:
errors.push({
@@ -78,9 +94,9 @@ export const handleBatch = async (
if (env.CLICKHOUSE_URL) {
try {
await enqueueIngestionEvents(authCheck.scope.projectId, events);
console.log(`Added ${events.length} ingestion events to queue`);
logger.info(`Added ${events.length} ingestion events to queue`);
} catch (err) {
console.error("Error adding ingestion events to queue", err);
logger.error("Error adding ingestion events to queue", err);
}
}
@@ -92,10 +108,10 @@ async function retry<T>(request: () => Promise<T>): Promise<T> {
numOfAttempts: env.LANGFUSE_ASYNC_INGESTION_PROCESSING === "true" ? 5 : 3,
retry: (e: Error, attemptNumber: number) => {
if (e instanceof UnauthorizedError || e instanceof ForbiddenError) {
console.log("not retrying auth error");
logger.info("not retrying auth error");
return false;
}
console.log(`retrying processing events ${attemptNumber}`);
logger.info(`retrying processing events ${attemptNumber}`);
return true;
},
});
@@ -103,11 +119,11 @@ async function retry<T>(request: () => Promise<T>): Promise<T> {
const handleSingleEvent = async (
event: z.infer<typeof ingestionEvent>,
apiScope: ApiAccessScope,
apiScope: LegacyIngestionAccessScope,
calculateTokenDelegate: (p: {
model: Model;
text: unknown;
}) => number | undefined
}) => number | undefined,
) => {
const { body } = event;
let restEvent = body;
@@ -122,8 +138,8 @@ const handleSingleEvent = async (
restEvent = rest;
}
console.log(
`handling single event ${event.id} of type ${event.type}: ${JSON.stringify({ body: restEvent })}`
logger.info(
`handling single event ${event.id} of type ${event.type}: ${JSON.stringify({ body: restEvent })}`,
);
const cleanedEvent = ingestionEvent.parse(cleanEvent(event));
@@ -144,7 +160,7 @@ const handleSingleEvent = async (
case eventTypes.GENERATION_UPDATE:
processor = new ObservationProcessor(
cleanedEvent,
calculateTokenDelegate
calculateTokenDelegate,
);
break;
case eventTypes.SCORE_CREATE: {
@@ -186,7 +202,7 @@ export function cleanEvent(obj: unknown): unknown {
}
export const isNotNullOrUndefined = <T>(
val?: T | null
val?: T | null,
): val is Exclude<T, null | undefined> => !isUndefinedOrNull(val);
export const isUndefinedOrNull = <T>(val?: T | null): val is undefined | null =>
@@ -194,7 +210,7 @@ export const isUndefinedOrNull = <T>(val?: T | null): val is undefined | null =>
export const sendToWorkerIfEnvironmentConfigured = async (
batchResults: BatchResult[],
projectId: string
projectId: string,
): Promise<void> => {
const traceEvents: TraceUpsertEventType[] = batchResults
.filter((result) => result.type === eventTypes.TRACE_CREATE) // we only have create, no update.
@@ -204,17 +220,17 @@ export const sendToWorkerIfEnvironmentConfigured = async (
"id" in result.result
? // ingestion API only gets traces for one projectId
{ traceId: result.result.id as string, projectId }
: null
: null,
)
.filter(isNotNullOrUndefined);
try {
if (env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION && redis) {
console.log(`Sending ${traceEvents.length} events to worker via Redis`);
logger.info(`Sending ${traceEvents.length} events to worker via Redis`);
const queue = getTraceUpsertQueue();
if (!queue) {
console.error("TraceUpsertQueue not initialized");
logger.error("TraceUpsertQueue not initialized");
return;
}
@@ -224,7 +240,7 @@ export const sendToWorkerIfEnvironmentConfigured = async (
env.LANGFUSE_WORKER_PASSWORD &&
env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION
) {
console.log(`Sending ${traceEvents.length} events to worker via HTTP`);
logger.info(`Sending ${traceEvents.length} events to worker via HTTP`);
const body: EventBodyType = {
name: EventName.TraceUpsert,
payload: traceEvents,
@@ -238,7 +254,7 @@ export const sendToWorkerIfEnvironmentConfigured = async (
Authorization:
"Basic " +
Buffer.from(
"admin" + ":" + env.LANGFUSE_WORKER_PASSWORD
"admin" + ":" + env.LANGFUSE_WORKER_PASSWORD,
).toString("base64"),
},
body: JSON.stringify(body),
@@ -247,6 +263,6 @@ export const sendToWorkerIfEnvironmentConfigured = async (
}
}
} catch (error) {
console.error("Error sending events to worker", error);
logger.error("Error sending events to worker", error);
}
};
@@ -8,7 +8,7 @@ export async function findModel(p: {
unit?: string;
startTime?: Date;
};
existingDbObservation?: Observation;
existingDbObservation?: Omit<Observation, "input" | "output">;
}): Promise<Model | null> {
const { event, existingDbObservation } = p;
// either get the model from the existing observation
@@ -15,7 +15,7 @@ type ValidateAndInflateScoreParams = {
};
export async function validateAndInflateScore(
params: ValidateAndInflateScoreParams
params: ValidateAndInflateScoreParams,
): Promise<Score> {
const { body, projectId } = params;
@@ -29,7 +29,7 @@ export async function validateAndInflateScore(
if (!config || !validateDbScoreConfigSafe(config).success)
throw new LangfuseNotFoundError(
"The configId you provided does not match a valid config in this project"
"The configId you provided does not match a valid config in this project",
);
validateConfigAgainstBody(body, config as ValidatedScoreConfig);
@@ -47,7 +47,7 @@ export async function validateAndInflateScore(
if (!validation.success) {
throw new InvalidRequestError(
`Ingested score value type not valid against provided data type. Provide numeric values for numeric and boolean scores, and string values for categorical scores.`
`Ingested score value type not valid against provided data type. Provide numeric values for numeric and boolean scores, and string values for categorical scores.`,
);
}
@@ -62,7 +62,7 @@ function inferDataType(value: string | number): ScoreDataType {
function mapStringValueToNumericValue(
config: ValidatedScoreConfig,
label: string
label: string,
): number | null {
return (
config.categories?.find((category) => category.label === label)?.value ??
@@ -71,7 +71,7 @@ function mapStringValueToNumericValue(
}
function inflateScoreBody(
params: ValidateAndInflateScoreParams & { config?: ValidatedScoreConfig }
params: ValidateAndInflateScoreParams & { config?: ValidatedScoreConfig },
): Score {
const { body, projectId, scoreId, config } = params;
@@ -105,25 +105,25 @@ function inflateScoreBody(
function validateConfigAgainstBody(
body: any,
config: ValidatedScoreConfig
config: ValidatedScoreConfig,
): void {
const { maxValue, minValue, categories, dataType: configDataType } = config;
if (body.dataType && body.dataType !== configDataType) {
throw new InvalidRequestError(
`Data type mismatch based on config: expected ${configDataType}, got ${body.dataType}`
`Data type mismatch based on config: expected ${configDataType}, got ${body.dataType}`,
);
}
if (config.isArchived) {
throw new InvalidRequestError(
"Config is archived and cannot be used to create new scores. Please restore the config first."
"Config is archived and cannot be used to create new scores. Please restore the config first.",
);
}
if (config.name !== body.name) {
throw new InvalidRequestError(
`Name mismatch based on config: expected ${config.name}, got ${body.name}`
`Name mismatch based on config: expected ${config.name}, got ${body.name}`,
);
}
@@ -136,7 +136,7 @@ function validateConfigAgainstBody(
if (!dataTypeValidation.success) {
throw new InvalidRequestError(
`Ingested score body not valid against provided config data type.`
`Ingested score body not valid against provided config data type.`,
);
}
@@ -154,7 +154,7 @@ function validateConfigAgainstBody(
.join(", ");
throw new InvalidRequestError(
`Ingested score body not valid against provided config: ${errorDetails}`
`Ingested score body not valid against provided config: ${errorDetails}`,
);
}
}
@@ -0,0 +1,24 @@
# Instrumentation at Langfuse
Throughout our applications we want to use as much Otel as possible. This helps us to be flexible choosing our observability backend, and we will benefit from features and packages built by the Otel community.
## How to use Otel in your application
- Use the `instrument` or `instrument` functions to wrap your functions with Otel instrumentation. This will automatically create spans for your functions and send them to the Otel collector. If an instrumented function throws, exceptions will be added to the span and the span will be marked as failed.
- Use `recordGauge`, `recordCounter`, `recordHistogram` to record metrics. These will be sent to the Otel collector.
## Configuration options
- `web` and `worker` have an instrumentation.ts file, which configures otel for the application.
- For trpc, we use `@baselime/trpc-opentelemetry-middleware` to enrich spans with trpc inputs and outputs.
- When building adding new infrastructure, we should search for auto instumentations for our code base.
## Library support
- `dd-trace` has a direct dependency of `@opentelemetry/api` version < `1.9` [GH](https://github.com/DataDog/dd-trace-js/blob/ed9b0b30f7b0283579a9bf8c18e1f9deab18fecf/package.json#L81).
- `@prisma/instrumentation` requires at least `@opentelemetry/api` version `1.8` [GH](https://github.com/prisma/prisma/blob/d780290b13754420abcfa5d7592f02049c6cc005/packages/instrumentation/package.json#L25)
- We need to ensure that all other libraries we use are compatible with `@opentelemetry/api` version `1.8`. If we have a package using a later version, this will break the instrumentation and crash the container. We can check the versions with the following command:
```bash
pnpm --filter=web why @opentelemetry/api
```
@@ -0,0 +1,194 @@
import * as opentelemetry from "@opentelemetry/api";
import * as dd from "dd-trace";
// type CallbackFn<T> = () => T;
export type TCarrier = {
traceparent?: string;
tracestate?: string;
};
export type SpanCtx = {
name: string;
spanKind?: opentelemetry.SpanKind; // https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/api.md#spankind
rootSpan?: boolean; // https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/overview.md#traces
traceScope?: string;
traceContext?: TCarrier;
};
type AsyncCallbackFn<T> = (span: opentelemetry.Span) => Promise<T>;
export async function instrumentAsync<T>(
ctx: SpanCtx,
callback: AsyncCallbackFn<T>,
): Promise<T> {
const activeContext = ctx.traceContext
? opentelemetry.propagation.extract(
opentelemetry.context.active(),
ctx.traceContext,
)
: opentelemetry.context.active();
return getTracer(ctx.traceScope ?? callback.name).startActiveSpan(
ctx.name,
{
root: !Boolean(ctx.traceContext) && ctx.rootSpan,
kind: ctx.spanKind,
},
activeContext,
async (span) => {
try {
const result = await callback(span);
span.end();
return result;
} catch (ex) {
traceException(ex as opentelemetry.Exception, span);
span.end();
throw ex;
}
},
);
}
type SyncCallbackFn<T> = (span: opentelemetry.Span) => T;
export function instrumentSync<T>(
ctx: SpanCtx,
callback: SyncCallbackFn<T>,
): T {
const activeContext = ctx.traceContext
? opentelemetry.propagation.extract(
opentelemetry.context.active(),
ctx.traceContext,
)
: opentelemetry.context.active();
return getTracer(ctx.traceScope ?? callback.name).startActiveSpan(
ctx.name,
{
root: !Boolean(ctx.traceContext) && ctx.rootSpan,
kind: ctx.spanKind,
},
activeContext,
(span) => {
try {
const result = callback(span);
span.end();
return result;
} catch (ex) {
traceException(ex as opentelemetry.Exception, span);
span.end();
throw ex;
}
},
);
}
export const getCurrentSpan = () => opentelemetry.trace.getActiveSpan();
export const addTraceContext = <T extends Record<string, any>>(
input: T,
): T & { _tracecontext?: TCarrier } => {
const context = {};
opentelemetry.propagation.inject(opentelemetry.context.active(), context);
return {
...input,
_tracecontext: context,
};
};
export const traceException = (
ex: unknown,
span?: opentelemetry.Span,
code?: string,
) => {
const activeSpan = span ?? getCurrentSpan();
if (!activeSpan) {
return;
}
const exception = {
code: code,
message:
ex instanceof Error
? ex.message
: typeof ex === "object" && ex !== null && "message" in ex
? JSON.stringify(ex.message)
: JSON.stringify(ex),
name:
ex instanceof Error
? ex.name
: typeof ex === "object" && ex !== null && "name" in ex
? JSON.stringify(ex.name)
: "Error",
stack:
ex instanceof Error
? JSON.stringify(ex.stack)
: typeof ex === "object" && ex !== null && "stack" in ex
? JSON.stringify(ex.stack)
: undefined,
};
// adds an otel event
activeSpan.recordException(exception);
//adds tags for datadog error tracking
activeSpan.setAttributes({
"error.stack": exception.stack,
"error.message": exception.message,
"error.type": exception.name,
});
activeSpan.setStatus({
code: opentelemetry.SpanStatusCode.ERROR,
message: exception.message,
});
};
export const addUserToSpan = (
attributes: { userId?: string; projectId?: string; email?: string },
span?: opentelemetry.Span,
) => {
const activeSpan = span ?? getCurrentSpan();
if (!activeSpan) {
return;
}
attributes.userId && activeSpan.setAttribute("user.id", attributes.userId);
attributes.email && activeSpan.setAttribute("user.email", attributes.email);
attributes.projectId &&
activeSpan.setAttribute("project.id", attributes.projectId);
};
export const getTracer = (name: string) => opentelemetry.trace.getTracer(name);
export const recordGauge = (
stat: string,
value?: number | undefined,
tags?:
| {
[tag: string]: string | number;
}
| undefined,
) => {
dd.dogstatsd.gauge(stat, value, tags);
};
export const recordIncrement = (
stat: string,
value?: number | undefined,
tags?: { [tag: string]: string | number } | undefined,
) => {
dd.dogstatsd.increment(stat, value, tags);
};
export const recordHistogram = (
stat: string,
value?: number | undefined,
tags?: { [tag: string]: string | number } | undefined,
) => {
dd.dogstatsd.histogram(stat, value, tags);
};
@@ -1,3 +1,5 @@
import { type ZodSchema } from "zod";
import { ChatAnthropic } from "@langchain/anthropic";
import {
AIMessage,
@@ -12,20 +14,12 @@ import {
import { IterableReadableStream } from "@langchain/core/utils/stream";
import { ChatOpenAI } from "@langchain/openai";
import {
ChatMessage,
ChatMessageRole,
LLMFunctionCall,
ModelParams,
LLMAdapter,
} from "./types";
import zodToJsonSchema from "zod-to-json-schema";
import { JsonOutputFunctionsParser } from "langchain/output_parsers";
import { ChatMessage, ChatMessageRole, ModelParams, LLMAdapter } from "./types";
type LLMCompletionParams = {
messages: ChatMessage[];
modelParams: ModelParams;
functionCall?: LLMFunctionCall;
structuredOutputSchema?: ZodSchema;
callbacks?: BaseCallbackHandler[];
baseURL?: string;
apiKey?: string;
@@ -51,7 +45,7 @@ export async function fetchLLMCompletion(
export async function fetchLLMCompletion(
params: LLMCompletionParams & {
streaming: false;
functionCall: LLMFunctionCall;
structuredOutputSchema: ZodSchema;
}
): Promise<unknown>;
@@ -121,18 +115,41 @@ export async function fetchLLMCompletion(
throw new Error("This model provider is not supported.");
}
if (params.functionCall) {
const functionCallingModel = chatModel.bind({
functions: [
{
...params.functionCall,
parameters: zodToJsonSchema(params.functionCall.parameters),
},
],
function_call: { name: params.functionCall.name },
});
const outputParser = new JsonOutputFunctionsParser();
return await functionCallingModel.pipe(outputParser).invoke(finalMessages);
if (params.structuredOutputSchema) {
return await (chatModel as ChatOpenAI) // Typecast necessary due to https://github.com/langchain-ai/langchainjs/issues/6795
.withStructuredOutput(params.structuredOutputSchema)
.invoke(finalMessages);
}
/*
Workaround OpenAI o1 while in beta:
This is a temporary workaround to avoid sending system messages to OpenAI's O1 models.
O1 models do not support in beta:
- system messages
- top_p
- max_tokens at all, one has to use max_completion_tokens instead
- temperature different than 1
Reference: https://platform.openai.com/docs/guides/reasoning/beta-limitations
*/
if (modelParams.model.startsWith("o1-")) {
return await new ChatOpenAI({
openAIApiKey: apiKey,
modelName: modelParams.model,
temperature: 1,
maxTokens: undefined,
topP: undefined,
callbacks,
maxRetries,
configuration: {
baseURL,
},
})
.pipe(new StringOutputParser())
.invoke(
finalMessages.filter((message) => message._getType() !== "system")
);
}
if (streaming) {
+7 -1
View File
@@ -4,7 +4,7 @@ import z from "zod";
export type PromptVariable = { name: string; value: string; isUsed: boolean };
export type ChatMessage = {
role: ChatMessageRole;
role: ChatMessageRole | string; // Users may ingest any string as role via API/SDK
content: string;
};
@@ -22,6 +22,8 @@ export enum ChatMessageRole {
Assistant = "assistant",
}
export const ChatMessageDefaultRoleSchema = z.nativeEnum(ChatMessageRole);
export type ModelParams = {
provider: string;
adapter: LLMAdapter;
@@ -52,6 +54,10 @@ export const openAIModels = [
"gpt-4o-2024-05-13",
"gpt-4o-mini",
"gpt-4o-mini-2024-07-18",
"o1-preview",
"o1-preview-2024-09-12",
"o1-mini",
"o1-mini-2024-09-12",
"gpt-4-turbo-preview",
"gpt-4-1106-preview",
"gpt-4-0613",
+58
View File
@@ -0,0 +1,58 @@
import { env } from "../env";
import winston from "winston";
import Transport from "winston-transport";
import { getCurrentSpan } from "./instrumentation";
class TracedTransport extends Transport {
constructor(opts: Transport.TransportStreamOptions = {}) {
super(opts);
}
log(info: Record<string, any>, callback: () => void): void {
setImmediate(() => {
this.emit("logged", info);
});
const currentSpan = getCurrentSpan();
info.trace_id = currentSpan?.spanContext().traceId;
info.span_id = currentSpan?.spanContext().spanId;
console.log(JSON.stringify(info));
callback();
}
}
const getWinstonLogger = (
nodeEnv: "development" | "production" | "test",
minLevel = "info",
) => {
const textLoggerFormat = winston.format.combine(
winston.format.errors({ stack: true }),
winston.format.timestamp(),
winston.format.align(),
winston.format.printf((info) => {
const logMessage = `${info.timestamp} ${info.level} ${info.message}`;
return info.stack ? `${logMessage}\n${info.stack}` : logMessage;
}),
);
const jsonLoggerFormat = winston.format.combine(
winston.format.errors({ stack: true }),
winston.format.timestamp(),
winston.format.json(),
);
const format =
env.LANGFUSE_LOG_FORMAT === "text" ? textLoggerFormat : jsonLoggerFormat;
const transport =
env.LANGFUSE_LOG_FORMAT === "text"
? new winston.transports.Console()
: new TracedTransport();
return winston.createLogger({
level: minLevel,
format: format,
transports: [transport],
});
};
export const logger = getWinstonLogger(env.NODE_ENV, env.LANGFUSE_LOG_LEVEL);
@@ -2,8 +2,9 @@ import { z } from "zod";
import { Prisma } from "@prisma/client";
import type { ColumnDefinition } from "./tableDefinitions/types";
import type { OrderByState } from "./interfaces/orderBy";
import type { ColumnDefinition } from "../tableDefinitions/types";
import type { OrderByState } from "../interfaces/orderBy";
import { logger } from "./logger";
/**
* Convert orderBy to SQL ORDER BY clause
@@ -13,20 +14,20 @@ import type { OrderByState } from "./interfaces/orderBy";
*/
export function orderByToPrismaSql(
orderBy: OrderByState,
tableColumns: ColumnDefinition[]
tableColumns: ColumnDefinition[],
): Prisma.Sql {
if (!orderBy) {
return Prisma.sql`ORDER BY t.timestamp DESC NULLS LAST`;
return Prisma.sql`ORDER BY t.timestamp DESC`;
}
// Get column definition to map column to internal name, e.g. "t.id"
const col = tableColumns.find(
// TODO: Only use id instead of name.
// It's less error-prone & decouples data fetching from the human-readable UI labels
(c) => c.name === orderBy.column || c.id === orderBy.column
(c) => c.name === orderBy.column || c.id === orderBy.column,
);
if (!col) {
console.log("Invalid filter column", orderBy.column);
logger.warn("Invalid filter column", orderBy.column);
throw new Error("Invalid filter column: " + orderBy.column);
}
@@ -34,12 +35,12 @@ export function orderByToPrismaSql(
const orderByOrder = z.enum(["ASC", "DESC"]);
const order = orderByOrder.safeParse(orderBy.order);
if (!order.success) {
console.log("Invalid order", orderBy.order);
logger.warn("Invalid order", orderBy.order);
throw new Error("Invalid order: " + orderBy.order);
}
// Both column and order are safe, can use raw SQL
return Prisma.raw(
`ORDER BY ${col.internal} ${order.data} ${orderBy.order === "DESC" ? "NULLS LAST" : "NULLS FIRST"}`
`ORDER BY ${col.internal} ${order.data} ${col.nullable ? (orderBy.order === "DESC" ? "NULLS LAST" : "NULLS FIRST") : ""}`,
);
}
@@ -0,0 +1,172 @@
import { ObservationView, Prisma } from "@prisma/client";
import {
datetimeFilterToPrismaSql,
tableColumnsToSqlFilterAndPrefix,
} from "../filterToPrisma";
import { orderByToPrismaSql } from "../orderByToPrisma";
import { observationsTableCols } from "../../observationsTable";
import { TableFilters } from "./types";
type AdditionalObservationFields = {
traceName: string | null;
promptName: string | null;
promptVersion: string | null;
traceTags: Array<string>;
};
type FullObservation = AdditionalObservationFields & ObservationView;
export type FullObservations = Array<FullObservation>;
export type FullObservationsWithScores = Array<
FullObservation & { scores?: Record<string, string[] | number[]> | null }
>;
export type IOAndMetadataOmittedObservations = Array<
Omit<ObservationView, "input" | "output" | "metadata"> &
AdditionalObservationFields
>;
export function parseGetAllGenerationsInput(filters: TableFilters) {
const searchCondition = filters.searchQuery
? Prisma.sql`AND (
o."id" ILIKE ${`%${filters.searchQuery}%`} OR
o."name" ILIKE ${`%${filters.searchQuery}%`} OR
o."model" ILIKE ${`%${filters.searchQuery}%`} OR
t."name" ILIKE ${`%${filters.searchQuery}%`}
)`
: Prisma.empty;
const filterCondition = tableColumnsToSqlFilterAndPrefix(
filters.filter ?? [],
observationsTableCols,
"observations"
);
const orderByCondition = orderByToPrismaSql(
filters.orderBy,
observationsTableCols
);
// to improve query performance, add timeseries filter to observation queries as well
const startTimeFilter = filters.filter?.find(
(f) => f.column === "Start Time" && f.type === "datetime"
);
const datetimeFilter =
startTimeFilter && startTimeFilter.type === "datetime"
? datetimeFilterToPrismaSql(
"start_time",
startTimeFilter.operator,
startTimeFilter.value
)
: Prisma.empty;
return {
searchCondition,
filterCondition,
orderByCondition,
datetimeFilter,
};
}
export function createGenerationsQuery({
projectId,
datetimeFilter = Prisma.empty,
page,
limit,
searchCondition = Prisma.empty,
filterCondition = Prisma.empty,
orderByCondition = Prisma.empty,
selectIOAndMetadata = false,
selectScoreValues = false,
}: {
projectId: string;
datetimeFilter?: Prisma.Sql;
page?: number;
limit?: number;
searchCondition?: Prisma.Sql;
filterCondition?: Prisma.Sql;
orderByCondition?: Prisma.Sql;
selectIOAndMetadata?: boolean;
selectScoreValues?: boolean;
}) {
return Prisma.sql`
WITH scores_avg AS (
SELECT
trace_id,
observation_id,
${selectScoreValues ? Prisma.sql`jsonb_object_agg(name::text, "values") AS "scores_values",` : Prisma.empty}
jsonb_object_agg(name::text, avg_value::double precision) AS "scores_avg"
FROM (
SELECT
trace_id,
observation_id,
name,
${selectScoreValues ? Prisma.sql`array_agg(COALESCE(string_value, value::text)) AS "values",` : Prisma.empty}
avg(value) avg_value,
comment
FROM
scores
WHERE
project_id = ${projectId}
${selectScoreValues ? Prisma.empty : Prisma.sql`AND scores."data_type" IN ('NUMERIC', 'BOOLEAN')`}
GROUP BY
trace_id,
observation_id,
name,
comment
ORDER BY
trace_id
) tmp
GROUP BY
trace_id,
observation_id
)
SELECT
${selectScoreValues ? Prisma.sql`s_avg."scores_values" AS "scores",` : Prisma.empty}
o.id,
o.name,
o.model,
o."modelParameters",
o.start_time as "startTime",
o.end_time as "endTime",
${selectIOAndMetadata ? Prisma.sql`o.input, o.output, o.metadata,` : Prisma.empty}
o.trace_id as "traceId",
t.name as "traceName",
o.completion_start_time as "completionStartTime",
o.time_to_first_token as "timeToFirstToken",
o.prompt_tokens as "promptTokens",
o.completion_tokens as "completionTokens",
o.total_tokens as "totalTokens",
o.unit,
o.level,
o.status_message as "statusMessage",
o.version,
o.model_id as "modelId",
o.input_price as "inputPrice",
o.output_price as "outputPrice",
o.total_price as "totalPrice",
o.calculated_input_cost as "calculatedInputCost",
o.calculated_output_cost as "calculatedOutputCost",
o.calculated_total_cost as "calculatedTotalCost",
o."latency",
o.prompt_id as "promptId",
p.name as "promptName",
p.version as "promptVersion",
t.tags as "traceTags"
FROM observations_view o
JOIN traces t ON t.id = o.trace_id AND t.project_id = ${projectId}
LEFT JOIN scores_avg AS s_avg ON s_avg.trace_id = t.id and s_avg.observation_id = o.id
LEFT JOIN prompts p ON p.id = o.prompt_id AND p.project_id = ${projectId}
WHERE
o.project_id = ${projectId}
AND o.type = 'GENERATION'
${datetimeFilter}
${searchCondition}
${filterCondition}
${orderByCondition}
${limit ? Prisma.sql`LIMIT ${limit}` : Prisma.empty}
${page && limit ? Prisma.sql`OFFSET ${page * limit}` : Prisma.empty}
`;
}
@@ -2,10 +2,10 @@ import { z } from "zod";
import { Prisma } from "@prisma/client";
import { tableColumnsToSqlFilterAndPrefix } from "../filterToPrisma";
import { singleFilter } from "../interfaces/filters";
import { orderBy } from "../interfaces/orderBy";
import { singleFilter } from "../../interfaces/filters";
import { orderBy } from "../../interfaces/orderBy";
import { orderByToPrismaSql } from "../orderByToPrisma";
import { sessionsViewCols } from "../tableDefinitions/index";
import { sessionsViewCols } from "../../tableDefinitions";
const GetSessionTableSQLParamsSchema = z.object({
projectId: z.string(),
@@ -16,8 +16,13 @@ const GetSessionTableSQLParamsSchema = z.object({
});
type GetSessionTableSQLParams = z.infer<typeof GetSessionTableSQLParamsSchema>;
export const getSessionTableSQL = (
params: GetSessionTableSQLParams
export const createSessionsAllQuery = (
select: Prisma.Sql,
params: GetSessionTableSQLParams,
options?: {
ignoreOrderBy?: boolean; // used by session.metrics and session.all.totalCount
sessionIdList?: string[]; // used by session.metrics
},
): Prisma.Sql => {
const { projectId, filter, orderBy, page, limit } =
GetSessionTableSQLParamsSchema.parse(params);
@@ -25,26 +30,17 @@ export const getSessionTableSQL = (
const filterCondition = tableColumnsToSqlFilterAndPrefix(
filter ?? [],
sessionsViewCols,
"sessions"
"sessions",
);
const orderByCondition = orderByToPrismaSql(orderBy, sessionsViewCols);
const sessionIdFilter = options?.sessionIdList
? Prisma.sql`AND s.id IN (${Prisma.join(options?.sessionIdList)})`
: Prisma.sql``;
const sql = Prisma.sql`
SELECT
s.id,
s. "created_at" AS "createdAt",
s.bookmarked,
s.public,
t. "userIds",
t. "countTraces",
o. "sessionDuration",
o. "totalCost" AS "totalCost",
o. "inputCost" AS "inputCost",
o. "outputCost" AS "outputCost",
o. "promptTokens" AS "promptTokens",
o. "completionTokens" AS "completionTokens",
o. "totalTokens" AS "totalTokens",
(count(*) OVER ())::int AS "totalCount"
${select}
FROM
trace_sessions AS s
LEFT JOIN LATERAL (
@@ -54,9 +50,12 @@ export const getSessionTableSQL = (
MIN(t. "timestamp") AS "min_timestamp",
array_agg(t.id) AS "traceIds",
array_agg(DISTINCT t.user_id) AS "userIds",
count(t.id)::int AS "countTraces"
count(t.id)::int AS "countTraces",
array_agg(DISTINCT u.tag) AS "tags"
FROM
traces t
LEFT JOIN LATERAL (
SELECT DISTINCT UNNEST(t.tags) AS tag) AS u ON TRUE
WHERE
t.project_id = ${projectId}
AND t.session_id = s.id
@@ -79,7 +78,8 @@ export const getSessionTableSQL = (
WHERE
s. "project_id" = ${projectId}
${filterCondition}
${orderByCondition}
${sessionIdFilter}
${options?.ignoreOrderBy ? Prisma.sql`` : orderByCondition}
LIMIT ${limit}
OFFSET ${page * limit}
`;
@@ -0,0 +1,135 @@
import { Prisma } from "@prisma/client";
import { TableFilters } from "./types";
import {
datetimeFilterToPrismaSql,
tableColumnsToSqlFilterAndPrefix,
} from "../filterToPrisma";
import { tracesTableCols } from "../../tracesTable";
import { orderByToPrismaSql } from "../orderByToPrisma";
export function parseTraceAllFilters(input: TableFilters) {
const filterCondition = tableColumnsToSqlFilterAndPrefix(
input.filter ?? [],
tracesTableCols,
"traces"
);
const orderByCondition = orderByToPrismaSql(input.orderBy, tracesTableCols);
// to improve query performance, add timeseries filter to observation queries as well
const timeseriesFilter = input.filter?.find(
(f) => f.column === "Timestamp" && f.type === "datetime"
);
const observationTimeseriesFilter =
timeseriesFilter && timeseriesFilter.type === "datetime"
? datetimeFilterToPrismaSql(
"start_time",
timeseriesFilter.operator,
timeseriesFilter.value
)
: Prisma.empty;
const searchCondition = input.searchQuery
? Prisma.sql`AND (
t."id" ILIKE ${`%${input.searchQuery}%`} OR
t."external_id" ILIKE ${`%${input.searchQuery}%`} OR
t."user_id" ILIKE ${`%${input.searchQuery}%`} OR
t."name" ILIKE ${`%${input.searchQuery}%`}
)`
: Prisma.empty;
return {
filterCondition,
orderByCondition,
observationTimeseriesFilter,
searchCondition,
};
}
export function createTracesQuery({
select,
projectId,
observationTimeseriesFilter = Prisma.empty,
page,
limit,
searchCondition = Prisma.empty,
filterCondition = Prisma.empty,
orderByCondition = Prisma.empty,
selectScoreValues = false,
}: {
select: Prisma.Sql;
projectId: string;
observationTimeseriesFilter?: Prisma.Sql;
page?: number;
limit?: number;
searchCondition?: Prisma.Sql;
filterCondition?: Prisma.Sql;
orderByCondition?: Prisma.Sql;
selectScoreValues?: boolean;
}) {
return Prisma.sql`
SELECT
${select}
FROM
"traces" AS t
LEFT JOIN LATERAL (
SELECT
SUM(prompt_tokens) AS "promptTokens",
SUM(completion_tokens) AS "completionTokens",
SUM(total_tokens) AS "totalTokens",
SUM(calculated_total_cost) AS "calculatedTotalCost",
SUM(calculated_input_cost) AS "calculatedInputCost",
SUM(calculated_output_cost) AS "calculatedOutputCost"
FROM
"observations_view"
WHERE
trace_id = t.id
AND "type" = 'GENERATION'
AND "project_id" = ${projectId}
${observationTimeseriesFilter}
) AS generation_metrics ON true
LEFT JOIN LATERAL (
SELECT
COUNT(*) AS "observationCount",
EXTRACT(EPOCH FROM COALESCE(MAX("end_time"), MAX("start_time"))) - EXTRACT(EPOCH FROM MIN("start_time"))::double precision AS "latency",
COALESCE(
MAX(CASE WHEN level = 'ERROR' THEN 'ERROR' END),
MAX(CASE WHEN level = 'WARNING' THEN 'WARNING' END),
MAX(CASE WHEN level = 'DEFAULT' THEN 'DEFAULT' END),
'DEBUG'
) AS "level"
FROM
"observations"
WHERE
trace_id = t.id
AND "project_id" = ${projectId}
${observationTimeseriesFilter}
) AS observation_metrics ON true
LEFT JOIN LATERAL (
SELECT
${selectScoreValues ? Prisma.sql`jsonb_object_agg(name::text, "values") AS "scores_values",` : Prisma.empty}
jsonb_object_agg(name::text, avg_value::double precision) AS "scores_avg"
FROM (
SELECT
name,
${selectScoreValues ? Prisma.sql`array_agg(COALESCE(string_value, value::text)) AS "values",` : Prisma.empty}
AVG(value) avg_value
FROM
scores
WHERE
trace_id = t.id
AND t."project_id" = ${projectId}
${selectScoreValues ? Prisma.empty : Prisma.sql`AND scores."data_type" IN ('NUMERIC', 'BOOLEAN')`}
GROUP BY
name
) tmp
) AS s_avg ON true
WHERE
t."project_id" = ${projectId}
${searchCondition}
${filterCondition}
${orderByCondition}
${limit ? Prisma.sql`LIMIT ${limit}` : Prisma.empty}
${page && limit ? Prisma.sql`OFFSET ${page * limit}` : Prisma.empty}
`;
}
@@ -0,0 +1,9 @@
export { createSessionsAllQuery } from "./createSessionsAllQuery";
export { createTracesQuery, parseTraceAllFilters } from "./createTracesQuery";
export {
createGenerationsQuery,
parseGetAllGenerationsInput,
type FullObservations,
type FullObservationsWithScores,
type IOAndMetadataOmittedObservations,
} from "./createGenerationsQuery";
@@ -0,0 +1,14 @@
import z from "zod";
import { singleFilter } from "../../interfaces/filters";
import { orderBy } from "../../interfaces/orderBy";
import { optionalPaginationZod } from "../../utils/zod";
const TableFilterSchema = z.object({
projectId: z.string(),
filter: z.array(singleFilter).nullish(),
searchQuery: z.string().nullish(),
orderBy: orderBy,
...optionalPaginationZod,
});
export type TableFilters = z.infer<typeof TableFilterSchema>;
+8 -1
View File
@@ -1,11 +1,12 @@
import { z } from "zod";
import { ingestionBatchEvent } from ".";
import { ingestionBatchEvent, TCarrier } from ".";
export enum EventName {
TraceUpsert = "TraceUpsert",
BatchExport = "BatchExport",
EvaluationExecution = "EvaluationExecution",
LegacyIngestion = "LegacyIngestion",
CloudUsageMetering = "CloudUsageMetering",
}
export const LegacyIngestionEvent = z.object({
@@ -60,6 +61,7 @@ export enum QueueName {
RepeatQueue = "repeat-queue",
IngestionFlushQueue = "ingestion-flush-queue",
LegacyIngestionQueue = "legacy-ingestion-queue",
CloudUsageMeteringQueue = "cloud-usage-metering-queue",
}
export enum QueueJobs {
@@ -69,6 +71,7 @@ export enum QueueJobs {
EnqueueBatchExportJobs = "enqueue-batch-export-jobs",
FlushIngestionEntity = "flush-ingestion-entity",
LegacyIngestionJob = "legacy-ingestion-job",
CloudUsageMeteringJob = "cloud-usage-metering-job",
}
export type TQueueJobTypes = {
@@ -77,23 +80,27 @@ export type TQueueJobTypes = {
id: string;
payload: TraceUpsertEventType;
name: QueueJobs.TraceUpsert;
_tracecontext?: TCarrier;
};
[QueueName.EvaluationExecution]: {
timestamp: Date;
id: string;
payload: EvalExecutionEventType;
name: QueueJobs.EvaluationExecution;
_tracecontext?: TCarrier;
};
[QueueName.BatchExport]: {
timestamp: Date;
id: string;
payload: BatchExportJobType;
name: QueueJobs.BatchExportJob;
_tracecontext?: TCarrier;
};
[QueueName.LegacyIngestionQueue]: {
timestamp: Date;
id: string;
payload: LegacyIngestionEventType;
name: QueueJobs.LegacyIngestionJob;
_tracecontext?: TCarrier;
};
};
@@ -1,6 +1,6 @@
import { Queue } from "bullmq";
import { QueueName, TQueueJobTypes } from "../queues";
import { redis } from "./redis";
import { createNewRedisInstance } from "./redis";
let batchExportQueue: Queue<TQueueJobTypes[QueueName.BatchExport]> | null =
null;
@@ -8,13 +8,19 @@ let batchExportQueue: Queue<TQueueJobTypes[QueueName.BatchExport]> | null =
export const getBatchExportQueue = () => {
if (batchExportQueue) return batchExportQueue;
batchExportQueue = redis
const connection = createNewRedisInstance();
batchExportQueue = connection
? new Queue<TQueueJobTypes[QueueName.BatchExport]>(QueueName.BatchExport, {
connection: redis,
connection: connection,
defaultJobOptions: {
removeOnComplete: true,
removeOnFail: 100,
removeOnFail: 10_000,
attempts: 2,
backoff: {
type: "exponential",
delay: 5000,
},
},
})
: null;
@@ -1,7 +1,7 @@
import { Queue } from "bullmq";
import { env } from "../../env";
import { redis } from "../redis/redis";
import { createNewRedisInstance } from "../redis/redis";
import { QueueName } from "../queues";
export type IngestionFlushQueue = Queue<null>;
@@ -11,14 +11,20 @@ let ingestionFlushQueue: IngestionFlushQueue | null = null;
export const getIngestionFlushQueue = () => {
if (ingestionFlushQueue) return ingestionFlushQueue;
ingestionFlushQueue = redis
const connection = createNewRedisInstance();
ingestionFlushQueue = connection
? new Queue<null>(QueueName.IngestionFlushQueue, {
connection: redis,
connection: connection,
defaultJobOptions: {
removeOnComplete: true, // Important: If not true, new jobs for that ID would be ignored as jobs in the complete set are still considered as part of the queue
removeOnFail: 1000,
removeOnFail: 100_000,
delay: env.LANGFUSE_INGESTION_FLUSH_DELAY_MS,
attempts: env.LANGFUSE_INGESTION_FLUSH_ATTEMPTS,
backoff: {
type: "exponential",
delay: 5000,
},
},
})
: null;
@@ -1,27 +1,37 @@
import { Queue } from "bullmq";
import { QueueName, TQueueJobTypes } from "../queues";
import { redis } from "./redis";
import { createNewRedisInstance } from "./redis";
let legacyIngestionQueue: Queue<
TQueueJobTypes[QueueName.LegacyIngestionQueue]
> | null = null;
export class LegacyIngestionQueue {
private static instance: Queue<
TQueueJobTypes[QueueName.LegacyIngestionQueue]
> | null = null;
export const getLegacyIngestionQueue = () => {
if (legacyIngestionQueue) return legacyIngestionQueue;
public static getInstance(): Queue<
TQueueJobTypes[QueueName.LegacyIngestionQueue]
> | null {
if (LegacyIngestionQueue.instance) return LegacyIngestionQueue.instance;
legacyIngestionQueue = redis
? new Queue<TQueueJobTypes[QueueName.LegacyIngestionQueue]>(
QueueName.LegacyIngestionQueue,
{
connection: redis,
defaultJobOptions: {
removeOnComplete: true,
removeOnFail: 100,
attempts: 5,
const newRedis = createNewRedisInstance({ enableOfflineQueue: false });
LegacyIngestionQueue.instance = newRedis
? new Queue<TQueueJobTypes[QueueName.LegacyIngestionQueue]>(
QueueName.LegacyIngestionQueue,
{
connection: newRedis,
defaultJobOptions: {
removeOnComplete: true,
removeOnFail: 100_000,
attempts: 5,
backoff: {
type: "exponential",
delay: 5000,
},
},
},
}
)
: null;
)
: null;
return legacyIngestionQueue;
};
return LegacyIngestionQueue.instance;
}
}
+25 -12
View File
@@ -1,20 +1,33 @@
import Redis from "ioredis";
import Redis, { RedisOptions } from "ioredis";
import { env } from "../../env";
import { logger } from "../logger";
export const createNewRedisInstance = (
additionalOptions: Partial<RedisOptions> = {},
) => {
return env.REDIS_CONNECTION_STRING
? new Redis(env.REDIS_CONNECTION_STRING, {
maxRetriesPerRequest: null,
enableAutoPipelining: env.REDIS_ENABLE_AUTO_PIPELINING === "true",
...additionalOptions,
})
: env.REDIS_HOST
? new Redis({
host: String(env.REDIS_HOST),
port: Number(env.REDIS_PORT),
password: String(env.REDIS_AUTH),
maxRetriesPerRequest: null, // Set to `null` to disable retrying
enableAutoPipelining: env.REDIS_ENABLE_AUTO_PIPELINING === "true",
...additionalOptions,
})
: null;
};
const createRedisClient = () => {
try {
return env.REDIS_CONNECTION_STRING
? new Redis(env.REDIS_CONNECTION_STRING, { maxRetriesPerRequest: null })
: env.REDIS_HOST
? new Redis({
host: String(env.REDIS_HOST),
port: Number(env.REDIS_PORT),
password: String(env.REDIS_AUTH),
maxRetriesPerRequest: null, // Set to `null` to disable retrying
})
: null;
return createNewRedisInstance();
} catch (e) {
console.error(e, "Failed to connect to redis");
logger.error("Failed to connect to redis", e);
return null;
}
};
@@ -6,7 +6,7 @@ import {
TraceUpsertEventType,
} from "../queues";
import { Queue } from "bullmq";
import { redis } from "./redis";
import { createNewRedisInstance } from "./redis";
let traceUpsertQueue: Queue<TQueueJobTypes[QueueName.TraceUpsert]> | null =
null;
@@ -14,9 +14,20 @@ let traceUpsertQueue: Queue<TQueueJobTypes[QueueName.TraceUpsert]> | null =
export const getTraceUpsertQueue = () => {
if (traceUpsertQueue) return traceUpsertQueue;
traceUpsertQueue = redis
const connection = createNewRedisInstance();
traceUpsertQueue = connection
? new Queue<TQueueJobTypes[QueueName.TraceUpsert]>(QueueName.TraceUpsert, {
connection: redis,
connection: connection,
defaultJobOptions: {
removeOnComplete: 100, // Important: If not true, new jobs for that ID would be ignored as jobs in the complete set are still considered as part of the queue
removeOnFail: 100_000,
attempts: 5,
backoff: {
type: "exponential",
delay: 5000,
},
},
})
: null;
@@ -24,7 +35,7 @@ export const getTraceUpsertQueue = () => {
};
export function convertTraceUpsertEventsToRedisEvents(
events: TraceUpsertEventType[]
events: TraceUpsertEventType[],
) {
const uniqueTracesPerProject = events.reduce((acc, event) => {
if (!acc.get(event.projectId)) {
@@ -50,7 +61,7 @@ export function convertTraceUpsertEventsToRedisEvents(
name: QueueJobs.TraceUpsert as const,
},
opts: {
removeOnFail: 10000,
removeOnFail: 1_000,
removeOnComplete: true,
attempts: 5,
backoff: {
@@ -1,6 +1,7 @@
import { Prompt, PrismaClient } from "@prisma/client";
import { Redis } from "ioredis";
import { env } from "../../env";
import { logger } from "../logger";
export class PromptService {
private cacheEnabled: boolean;
@@ -9,9 +10,9 @@ export class PromptService {
constructor(
private prisma: PrismaClient,
private redis: Redis | null,
private metricIncrementer?: // used for Sentry metrics
private metricIncrementer?: // used for otel metrics
(name: string, value?: number) => void,
cacheEnabled?: boolean // used for testing
cacheEnabled?: boolean, // used for testing
) {
this.cacheEnabled =
Boolean(redis) &&
@@ -25,7 +26,7 @@ export class PromptService {
const cachedPrompt = await this.getCachedPrompt(params);
this.incrementMetric(
cachedPrompt ? Metrics.PromptCacheHit : Metrics.PromptCacheMiss
cachedPrompt ? Metrics.PromptCacheHit : Metrics.PromptCacheMiss,
);
if (cachedPrompt) {
@@ -117,7 +118,7 @@ export class PromptService {
}
public async lockCache(
params: Pick<PromptParams, "projectId" | "promptName">
params: Pick<PromptParams, "projectId" | "promptName">,
): Promise<void> {
if (!this.cacheEnabled) return;
@@ -133,7 +134,7 @@ export class PromptService {
}
public async unlockCache(
params: Pick<PromptParams, "projectId" | "promptName">
params: Pick<PromptParams, "projectId" | "promptName">,
): Promise<void> {
if (!this.cacheEnabled) return;
@@ -149,7 +150,7 @@ export class PromptService {
}
private async isCacheLocked(
params: Pick<PromptParams, "projectId" | "promptName">
params: Pick<PromptParams, "projectId" | "promptName">,
): Promise<boolean> {
const lockKey = this.getLockKey(params);
@@ -163,14 +164,14 @@ export class PromptService {
}
private getLockKey(
params: Pick<PromptParams, "projectId" | "promptName">
params: Pick<PromptParams, "projectId" | "promptName">,
): string {
// Important to *pre*fix LOCK as otherwise it would be deleted by deleteKeysByPrefix
return `LOCK:${this.getCacheKeyPrefix(params)}`;
}
public async invalidateCache(
params: Pick<PromptParams, "projectId" | "promptName">
params: Pick<PromptParams, "projectId" | "promptName">,
): Promise<void> {
if (!this.cacheEnabled) return;
@@ -187,7 +188,7 @@ export class PromptService {
await this.redis?.del([...(keys ?? []), keyIndexKey]);
this.logInfo(
`Cache invalidated for prefix ${cacheKeyPrefix} in ${Date.now() - startTime}ms`
`Cache invalidated for prefix ${cacheKeyPrefix} in ${Date.now() - startTime}ms`,
);
} catch (e) {
this.logError("Error deleting keys for prefix", cacheKeyPrefix, e);
@@ -203,23 +204,23 @@ export class PromptService {
}
private getCacheKeyPrefix(
params: Pick<PromptParams, "projectId" | "promptName">
params: Pick<PromptParams, "projectId" | "promptName">,
): string {
return `prompt:${params.projectId}:${params.promptName}`;
}
private getKeyIndexKey(
params: Pick<PromptParams, "projectId" | "promptName">
params: Pick<PromptParams, "projectId" | "promptName">,
): string {
return `prompt_key_index:${params.projectId}:${params.promptName}`;
}
private logError(message: string, ...args: any[]) {
console.error(`[PromptService] ${message}`, ...args);
logger.error(`[PromptService] ${message}`, ...args);
}
private logInfo(message: string, ...args: any[]) {
console.log(`[PromptService] ${message}`, ...args);
logger.info(`[PromptService] ${message}`, ...args);
}
private incrementMetric(name: Metrics, value: number = 1) {
@@ -1,7 +1,12 @@
import type { Readable } from "stream";
import { GetObjectCommand, S3Client } from "@aws-sdk/client-s3";
import {
GetObjectCommand,
PutObjectCommand,
S3Client,
} from "@aws-sdk/client-s3";
import { Upload } from "@aws-sdk/lib-storage";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
import { logger } from "../logger";
type UploadFile = {
fileName: string;
@@ -15,20 +20,26 @@ export class S3StorageService {
private bucketName: string;
constructor(params: {
accessKeyId: string;
secretAccessKey: string;
accessKeyId: string | undefined;
secretAccessKey: string | undefined;
bucketName: string;
endpoint: string;
region: string;
endpoint: string | undefined;
region: string | undefined;
}) {
const { accessKeyId, secretAccessKey, bucketName, endpoint, region } =
params;
// Use accessKeyId and secretAccessKey if provided or fallback to default credentials
const credentials =
accessKeyId !== undefined && secretAccessKey !== undefined
? {
accessKeyId,
secretAccessKey,
}
: undefined;
this.client = new S3Client({
credentials: {
accessKeyId,
secretAccessKey,
},
credentials,
endpoint,
region,
});
@@ -56,15 +67,30 @@ export class S3StorageService {
return { signedUrl };
} catch (err) {
console.error(err);
logger.error(`Failed to upload file to ${fileName}`, err);
throw new Error("Failed to upload to S3 or generate signed URL");
}
}
public async uploadJson(path: string, body: Record<string, unknown>) {
const putCommand = new PutObjectCommand({
Bucket: this.bucketName,
Key: path,
Body: JSON.stringify(body),
ContentType: "application/json",
});
try {
await this.client.send(putCommand);
} catch (err) {
logger.error(`Failed to upload JSON to S3 ${path}`, err);
throw Error("Failed to upload JSON to S3");
}
}
private async getSignedUrl(
fileName: string,
ttlSeconds: number
ttlSeconds: number,
): Promise<string> {
try {
return await getSignedUrl(
@@ -74,9 +100,10 @@ export class S3StorageService {
Key: fileName,
ResponseContentDisposition: `attachment; filename="${fileName}"`,
}),
{ expiresIn: ttlSeconds }
{ expiresIn: ttlSeconds },
);
} catch (err) {
logger.error(`Failed to generate presigned URL for ${fileName}`, err);
throw Error("Failed to generate signed URL");
}
}
@@ -59,6 +59,10 @@ export const BatchExportSuccessEmailTemplate = ({
download. The download link will expire in{" "}
<strong>{expiresInHours} hours</strong>.
</Text>
<Text className="text-sm leading-6 text-black">
Please note data exports do not reflect custom column ordering or
visibility.
</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"
@@ -3,6 +3,7 @@ import { parseConnectionUrl } from "nodemailer/lib/shared/index.js";
import { render } from "@react-email/render";
import { BatchExportSuccessEmailTemplate } from "./BatchExportSuccessEmailTemplate";
import { logger } from "../../../logger";
type SendBatchExportSuccessParams = {
env: Partial<
@@ -24,8 +25,7 @@ export const sendBatchExportSuccessEmail = async ({
expiresInHours,
}: SendBatchExportSuccessParams) => {
if (!env.EMAIL_FROM_ADDRESS || !env.SMTP_CONNECTION_URL) {
console.error("Missing environment variables for sending email.");
logger.error("Missing environment variables for sending email.");
return;
}
@@ -38,7 +38,7 @@ export const sendBatchExportSuccessEmail = async ({
userName,
batchExportName,
expiresInHours,
})
}),
);
await mailer.sendMail({
@@ -51,6 +51,6 @@ export const sendBatchExportSuccessEmail = async ({
html: htmlTemplate,
});
} catch (error) {
console.error(error);
logger.error(error);
}
};
@@ -3,6 +3,7 @@ import { parseConnectionUrl } from "nodemailer/lib/shared/index.js";
import { render } from "@react-email/render";
import MembershipInvitationTemplate from "./MembershipInvitationEmailTemplate";
import { logger } from "../../../logger";
const langfuseUrls = {
US: "https://us.cloud.langfuse.com",
@@ -34,8 +35,8 @@ export const sendMembershipInvitationEmail = async ({
orgName,
}: SendMembershipInvitationParams) => {
if (!env.EMAIL_FROM_ADDRESS || !env.SMTP_CONNECTION_URL) {
console.error(
"Missing environment variables for sending membership invitation email."
logger.error(
"Missing environment variables for sending membership invitation email.",
);
return;
}
@@ -49,8 +50,8 @@ export const sendMembershipInvitationEmail = async ({
const authUrl = getAuthURL();
if (!authUrl) {
console.error(
"Missing NEXTAUTH_URL or NEXT_PUBLIC_LANGFUSE_CLOUD_REGION environment variable."
logger.error(
"Missing NEXTAUTH_URL or NEXT_PUBLIC_LANGFUSE_CLOUD_REGION environment variable.",
);
return;
}
@@ -67,7 +68,7 @@ export const sendMembershipInvitationEmail = async ({
inviteLink: authUrl,
emailFromAddress: env.EMAIL_FROM_ADDRESS,
langfuseCloudRegion: env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION,
})
}),
);
await mailer.sendMail({
@@ -77,6 +78,6 @@ export const sendMembershipInvitationEmail = async ({
html: htmlTemplate,
});
} catch (error) {
console.error(error);
logger.error(error);
}
};
@@ -56,8 +56,9 @@ const ResetPasswordTemplate = ({ url }: ResetPasswordTemplateProps) => {
</Button>
</Section>
<Text className="text-center text-xs leading-6 text-[#666666]">
If you do not want to change your password or didn&apos;t request
a reset, you can ignore and delete this email.
The link is valid for 10 minutes. If you do not want to change
your password or didn&apos;t request a reset, you can ignore and
delete this email.
</Text>
</Container>
</Body>
@@ -76,7 +77,7 @@ export async function sendResetPasswordVerificationRequest(
to: identifier,
from: provider.from,
subject: `Forgot your password?`,
text: `To reset your Langfuse password, please confirm your email:\n${url}\n\nIf you do not want to change your password or didn't request a reset, you can ignore and delete this email.`,
text: `To reset your Langfuse password, please confirm your email:\n${url}\n\nThe link is valid for 10 minutes. If you do not want to change your password or didn't request a reset, you can ignore and delete this email.`,
html: htmlTemplate,
});
const failed = result.rejected.concat(result.pending).filter(Boolean);
@@ -1,6 +1,8 @@
import { Transform, type TransformCallback } from "stream";
import { stringify } from "./stringify";
const DELIMITER = ",";
export function transformStreamToCsv(): Transform {
let isFirstChunk = true;
let headers: string[] = [];
@@ -15,15 +17,19 @@ export function transformStreamToCsv(): Transform {
if (isFirstChunk) {
// Extract headers from the first object
headers = Object.keys(row);
this.push(headers.join(",") + "\n");
this.push(headers.join(DELIMITER) + "\n");
isFirstChunk = false;
}
// Convert the object to a CSV line and push it
const csvRow = headers.map((header) => {
const field = row[header];
const str = stringify(field);
let str = stringify(field);
// escape and format fields that contain commas
if (str.includes(",")) {
str = `"${str.replace(/"/g, '""')}"`;
}
if (str.startsWith('"') && str.endsWith('"')) {
return str;
} else {
@@ -31,7 +37,7 @@ export function transformStreamToCsv(): Transform {
}
});
this.push(csvRow.join(",") + "\n");
this.push(csvRow.join(DELIMITER) + "\n");
callback();
},
@@ -17,6 +17,7 @@ export const sessionsViewCols: ColumnDefinition[] = [
type: "arrayOptions",
internal: 't."userIds"',
options: [], // to be filled in at runtime
nullable: true,
},
{
name: "Session Duration (s)",
@@ -78,10 +79,18 @@ export const sessionsViewCols: ColumnDefinition[] = [
type: "number",
internal: 'o."totalTokens"',
},
{
name: "Trace Tags",
id: "tags",
type: "arrayOptions",
internal: 't."tags"',
options: [], // to be filled in at runtime
},
];
export type SessionOptions = {
userIds: Array<OptionsDefinition>;
tags: Array<OptionsDefinition>;
};
export function sessionsTableColsWithOptions(
@@ -91,6 +100,9 @@ export function sessionsTableColsWithOptions(
if (col.id === "userIds") {
return { ...col, options: options?.userIds ?? [] };
}
if (col.id === "tags") {
return { ...col, options: options?.tags ?? [] };
}
return col;
});
}
@@ -9,6 +9,7 @@ export type ColumnDefinition =
id: string;
type: "number" | "string" | "datetime" | "boolean";
internal: string;
nullable?: boolean;
}
| {
name: string;
@@ -16,6 +17,7 @@ export type ColumnDefinition =
type: "stringOptions";
options: Array<OptionsDefinition>;
internal: string;
nullable?: boolean;
}
| {
name: string;
@@ -23,6 +25,7 @@ export type ColumnDefinition =
type: "arrayOptions";
options: Array<OptionsDefinition>;
internal: string;
nullable?: boolean;
}
| {
name: string;
@@ -30,6 +33,7 @@ export type ColumnDefinition =
type: "stringObject" | "numberObject";
internal: string;
keyOptions?: Array<string>;
nullable?: boolean;
};
export const tableNames = [
+23 -6
View File
@@ -15,6 +15,7 @@ const tracesOnlyCols: ColumnDefinition[] = [
type: "stringOptions",
internal: 't."name"',
options: [], // to be filled in at runtime
nullable: true,
},
{
name: "Timestamp",
@@ -22,12 +23,19 @@ const tracesOnlyCols: ColumnDefinition[] = [
type: "datetime",
internal: 't."timestamp"',
},
{ name: "User ID", id: "userId", type: "string", internal: 't."user_id"' },
{
name: "User ID",
id: "userId",
type: "string",
internal: 't."user_id"',
nullable: true,
},
{
name: "Session ID",
id: "sessionId",
type: "string",
internal: 't."session_id"',
nullable: true,
},
{
name: "Metadata",
@@ -40,12 +48,14 @@ const tracesOnlyCols: ColumnDefinition[] = [
id: "version",
type: "string",
internal: 't."version"',
nullable: true,
},
{
name: "Release",
id: "release",
type: "string",
internal: 't."release"',
nullable: true,
},
{
name: "Level",
@@ -68,25 +78,29 @@ export const tracesTableCols: ColumnDefinition[] = [
name: "Input Tokens",
id: "inputTokens",
type: "number",
internal: 'tm."promptTokens"',
internal: 'generation_metrics."promptTokens"',
nullable: true,
},
{
name: "Output Tokens",
id: "outputTokens",
type: "number",
internal: 'tm."completionTokens"',
internal: 'generation_metrics."completionTokens"',
nullable: true,
},
{
name: "Total Tokens",
id: "totalTokens",
type: "number",
internal: 'tm."totalTokens"',
internal: 'generation_metrics."totalTokens"',
nullable: true,
},
{
name: "Usage",
id: "usage",
type: "number",
internal: 'tm."totalTokens"',
internal: 'generation_metrics."totalTokens"',
nullable: true,
},
{
@@ -99,25 +113,28 @@ export const tracesTableCols: ColumnDefinition[] = [
name: "Latency (s)",
id: "latency",
type: "number",
internal: "tl.latency",
internal: "observation_metrics.latency",
},
{
name: "Input Cost ($)",
id: "inputCost",
type: "number",
internal: '"calculatedInputCost"',
nullable: true,
},
{
name: "Output Cost ($)",
id: "outputCost",
type: "number",
internal: '"calculatedOutputCost"',
nullable: true,
},
{
name: "Total Cost ($)",
id: "totalCost",
type: "number",
internal: '"calculatedTotalCost"',
nullable: true,
},
];
+2 -1
View File
@@ -1,7 +1,8 @@
import { type z } from "zod";
import { singleFilter } from "./interfaces/filters";
import { singleFilter, timeFilter } from "./interfaces/filters";
// to be sent to the server
export type TimeFilter = z.infer<typeof timeFilter>;
export type FilterCondition = z.infer<typeof singleFilter>;
export type FilterState = FilterCondition[];
+4306 -2517
View File
File diff suppressed because it is too large Load Diff

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