From 70b4b2572bebaee43e248c1cfb900f7992507f42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Peer=20St=C3=B6cklmair?= Date: Mon, 20 Apr 2026 08:57:05 +0200 Subject: [PATCH 1/2] test(effect): Rename effect e2e tests to a versioned folder (#20390) This PR is only renaming Effect v3 e2e tests, which had no versioning, to one with a version. This makes it easier in the future to distinguish between Effect v3 and v4 tests. So effectively this has changed: - `effect-node` -> `effect-3-node` - `effect-browser` -> `effect-3-browser` --- .../{effect-browser => effect-3-browser}/.gitignore | 0 .../{effect-browser => effect-3-browser}/.npmrc | 0 .../{effect-browser => effect-3-browser}/build.mjs | 0 .../{effect-browser => effect-3-browser}/package.json | 2 +- .../playwright.config.mjs | 0 .../public/index.html | 0 .../{effect-browser => effect-3-browser}/src/index.js | 0 .../start-event-proxy.mjs | 2 +- .../tests/errors.test.ts | 6 +++--- .../tests/logs.test.ts | 10 +++++----- .../tests/transactions.test.ts | 10 +++++----- .../{effect-browser => effect-3-browser}/tsconfig.json | 0 .../{effect-node => effect-3-node}/.gitignore | 0 .../{effect-node => effect-3-node}/.npmrc | 0 .../{effect-node => effect-3-node}/package.json | 2 +- .../playwright.config.mjs | 0 .../{effect-node => effect-3-node}/src/app.ts | 0 .../start-event-proxy.mjs | 2 +- .../tests/errors.test.ts | 8 ++++---- .../{effect-node => effect-3-node}/tests/logs.test.ts | 10 +++++----- .../tests/transactions.test.ts | 8 ++++---- .../{effect-node => effect-3-node}/tsconfig.json | 0 22 files changed, 30 insertions(+), 30 deletions(-) rename dev-packages/e2e-tests/test-applications/{effect-browser => effect-3-browser}/.gitignore (100%) rename dev-packages/e2e-tests/test-applications/{effect-browser => effect-3-browser}/.npmrc (100%) rename dev-packages/e2e-tests/test-applications/{effect-browser => effect-3-browser}/build.mjs (100%) rename dev-packages/e2e-tests/test-applications/{effect-browser => effect-3-browser}/package.json (96%) rename dev-packages/e2e-tests/test-applications/{effect-browser => effect-3-browser}/playwright.config.mjs (100%) rename dev-packages/e2e-tests/test-applications/{effect-browser => effect-3-browser}/public/index.html (100%) rename dev-packages/e2e-tests/test-applications/{effect-browser => effect-3-browser}/src/index.js (100%) rename dev-packages/e2e-tests/test-applications/{effect-browser => effect-3-browser}/start-event-proxy.mjs (74%) rename dev-packages/e2e-tests/test-applications/{effect-browser => effect-3-browser}/tests/errors.test.ts (87%) rename dev-packages/e2e-tests/test-applications/{effect-browser => effect-3-browser}/tests/logs.test.ts (90%) rename dev-packages/e2e-tests/test-applications/{effect-browser => effect-3-browser}/tests/transactions.test.ts (90%) rename dev-packages/e2e-tests/test-applications/{effect-browser => effect-3-browser}/tsconfig.json (100%) rename dev-packages/e2e-tests/test-applications/{effect-node => effect-3-node}/.gitignore (100%) rename dev-packages/e2e-tests/test-applications/{effect-node => effect-3-node}/.npmrc (100%) rename dev-packages/e2e-tests/test-applications/{effect-node => effect-3-node}/package.json (95%) rename dev-packages/e2e-tests/test-applications/{effect-node => effect-3-node}/playwright.config.mjs (100%) rename dev-packages/e2e-tests/test-applications/{effect-node => effect-3-node}/src/app.ts (100%) rename dev-packages/e2e-tests/test-applications/{effect-node => effect-3-node}/start-event-proxy.mjs (75%) rename dev-packages/e2e-tests/test-applications/{effect-node => effect-3-node}/tests/errors.test.ts (86%) rename dev-packages/e2e-tests/test-applications/{effect-node => effect-3-node}/tests/logs.test.ts (88%) rename dev-packages/e2e-tests/test-applications/{effect-node => effect-3-node}/tests/transactions.test.ts (88%) rename dev-packages/e2e-tests/test-applications/{effect-node => effect-3-node}/tsconfig.json (100%) diff --git a/dev-packages/e2e-tests/test-applications/effect-browser/.gitignore b/dev-packages/e2e-tests/test-applications/effect-3-browser/.gitignore similarity index 100% rename from dev-packages/e2e-tests/test-applications/effect-browser/.gitignore rename to dev-packages/e2e-tests/test-applications/effect-3-browser/.gitignore diff --git a/dev-packages/e2e-tests/test-applications/effect-browser/.npmrc b/dev-packages/e2e-tests/test-applications/effect-3-browser/.npmrc similarity index 100% rename from dev-packages/e2e-tests/test-applications/effect-browser/.npmrc rename to dev-packages/e2e-tests/test-applications/effect-3-browser/.npmrc diff --git a/dev-packages/e2e-tests/test-applications/effect-browser/build.mjs b/dev-packages/e2e-tests/test-applications/effect-3-browser/build.mjs similarity index 100% rename from dev-packages/e2e-tests/test-applications/effect-browser/build.mjs rename to dev-packages/e2e-tests/test-applications/effect-3-browser/build.mjs diff --git a/dev-packages/e2e-tests/test-applications/effect-browser/package.json b/dev-packages/e2e-tests/test-applications/effect-3-browser/package.json similarity index 96% rename from dev-packages/e2e-tests/test-applications/effect-browser/package.json rename to dev-packages/e2e-tests/test-applications/effect-3-browser/package.json index 6c2e7e63ced8..c8cfbb5b587d 100644 --- a/dev-packages/e2e-tests/test-applications/effect-browser/package.json +++ b/dev-packages/e2e-tests/test-applications/effect-3-browser/package.json @@ -1,5 +1,5 @@ { - "name": "effect-browser-test-app", + "name": "effect-3-browser-test-app", "version": "1.0.0", "private": true, "scripts": { diff --git a/dev-packages/e2e-tests/test-applications/effect-browser/playwright.config.mjs b/dev-packages/e2e-tests/test-applications/effect-3-browser/playwright.config.mjs similarity index 100% rename from dev-packages/e2e-tests/test-applications/effect-browser/playwright.config.mjs rename to dev-packages/e2e-tests/test-applications/effect-3-browser/playwright.config.mjs diff --git a/dev-packages/e2e-tests/test-applications/effect-browser/public/index.html b/dev-packages/e2e-tests/test-applications/effect-3-browser/public/index.html similarity index 100% rename from dev-packages/e2e-tests/test-applications/effect-browser/public/index.html rename to dev-packages/e2e-tests/test-applications/effect-3-browser/public/index.html diff --git a/dev-packages/e2e-tests/test-applications/effect-browser/src/index.js b/dev-packages/e2e-tests/test-applications/effect-3-browser/src/index.js similarity index 100% rename from dev-packages/e2e-tests/test-applications/effect-browser/src/index.js rename to dev-packages/e2e-tests/test-applications/effect-3-browser/src/index.js diff --git a/dev-packages/e2e-tests/test-applications/effect-browser/start-event-proxy.mjs b/dev-packages/e2e-tests/test-applications/effect-3-browser/start-event-proxy.mjs similarity index 74% rename from dev-packages/e2e-tests/test-applications/effect-browser/start-event-proxy.mjs rename to dev-packages/e2e-tests/test-applications/effect-3-browser/start-event-proxy.mjs index a86a1bd91404..6da20fa0890e 100644 --- a/dev-packages/e2e-tests/test-applications/effect-browser/start-event-proxy.mjs +++ b/dev-packages/e2e-tests/test-applications/effect-3-browser/start-event-proxy.mjs @@ -2,5 +2,5 @@ import { startEventProxyServer } from '@sentry-internal/test-utils'; startEventProxyServer({ port: 3031, - proxyServerName: 'effect-browser', + proxyServerName: 'effect-3-browser', }); diff --git a/dev-packages/e2e-tests/test-applications/effect-browser/tests/errors.test.ts b/dev-packages/e2e-tests/test-applications/effect-3-browser/tests/errors.test.ts similarity index 87% rename from dev-packages/e2e-tests/test-applications/effect-browser/tests/errors.test.ts rename to dev-packages/e2e-tests/test-applications/effect-3-browser/tests/errors.test.ts index 80589f683c28..bca922963ee1 100644 --- a/dev-packages/e2e-tests/test-applications/effect-browser/tests/errors.test.ts +++ b/dev-packages/e2e-tests/test-applications/effect-3-browser/tests/errors.test.ts @@ -2,7 +2,7 @@ import { expect, test } from '@playwright/test'; import { waitForError, waitForTransaction } from '@sentry-internal/test-utils'; test('captures an error', async ({ page }) => { - const errorEventPromise = waitForError('effect-browser', event => { + const errorEventPromise = waitForError('effect-3-browser', event => { return !event.type && event.exception?.values?.[0]?.value === 'I am an error!'; }); @@ -29,11 +29,11 @@ test('captures an error', async ({ page }) => { }); test('sets correct transactionName', async ({ page }) => { - const transactionPromise = waitForTransaction('effect-browser', async transactionEvent => { + const transactionPromise = waitForTransaction('effect-3-browser', async transactionEvent => { return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'pageload'; }); - const errorEventPromise = waitForError('effect-browser', event => { + const errorEventPromise = waitForError('effect-3-browser', event => { return !event.type && event.exception?.values?.[0]?.value === 'I am an error!'; }); diff --git a/dev-packages/e2e-tests/test-applications/effect-browser/tests/logs.test.ts b/dev-packages/e2e-tests/test-applications/effect-3-browser/tests/logs.test.ts similarity index 90% rename from dev-packages/e2e-tests/test-applications/effect-browser/tests/logs.test.ts rename to dev-packages/e2e-tests/test-applications/effect-3-browser/tests/logs.test.ts index f81bc249cbd8..7857b7f9a156 100644 --- a/dev-packages/e2e-tests/test-applications/effect-browser/tests/logs.test.ts +++ b/dev-packages/e2e-tests/test-applications/effect-3-browser/tests/logs.test.ts @@ -3,7 +3,7 @@ import { waitForEnvelopeItem } from '@sentry-internal/test-utils'; import type { SerializedLogContainer } from '@sentry/core'; test('should send Effect debug logs', async ({ page }) => { - const logEnvelopePromise = waitForEnvelopeItem('effect-browser', envelope => { + const logEnvelopePromise = waitForEnvelopeItem('effect-3-browser', envelope => { return ( envelope[0].type === 'log' && (envelope[1] as SerializedLogContainer).items.some( @@ -26,7 +26,7 @@ test('should send Effect debug logs', async ({ page }) => { }); test('should send Effect info logs', async ({ page }) => { - const logEnvelopePromise = waitForEnvelopeItem('effect-browser', envelope => { + const logEnvelopePromise = waitForEnvelopeItem('effect-3-browser', envelope => { return ( envelope[0].type === 'log' && (envelope[1] as SerializedLogContainer).items.some( @@ -49,7 +49,7 @@ test('should send Effect info logs', async ({ page }) => { }); test('should send Effect warning logs', async ({ page }) => { - const logEnvelopePromise = waitForEnvelopeItem('effect-browser', envelope => { + const logEnvelopePromise = waitForEnvelopeItem('effect-3-browser', envelope => { return ( envelope[0].type === 'log' && (envelope[1] as SerializedLogContainer).items.some( @@ -72,7 +72,7 @@ test('should send Effect warning logs', async ({ page }) => { }); test('should send Effect error logs', async ({ page }) => { - const logEnvelopePromise = waitForEnvelopeItem('effect-browser', envelope => { + const logEnvelopePromise = waitForEnvelopeItem('effect-3-browser', envelope => { return ( envelope[0].type === 'log' && (envelope[1] as SerializedLogContainer).items.some( @@ -95,7 +95,7 @@ test('should send Effect error logs', async ({ page }) => { }); test('should send Effect logs with context attributes', async ({ page }) => { - const logEnvelopePromise = waitForEnvelopeItem('effect-browser', envelope => { + const logEnvelopePromise = waitForEnvelopeItem('effect-3-browser', envelope => { return ( envelope[0].type === 'log' && (envelope[1] as SerializedLogContainer).items.some(item => item.body === 'Log with context') diff --git a/dev-packages/e2e-tests/test-applications/effect-browser/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/effect-3-browser/tests/transactions.test.ts similarity index 90% rename from dev-packages/e2e-tests/test-applications/effect-browser/tests/transactions.test.ts rename to dev-packages/e2e-tests/test-applications/effect-3-browser/tests/transactions.test.ts index b7c60b488403..db2a1dc352a8 100644 --- a/dev-packages/e2e-tests/test-applications/effect-browser/tests/transactions.test.ts +++ b/dev-packages/e2e-tests/test-applications/effect-3-browser/tests/transactions.test.ts @@ -2,7 +2,7 @@ import { expect, test } from '@playwright/test'; import { waitForTransaction } from '@sentry-internal/test-utils'; test('captures a pageload transaction', async ({ page }) => { - const transactionPromise = waitForTransaction('effect-browser', async transactionEvent => { + const transactionPromise = waitForTransaction('effect-3-browser', async transactionEvent => { return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'pageload'; }); @@ -49,11 +49,11 @@ test('captures a pageload transaction', async ({ page }) => { }); test('captures a navigation transaction', async ({ page }) => { - const pageLoadTransactionPromise = waitForTransaction('effect-browser', async transactionEvent => { + const pageLoadTransactionPromise = waitForTransaction('effect-3-browser', async transactionEvent => { return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'pageload'; }); - const navigationTransactionPromise = waitForTransaction('effect-browser', async transactionEvent => { + const navigationTransactionPromise = waitForTransaction('effect-3-browser', async transactionEvent => { return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'navigation'; }); @@ -80,11 +80,11 @@ test('captures a navigation transaction', async ({ page }) => { }); test('captures Effect spans with correct parent-child structure', async ({ page }) => { - const pageloadPromise = waitForTransaction('effect-browser', transactionEvent => { + const pageloadPromise = waitForTransaction('effect-3-browser', transactionEvent => { return transactionEvent?.contexts?.trace?.op === 'pageload'; }); - const transactionPromise = waitForTransaction('effect-browser', transactionEvent => { + const transactionPromise = waitForTransaction('effect-3-browser', transactionEvent => { return ( transactionEvent?.contexts?.trace?.op === 'ui.action.click' && transactionEvent.spans?.some(span => span.description === 'custom-effect-span') diff --git a/dev-packages/e2e-tests/test-applications/effect-browser/tsconfig.json b/dev-packages/e2e-tests/test-applications/effect-3-browser/tsconfig.json similarity index 100% rename from dev-packages/e2e-tests/test-applications/effect-browser/tsconfig.json rename to dev-packages/e2e-tests/test-applications/effect-3-browser/tsconfig.json diff --git a/dev-packages/e2e-tests/test-applications/effect-node/.gitignore b/dev-packages/e2e-tests/test-applications/effect-3-node/.gitignore similarity index 100% rename from dev-packages/e2e-tests/test-applications/effect-node/.gitignore rename to dev-packages/e2e-tests/test-applications/effect-3-node/.gitignore diff --git a/dev-packages/e2e-tests/test-applications/effect-node/.npmrc b/dev-packages/e2e-tests/test-applications/effect-3-node/.npmrc similarity index 100% rename from dev-packages/e2e-tests/test-applications/effect-node/.npmrc rename to dev-packages/e2e-tests/test-applications/effect-3-node/.npmrc diff --git a/dev-packages/e2e-tests/test-applications/effect-node/package.json b/dev-packages/e2e-tests/test-applications/effect-3-node/package.json similarity index 95% rename from dev-packages/e2e-tests/test-applications/effect-node/package.json rename to dev-packages/e2e-tests/test-applications/effect-3-node/package.json index 621a017d3020..43f9bee85306 100644 --- a/dev-packages/e2e-tests/test-applications/effect-node/package.json +++ b/dev-packages/e2e-tests/test-applications/effect-3-node/package.json @@ -1,5 +1,5 @@ { - "name": "effect-node-app", + "name": "effect-3-node-app", "version": "1.0.0", "private": true, "type": "module", diff --git a/dev-packages/e2e-tests/test-applications/effect-node/playwright.config.mjs b/dev-packages/e2e-tests/test-applications/effect-3-node/playwright.config.mjs similarity index 100% rename from dev-packages/e2e-tests/test-applications/effect-node/playwright.config.mjs rename to dev-packages/e2e-tests/test-applications/effect-3-node/playwright.config.mjs diff --git a/dev-packages/e2e-tests/test-applications/effect-node/src/app.ts b/dev-packages/e2e-tests/test-applications/effect-3-node/src/app.ts similarity index 100% rename from dev-packages/e2e-tests/test-applications/effect-node/src/app.ts rename to dev-packages/e2e-tests/test-applications/effect-3-node/src/app.ts diff --git a/dev-packages/e2e-tests/test-applications/effect-node/start-event-proxy.mjs b/dev-packages/e2e-tests/test-applications/effect-3-node/start-event-proxy.mjs similarity index 75% rename from dev-packages/e2e-tests/test-applications/effect-node/start-event-proxy.mjs rename to dev-packages/e2e-tests/test-applications/effect-3-node/start-event-proxy.mjs index 41eb647958b7..d74e61dc653b 100644 --- a/dev-packages/e2e-tests/test-applications/effect-node/start-event-proxy.mjs +++ b/dev-packages/e2e-tests/test-applications/effect-3-node/start-event-proxy.mjs @@ -2,5 +2,5 @@ import { startEventProxyServer } from '@sentry-internal/test-utils'; startEventProxyServer({ port: 3031, - proxyServerName: 'effect-node', + proxyServerName: 'effect-3-node', }); diff --git a/dev-packages/e2e-tests/test-applications/effect-node/tests/errors.test.ts b/dev-packages/e2e-tests/test-applications/effect-3-node/tests/errors.test.ts similarity index 86% rename from dev-packages/e2e-tests/test-applications/effect-node/tests/errors.test.ts rename to dev-packages/e2e-tests/test-applications/effect-3-node/tests/errors.test.ts index 3b7da230c0e0..848ffcfb8117 100644 --- a/dev-packages/e2e-tests/test-applications/effect-node/tests/errors.test.ts +++ b/dev-packages/e2e-tests/test-applications/effect-3-node/tests/errors.test.ts @@ -2,7 +2,7 @@ import { expect, test } from '@playwright/test'; import { waitForError } from '@sentry-internal/test-utils'; test('Captures manually reported error', async ({ baseURL }) => { - const errorEventPromise = waitForError('effect-node', event => { + const errorEventPromise = waitForError('effect-3-node', event => { return !event.type && event.exception?.values?.[0]?.value === 'This is an error'; }); @@ -17,7 +17,7 @@ test('Captures manually reported error', async ({ baseURL }) => { }); test('Captures thrown exception', async ({ baseURL }) => { - const errorEventPromise = waitForError('effect-node', event => { + const errorEventPromise = waitForError('effect-3-node', event => { return !event.type && event.exception?.values?.[0]?.value === 'This is an exception with id 123'; }); @@ -30,7 +30,7 @@ test('Captures thrown exception', async ({ baseURL }) => { }); test('Captures Effect.fail as error', async ({ baseURL }) => { - const errorEventPromise = waitForError('effect-node', event => { + const errorEventPromise = waitForError('effect-3-node', event => { return !event.type && event.exception?.values?.[0]?.value === 'Effect failure'; }); @@ -43,7 +43,7 @@ test('Captures Effect.fail as error', async ({ baseURL }) => { }); test('Captures Effect.die as error', async ({ baseURL }) => { - const errorEventPromise = waitForError('effect-node', event => { + const errorEventPromise = waitForError('effect-3-node', event => { return !event.type && event.exception?.values?.[0]?.value?.includes('Effect defect'); }); diff --git a/dev-packages/e2e-tests/test-applications/effect-node/tests/logs.test.ts b/dev-packages/e2e-tests/test-applications/effect-3-node/tests/logs.test.ts similarity index 88% rename from dev-packages/e2e-tests/test-applications/effect-node/tests/logs.test.ts rename to dev-packages/e2e-tests/test-applications/effect-3-node/tests/logs.test.ts index 85f5840e14a8..2519f18722fd 100644 --- a/dev-packages/e2e-tests/test-applications/effect-node/tests/logs.test.ts +++ b/dev-packages/e2e-tests/test-applications/effect-3-node/tests/logs.test.ts @@ -3,7 +3,7 @@ import { waitForEnvelopeItem } from '@sentry-internal/test-utils'; import type { SerializedLogContainer } from '@sentry/core'; test('should send Effect debug logs', async ({ baseURL }) => { - const logEnvelopePromise = waitForEnvelopeItem('effect-node', envelope => { + const logEnvelopePromise = waitForEnvelopeItem('effect-3-node', envelope => { return ( envelope[0].type === 'log' && (envelope[1] as SerializedLogContainer).items.some( @@ -22,7 +22,7 @@ test('should send Effect debug logs', async ({ baseURL }) => { }); test('should send Effect info logs', async ({ baseURL }) => { - const logEnvelopePromise = waitForEnvelopeItem('effect-node', envelope => { + const logEnvelopePromise = waitForEnvelopeItem('effect-3-node', envelope => { return ( envelope[0].type === 'log' && (envelope[1] as SerializedLogContainer).items.some( @@ -41,7 +41,7 @@ test('should send Effect info logs', async ({ baseURL }) => { }); test('should send Effect warning logs', async ({ baseURL }) => { - const logEnvelopePromise = waitForEnvelopeItem('effect-node', envelope => { + const logEnvelopePromise = waitForEnvelopeItem('effect-3-node', envelope => { return ( envelope[0].type === 'log' && (envelope[1] as SerializedLogContainer).items.some( @@ -60,7 +60,7 @@ test('should send Effect warning logs', async ({ baseURL }) => { }); test('should send Effect error logs', async ({ baseURL }) => { - const logEnvelopePromise = waitForEnvelopeItem('effect-node', envelope => { + const logEnvelopePromise = waitForEnvelopeItem('effect-3-node', envelope => { return ( envelope[0].type === 'log' && (envelope[1] as SerializedLogContainer).items.some( @@ -79,7 +79,7 @@ test('should send Effect error logs', async ({ baseURL }) => { }); test('should send Effect logs with context attributes', async ({ baseURL }) => { - const logEnvelopePromise = waitForEnvelopeItem('effect-node', envelope => { + const logEnvelopePromise = waitForEnvelopeItem('effect-3-node', envelope => { return ( envelope[0].type === 'log' && (envelope[1] as SerializedLogContainer).items.some(item => item.body === 'Log with context') diff --git a/dev-packages/e2e-tests/test-applications/effect-node/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/effect-3-node/tests/transactions.test.ts similarity index 88% rename from dev-packages/e2e-tests/test-applications/effect-node/tests/transactions.test.ts rename to dev-packages/e2e-tests/test-applications/effect-3-node/tests/transactions.test.ts index ed7a58fa28df..b9693b2af6df 100644 --- a/dev-packages/e2e-tests/test-applications/effect-node/tests/transactions.test.ts +++ b/dev-packages/e2e-tests/test-applications/effect-3-node/tests/transactions.test.ts @@ -2,7 +2,7 @@ import { expect, test } from '@playwright/test'; import { waitForTransaction } from '@sentry-internal/test-utils'; test('Sends an HTTP transaction', async ({ baseURL }) => { - const transactionEventPromise = waitForTransaction('effect-node', transactionEvent => { + const transactionEventPromise = waitForTransaction('effect-3-node', transactionEvent => { return transactionEvent?.transaction === 'http.server GET'; }); @@ -14,7 +14,7 @@ test('Sends an HTTP transaction', async ({ baseURL }) => { }); test('Sends transaction with manual Effect span', async ({ baseURL }) => { - const transactionEventPromise = waitForTransaction('effect-node', transactionEvent => { + const transactionEventPromise = waitForTransaction('effect-3-node', transactionEvent => { return ( transactionEvent?.transaction === 'http.server GET' && transactionEvent?.spans?.some(span => span.description === 'test-span') @@ -36,7 +36,7 @@ test('Sends transaction with manual Effect span', async ({ baseURL }) => { }); test('Sends Effect spans with correct parent-child structure', async ({ baseURL }) => { - const transactionEventPromise = waitForTransaction('effect-node', transactionEvent => { + const transactionEventPromise = waitForTransaction('effect-3-node', transactionEvent => { return ( transactionEvent?.transaction === 'http.server GET' && transactionEvent?.spans?.some(span => span.description === 'custom-effect-span') @@ -87,7 +87,7 @@ test('Sends Effect spans with correct parent-child structure', async ({ baseURL }); test('Sends transaction for error route', async ({ baseURL }) => { - const transactionEventPromise = waitForTransaction('effect-node', transactionEvent => { + const transactionEventPromise = waitForTransaction('effect-3-node', transactionEvent => { return transactionEvent?.transaction === 'http.server GET'; }); diff --git a/dev-packages/e2e-tests/test-applications/effect-node/tsconfig.json b/dev-packages/e2e-tests/test-applications/effect-3-node/tsconfig.json similarity index 100% rename from dev-packages/e2e-tests/test-applications/effect-node/tsconfig.json rename to dev-packages/e2e-tests/test-applications/effect-3-node/tsconfig.json From 6a2b804988ecbed3c0883082b1a9a6ecec50f65a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Peer=20St=C3=B6cklmair?= Date: Tue, 21 Apr 2026 09:44:35 +0200 Subject: [PATCH 2/2] feat(effect): Support v4 beta (#20394) This adds support to Effect v4, but also keeps the compatibility for v3. There is no way that we can unit test against v3, as the `devDependencies` need to use `effect@4` and an updated `@effect/vitest` version, which is not compatible with Effect v3 (this is added in The API for Effect v4 has changed a little, so there are safeguards to detect if it is v3 or v4 and uses the correct API. The good part is that for users nothing changed, so they still can use the same methods in their app as before (ofc, respecting the new Effect v4 API). Before (Effect v3): ```ts const SentryLive = Layer.mergeAll( Sentry.effectLayer({ dsn: '__DSN__', tracesSampleRate: 1.0, enableLogs: true, }), Layer.setTracer(Sentry.SentryEffectTracer), Logger.replace(Logger.defaultLogger, Sentry.SentryEffectLogger), Sentry.SentryEffectMetricsLayer, ); ``` After (Effect v4): ```js const SentryLive = Layer.mergeAll( Sentry.effectLayer({ dsn: '__DSN__', tracesSampleRate: 1.0, enableLogs: true, }), Layer.succeed(Tracer.Tracer, Sentry.SentryEffectTracer), Logger.layer([Sentry.SentryEffectLogger]), Sentry.SentryEffectMetricsLayer, ); ``` Both usages still work and are represented in the E2E tests. --- .../effect-4-browser/.gitignore | 28 +++ .../test-applications/effect-4-browser/.npmrc | 2 + .../effect-4-browser/build.mjs | 52 +++++ .../effect-4-browser/package.json | 43 ++++ .../effect-4-browser/playwright.config.mjs | 7 + .../effect-4-browser/public/index.html | 48 +++++ .../effect-4-browser/src/index.js | 96 +++++++++ .../effect-4-browser/start-event-proxy.mjs | 6 + .../effect-4-browser/tests/errors.test.ts | 56 +++++ .../effect-4-browser/tests/logs.test.ts | 116 ++++++++++ .../tests/transactions.test.ts | 120 +++++++++++ .../effect-4-browser/tsconfig.json | 19 ++ .../effect-4-node/.gitignore | 2 + .../test-applications/effect-4-node/.npmrc | 2 + .../effect-4-node/package.json | 29 +++ .../effect-4-node/playwright.config.mjs | 7 + .../effect-4-node/src/app.ts | 146 +++++++++++++ .../effect-4-node/start-event-proxy.mjs | 6 + .../effect-4-node/tests/errors.test.ts | 56 +++++ .../effect-4-node/tests/logs.test.ts | 96 +++++++++ .../effect-4-node/tests/transactions.test.ts | 99 +++++++++ .../effect-4-node/tsconfig.json | 14 ++ packages/effect/README.md | 48 ++++- packages/effect/package.json | 6 +- packages/effect/src/logger.ts | 24 ++- packages/effect/src/metrics.ts | 202 ++++++++++++------ packages/effect/src/tracer.ts | 106 ++++++++- packages/effect/test/layer.test.ts | 29 ++- packages/effect/test/logger.test.ts | 15 +- packages/effect/test/metrics.test.ts | 171 ++++++--------- packages/effect/test/tracer.test.ts | 45 ++-- yarn.lock | 142 ++++++++++-- 32 files changed, 1586 insertions(+), 252 deletions(-) create mode 100644 dev-packages/e2e-tests/test-applications/effect-4-browser/.gitignore create mode 100644 dev-packages/e2e-tests/test-applications/effect-4-browser/.npmrc create mode 100644 dev-packages/e2e-tests/test-applications/effect-4-browser/build.mjs create mode 100644 dev-packages/e2e-tests/test-applications/effect-4-browser/package.json create mode 100644 dev-packages/e2e-tests/test-applications/effect-4-browser/playwright.config.mjs create mode 100644 dev-packages/e2e-tests/test-applications/effect-4-browser/public/index.html create mode 100644 dev-packages/e2e-tests/test-applications/effect-4-browser/src/index.js create mode 100644 dev-packages/e2e-tests/test-applications/effect-4-browser/start-event-proxy.mjs create mode 100644 dev-packages/e2e-tests/test-applications/effect-4-browser/tests/errors.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/effect-4-browser/tests/logs.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/effect-4-browser/tests/transactions.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/effect-4-browser/tsconfig.json create mode 100644 dev-packages/e2e-tests/test-applications/effect-4-node/.gitignore create mode 100644 dev-packages/e2e-tests/test-applications/effect-4-node/.npmrc create mode 100644 dev-packages/e2e-tests/test-applications/effect-4-node/package.json create mode 100644 dev-packages/e2e-tests/test-applications/effect-4-node/playwright.config.mjs create mode 100644 dev-packages/e2e-tests/test-applications/effect-4-node/src/app.ts create mode 100644 dev-packages/e2e-tests/test-applications/effect-4-node/start-event-proxy.mjs create mode 100644 dev-packages/e2e-tests/test-applications/effect-4-node/tests/errors.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/effect-4-node/tests/logs.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/effect-4-node/tests/transactions.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/effect-4-node/tsconfig.json diff --git a/dev-packages/e2e-tests/test-applications/effect-4-browser/.gitignore b/dev-packages/e2e-tests/test-applications/effect-4-browser/.gitignore new file mode 100644 index 000000000000..bd66327c3b4a --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/effect-4-browser/.gitignore @@ -0,0 +1,28 @@ +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# production +/build +/dist + +# misc +.DS_Store +.env.local +.env.development.local +.env.test.local +.env.production.local + +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +/test-results/ +/playwright-report/ +/playwright/.cache/ + +!*.d.ts diff --git a/dev-packages/e2e-tests/test-applications/effect-4-browser/.npmrc b/dev-packages/e2e-tests/test-applications/effect-4-browser/.npmrc new file mode 100644 index 000000000000..070f80f05092 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/effect-4-browser/.npmrc @@ -0,0 +1,2 @@ +@sentry:registry=http://127.0.0.1:4873 +@sentry-internal:registry=http://127.0.0.1:4873 diff --git a/dev-packages/e2e-tests/test-applications/effect-4-browser/build.mjs b/dev-packages/e2e-tests/test-applications/effect-4-browser/build.mjs new file mode 100644 index 000000000000..63c63597d4fe --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/effect-4-browser/build.mjs @@ -0,0 +1,52 @@ +import * as path from 'path'; +import * as url from 'url'; +import HtmlWebpackPlugin from 'html-webpack-plugin'; +import TerserPlugin from 'terser-webpack-plugin'; +import webpack from 'webpack'; + +const __dirname = path.dirname(url.fileURLToPath(import.meta.url)); + +webpack( + { + entry: path.join(__dirname, 'src/index.js'), + output: { + path: path.join(__dirname, 'build'), + filename: 'app.js', + }, + optimization: { + minimize: true, + minimizer: [new TerserPlugin()], + }, + plugins: [ + new webpack.EnvironmentPlugin(['E2E_TEST_DSN']), + new HtmlWebpackPlugin({ + template: path.join(__dirname, 'public/index.html'), + }), + ], + performance: { + hints: false, + }, + mode: 'production', + }, + (err, stats) => { + if (err) { + console.error(err.stack || err); + if (err.details) { + console.error(err.details); + } + return; + } + + const info = stats.toJson(); + + if (stats.hasErrors()) { + console.error(info.errors); + process.exit(1); + } + + if (stats.hasWarnings()) { + console.warn(info.warnings); + process.exit(1); + } + }, +); diff --git a/dev-packages/e2e-tests/test-applications/effect-4-browser/package.json b/dev-packages/e2e-tests/test-applications/effect-4-browser/package.json new file mode 100644 index 000000000000..4baf797b1019 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/effect-4-browser/package.json @@ -0,0 +1,43 @@ +{ + "name": "effect-4-browser-test-app", + "version": "1.0.0", + "private": true, + "scripts": { + "start": "serve -s build", + "build": "node build.mjs", + "test": "playwright test", + "clean": "npx rimraf node_modules pnpm-lock.yaml", + "test:build": "pnpm install && pnpm build", + "test:assert": "pnpm test" + }, + "dependencies": { + "@sentry/effect": "latest || *", + "@types/node": "^18.19.1", + "effect": "^4.0.0-beta.50", + "typescript": "~5.0.0" + }, + "devDependencies": { + "@playwright/test": "~1.56.0", + "@sentry-internal/test-utils": "link:../../../test-utils", + "webpack": "^5.91.0", + "serve": "14.0.1", + "terser-webpack-plugin": "^5.3.10", + "html-webpack-plugin": "^5.6.0" + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + }, + "volta": { + "node": "22.15.0", + "extends": "../../package.json" + } +} diff --git a/dev-packages/e2e-tests/test-applications/effect-4-browser/playwright.config.mjs b/dev-packages/e2e-tests/test-applications/effect-4-browser/playwright.config.mjs new file mode 100644 index 000000000000..31f2b913b58b --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/effect-4-browser/playwright.config.mjs @@ -0,0 +1,7 @@ +import { getPlaywrightConfig } from '@sentry-internal/test-utils'; + +const config = getPlaywrightConfig({ + startCommand: `pnpm start`, +}); + +export default config; diff --git a/dev-packages/e2e-tests/test-applications/effect-4-browser/public/index.html b/dev-packages/e2e-tests/test-applications/effect-4-browser/public/index.html new file mode 100644 index 000000000000..19d5c3d99a2f --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/effect-4-browser/public/index.html @@ -0,0 +1,48 @@ + + + + + + Effect Browser App + + +

Effect Browser E2E Test

+ +
+
+

Error Tests

+ +
+ +
+

Effect Span Tests

+ + +
+ +
+

Effect Failure Tests

+ + +
+ + +
+ +
+

Log Tests

+ + +
+ + +
+ + +
+ + diff --git a/dev-packages/e2e-tests/test-applications/effect-4-browser/src/index.js b/dev-packages/e2e-tests/test-applications/effect-4-browser/src/index.js new file mode 100644 index 000000000000..1748b4200ce1 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/effect-4-browser/src/index.js @@ -0,0 +1,96 @@ +// @ts-check +import * as Sentry from '@sentry/effect'; +import * as Logger from 'effect/Logger'; +import * as Layer from 'effect/Layer'; +import * as ManagedRuntime from 'effect/ManagedRuntime'; +import * as Tracer from 'effect/Tracer'; +import * as References from 'effect/References'; +import * as Effect from 'effect/Effect'; + +const AppLayer = Layer.mergeAll( + Sentry.effectLayer({ + dsn: process.env.E2E_TEST_DSN, + integrations: [ + Sentry.browserTracingIntegration({ + _experiments: { enableInteractions: true }, + }), + ], + tracesSampleRate: 1.0, + release: 'e2e-test', + environment: 'qa', + tunnel: 'http://localhost:3031', + enableLogs: true, + }), + Logger.layer([Sentry.SentryEffectLogger]), + Layer.succeed(Tracer.Tracer, Sentry.SentryEffectTracer), + Layer.succeed(References.MinimumLogLevel, 'Debug'), +); + +// v4 pattern: ManagedRuntime creates a long-lived runtime from the layer +const runtime = ManagedRuntime.make(AppLayer); + +// Force layer to build immediately (synchronously) so Sentry initializes at page load +Effect.runSync(runtime.contextEffect); + +const runEffect = fn => runtime.runPromise(fn()); + +document.getElementById('exception-button')?.addEventListener('click', () => { + throw new Error('I am an error!'); +}); + +document.getElementById('effect-span-button')?.addEventListener('click', async () => { + await runEffect(() => + Effect.gen(function* () { + yield* Effect.sleep('50 millis'); + yield* Effect.sleep('25 millis').pipe(Effect.withSpan('nested-span')); + }).pipe(Effect.withSpan('custom-effect-span', { kind: 'internal' })), + ); + const el = document.getElementById('effect-span-result'); + if (el) el.textContent = 'Span sent!'; +}); + +document.getElementById('effect-fail-button')?.addEventListener('click', async () => { + try { + await runEffect(() => Effect.fail(new Error('Effect failure'))); + } catch { + const el = document.getElementById('effect-fail-result'); + if (el) el.textContent = 'Effect failed (expected)'; + } +}); + +document.getElementById('effect-die-button')?.addEventListener('click', async () => { + try { + await runEffect(() => Effect.die('Effect defect')); + } catch { + const el = document.getElementById('effect-die-result'); + if (el) el.textContent = 'Effect died (expected)'; + } +}); + +document.getElementById('log-button')?.addEventListener('click', async () => { + await runEffect(() => + Effect.gen(function* () { + yield* Effect.logDebug('Debug log from Effect'); + yield* Effect.logInfo('Info log from Effect'); + yield* Effect.logWarning('Warning log from Effect'); + yield* Effect.logError('Error log from Effect'); + }), + ); + const el = document.getElementById('log-result'); + if (el) el.textContent = 'Logs sent!'; +}); + +document.getElementById('log-context-button')?.addEventListener('click', async () => { + await runEffect(() => + Effect.logInfo('Log with context').pipe( + Effect.annotateLogs('userId', '12345'), + Effect.annotateLogs('action', 'test'), + ), + ); + const el = document.getElementById('log-context-result'); + if (el) el.textContent = 'Log with context sent!'; +}); + +document.getElementById('navigation-link')?.addEventListener('click', () => { + document.getElementById('navigation-target')?.scrollIntoView({ behavior: 'smooth' }); +}); diff --git a/dev-packages/e2e-tests/test-applications/effect-4-browser/start-event-proxy.mjs b/dev-packages/e2e-tests/test-applications/effect-4-browser/start-event-proxy.mjs new file mode 100644 index 000000000000..04374ed614c6 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/effect-4-browser/start-event-proxy.mjs @@ -0,0 +1,6 @@ +import { startEventProxyServer } from '@sentry-internal/test-utils'; + +startEventProxyServer({ + port: 3031, + proxyServerName: 'effect-4-browser', +}); diff --git a/dev-packages/e2e-tests/test-applications/effect-4-browser/tests/errors.test.ts b/dev-packages/e2e-tests/test-applications/effect-4-browser/tests/errors.test.ts new file mode 100644 index 000000000000..25b5762390ad --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/effect-4-browser/tests/errors.test.ts @@ -0,0 +1,56 @@ +import { expect, test } from '@playwright/test'; +import { waitForError, waitForTransaction } from '@sentry-internal/test-utils'; + +test('captures an error', async ({ page }) => { + const errorEventPromise = waitForError('effect-4-browser', event => { + return !event.type && event.exception?.values?.[0]?.value === 'I am an error!'; + }); + + await page.goto('/'); + + const exceptionButton = page.locator('id=exception-button'); + await exceptionButton.click(); + + const errorEvent = await errorEventPromise; + + expect(errorEvent.exception?.values).toHaveLength(1); + expect(errorEvent.exception?.values?.[0]?.value).toBe('I am an error!'); + expect(errorEvent.transaction).toBe('/'); + + expect(errorEvent.request).toEqual({ + url: 'http://localhost:3030/', + headers: expect.any(Object), + }); + + expect(errorEvent.contexts?.trace).toEqual({ + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + span_id: expect.stringMatching(/[a-f0-9]{16}/), + }); +}); + +test('sets correct transactionName', async ({ page }) => { + const transactionPromise = waitForTransaction('effect-4-browser', async transactionEvent => { + return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'pageload'; + }); + + const errorEventPromise = waitForError('effect-4-browser', event => { + return !event.type && event.exception?.values?.[0]?.value === 'I am an error!'; + }); + + await page.goto('/'); + const transactionEvent = await transactionPromise; + + const exceptionButton = page.locator('id=exception-button'); + await exceptionButton.click(); + + const errorEvent = await errorEventPromise; + + expect(errorEvent.exception?.values).toHaveLength(1); + expect(errorEvent.exception?.values?.[0]?.value).toBe('I am an error!'); + expect(errorEvent.transaction).toEqual('/'); + + expect(errorEvent.contexts?.trace).toEqual({ + trace_id: transactionEvent.contexts?.trace?.trace_id, + span_id: expect.not.stringContaining(transactionEvent.contexts?.trace?.span_id || ''), + }); +}); diff --git a/dev-packages/e2e-tests/test-applications/effect-4-browser/tests/logs.test.ts b/dev-packages/e2e-tests/test-applications/effect-4-browser/tests/logs.test.ts new file mode 100644 index 000000000000..1026ed4ceeca --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/effect-4-browser/tests/logs.test.ts @@ -0,0 +1,116 @@ +import { expect, test } from '@playwright/test'; +import { waitForEnvelopeItem } from '@sentry-internal/test-utils'; +import type { SerializedLogContainer } from '@sentry/core'; + +test('should send Effect debug logs', async ({ page }) => { + const logEnvelopePromise = waitForEnvelopeItem('effect-4-browser', envelope => { + return ( + envelope[0].type === 'log' && + (envelope[1] as SerializedLogContainer).items.some( + item => item.level === 'debug' && item.body === 'Debug log from Effect', + ) + ); + }); + + await page.goto('/'); + const logButton = page.locator('id=log-button'); + await logButton.click(); + + await expect(page.locator('id=log-result')).toHaveText('Logs sent!'); + + const logEnvelope = await logEnvelopePromise; + const logs = (logEnvelope[1] as SerializedLogContainer).items; + const debugLog = logs.find(log => log.level === 'debug' && log.body === 'Debug log from Effect'); + expect(debugLog).toBeDefined(); + expect(debugLog?.level).toBe('debug'); +}); + +test('should send Effect info logs', async ({ page }) => { + const logEnvelopePromise = waitForEnvelopeItem('effect-4-browser', envelope => { + return ( + envelope[0].type === 'log' && + (envelope[1] as SerializedLogContainer).items.some( + item => item.level === 'info' && item.body === 'Info log from Effect', + ) + ); + }); + + await page.goto('/'); + const logButton = page.locator('id=log-button'); + await logButton.click(); + + await expect(page.locator('id=log-result')).toHaveText('Logs sent!'); + + const logEnvelope = await logEnvelopePromise; + const logs = (logEnvelope[1] as SerializedLogContainer).items; + const infoLog = logs.find(log => log.level === 'info' && log.body === 'Info log from Effect'); + expect(infoLog).toBeDefined(); + expect(infoLog?.level).toBe('info'); +}); + +test('should send Effect warning logs', async ({ page }) => { + const logEnvelopePromise = waitForEnvelopeItem('effect-4-browser', envelope => { + return ( + envelope[0].type === 'log' && + (envelope[1] as SerializedLogContainer).items.some( + item => item.level === 'warn' && item.body === 'Warning log from Effect', + ) + ); + }); + + await page.goto('/'); + const logButton = page.locator('id=log-button'); + await logButton.click(); + + await expect(page.locator('id=log-result')).toHaveText('Logs sent!'); + + const logEnvelope = await logEnvelopePromise; + const logs = (logEnvelope[1] as SerializedLogContainer).items; + const warnLog = logs.find(log => log.level === 'warn' && log.body === 'Warning log from Effect'); + expect(warnLog).toBeDefined(); + expect(warnLog?.level).toBe('warn'); +}); + +test('should send Effect error logs', async ({ page }) => { + const logEnvelopePromise = waitForEnvelopeItem('effect-4-browser', envelope => { + return ( + envelope[0].type === 'log' && + (envelope[1] as SerializedLogContainer).items.some( + item => item.level === 'error' && item.body === 'Error log from Effect', + ) + ); + }); + + await page.goto('/'); + const logButton = page.locator('id=log-button'); + await logButton.click(); + + await expect(page.locator('id=log-result')).toHaveText('Logs sent!'); + + const logEnvelope = await logEnvelopePromise; + const logs = (logEnvelope[1] as SerializedLogContainer).items; + const errorLog = logs.find(log => log.level === 'error' && log.body === 'Error log from Effect'); + expect(errorLog).toBeDefined(); + expect(errorLog?.level).toBe('error'); +}); + +test('should send Effect logs with context attributes', async ({ page }) => { + const logEnvelopePromise = waitForEnvelopeItem('effect-4-browser', envelope => { + return ( + envelope[0].type === 'log' && + (envelope[1] as SerializedLogContainer).items.some(item => item.body === 'Log with context') + ); + }); + + await page.goto('/'); + const logContextButton = page.locator('id=log-context-button'); + await logContextButton.click(); + + await expect(page.locator('id=log-context-result')).toHaveText('Log with context sent!'); + + const logEnvelope = await logEnvelopePromise; + const logs = (logEnvelope[1] as SerializedLogContainer).items; + const contextLog = logs.find(log => log.body === 'Log with context'); + expect(contextLog).toBeDefined(); + expect(contextLog?.level).toBe('info'); +}); diff --git a/dev-packages/e2e-tests/test-applications/effect-4-browser/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/effect-4-browser/tests/transactions.test.ts new file mode 100644 index 000000000000..6bec97ca4d79 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/effect-4-browser/tests/transactions.test.ts @@ -0,0 +1,120 @@ +import { expect, test } from '@playwright/test'; +import { waitForTransaction } from '@sentry-internal/test-utils'; + +test('captures a pageload transaction', async ({ page }) => { + const transactionPromise = waitForTransaction('effect-4-browser', async transactionEvent => { + return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'pageload'; + }); + + await page.goto('/'); + + const pageLoadTransaction = await transactionPromise; + + expect(pageLoadTransaction).toMatchObject({ + contexts: { + trace: { + data: expect.objectContaining({ + 'sentry.idle_span_finish_reason': 'idleTimeout', + 'sentry.op': 'pageload', + 'sentry.origin': 'auto.pageload.browser', + 'sentry.sample_rate': 1, + 'sentry.source': 'url', + }), + op: 'pageload', + origin: 'auto.pageload.browser', + span_id: expect.stringMatching(/[a-f0-9]{16}/), + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + }, + }, + environment: 'qa', + event_id: expect.stringMatching(/[a-f0-9]{32}/), + measurements: expect.any(Object), + platform: 'javascript', + release: 'e2e-test', + request: { + headers: { + 'User-Agent': expect.any(String), + }, + url: 'http://localhost:3030/', + }, + spans: expect.any(Array), + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + transaction: '/', + transaction_info: { + source: 'url', + }, + type: 'transaction', + }); +}); + +test('captures a navigation transaction', async ({ page }) => { + const pageLoadTransactionPromise = waitForTransaction('effect-4-browser', async transactionEvent => { + return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'pageload'; + }); + + const navigationTransactionPromise = waitForTransaction('effect-4-browser', async transactionEvent => { + return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'navigation'; + }); + + await page.goto('/'); + await pageLoadTransactionPromise; + + const linkElement = page.locator('id=navigation-link'); + await linkElement.click(); + + const navigationTransaction = await navigationTransactionPromise; + + expect(navigationTransaction).toMatchObject({ + contexts: { + trace: { + op: 'navigation', + origin: 'auto.navigation.browser', + }, + }, + transaction: '/', + transaction_info: { + source: 'url', + }, + }); +}); + +test('captures Effect spans with correct parent-child structure', async ({ page }) => { + const pageloadPromise = waitForTransaction('effect-4-browser', transactionEvent => { + return transactionEvent?.contexts?.trace?.op === 'pageload'; + }); + + const transactionPromise = waitForTransaction('effect-4-browser', transactionEvent => { + return ( + transactionEvent?.contexts?.trace?.op === 'ui.action.click' && + transactionEvent.spans?.some(span => span.description === 'custom-effect-span') + ); + }); + + await page.goto('/'); + await pageloadPromise; + + const effectSpanButton = page.locator('id=effect-span-button'); + await effectSpanButton.click(); + + await expect(page.locator('id=effect-span-result')).toHaveText('Span sent!'); + + const transactionEvent = await transactionPromise; + const spans = transactionEvent.spans || []; + + expect(spans).toContainEqual( + expect.objectContaining({ + description: 'custom-effect-span', + }), + ); + + expect(spans).toContainEqual( + expect.objectContaining({ + description: 'nested-span', + }), + ); + + const parentSpan = spans.find(s => s.description === 'custom-effect-span'); + const nestedSpan = spans.find(s => s.description === 'nested-span'); + expect(nestedSpan?.parent_span_id).toBe(parentSpan?.span_id); +}); diff --git a/dev-packages/e2e-tests/test-applications/effect-4-browser/tsconfig.json b/dev-packages/e2e-tests/test-applications/effect-4-browser/tsconfig.json new file mode 100644 index 000000000000..cb69f25b8d50 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/effect-4-browser/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "es2018", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "noFallthroughCasesInSwitch": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true + }, + "include": ["src", "tests"] +} diff --git a/dev-packages/e2e-tests/test-applications/effect-4-node/.gitignore b/dev-packages/e2e-tests/test-applications/effect-4-node/.gitignore new file mode 100644 index 000000000000..f06235c460c2 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/effect-4-node/.gitignore @@ -0,0 +1,2 @@ +node_modules +dist diff --git a/dev-packages/e2e-tests/test-applications/effect-4-node/.npmrc b/dev-packages/e2e-tests/test-applications/effect-4-node/.npmrc new file mode 100644 index 000000000000..070f80f05092 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/effect-4-node/.npmrc @@ -0,0 +1,2 @@ +@sentry:registry=http://127.0.0.1:4873 +@sentry-internal:registry=http://127.0.0.1:4873 diff --git a/dev-packages/e2e-tests/test-applications/effect-4-node/package.json b/dev-packages/e2e-tests/test-applications/effect-4-node/package.json new file mode 100644 index 000000000000..31ebb8b1ba53 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/effect-4-node/package.json @@ -0,0 +1,29 @@ +{ + "name": "effect-4-node-app", + "version": "1.0.0", + "private": true, + "type": "module", + "scripts": { + "build": "tsc", + "start": "node dist/app.js", + "test": "playwright test", + "clean": "npx rimraf node_modules pnpm-lock.yaml", + "test:build": "pnpm install && pnpm build", + "test:assert": "pnpm test" + }, + "dependencies": { + "@effect/platform-node": "^4.0.0-beta.50", + "@sentry/effect": "latest || *", + "@types/node": "^18.19.1", + "effect": "^4.0.0-beta.50", + "typescript": "~5.0.0" + }, + "devDependencies": { + "@playwright/test": "~1.56.0", + "@sentry-internal/test-utils": "link:../../../test-utils" + }, + "volta": { + "node": "22.15.0", + "extends": "../../package.json" + } +} diff --git a/dev-packages/e2e-tests/test-applications/effect-4-node/playwright.config.mjs b/dev-packages/e2e-tests/test-applications/effect-4-node/playwright.config.mjs new file mode 100644 index 000000000000..31f2b913b58b --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/effect-4-node/playwright.config.mjs @@ -0,0 +1,7 @@ +import { getPlaywrightConfig } from '@sentry-internal/test-utils'; + +const config = getPlaywrightConfig({ + startCommand: `pnpm start`, +}); + +export default config; diff --git a/dev-packages/e2e-tests/test-applications/effect-4-node/src/app.ts b/dev-packages/e2e-tests/test-applications/effect-4-node/src/app.ts new file mode 100644 index 000000000000..5ebfef33be77 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/effect-4-node/src/app.ts @@ -0,0 +1,146 @@ +import * as Sentry from '@sentry/effect'; +import { NodeHttpServer, NodeRuntime } from '@effect/platform-node'; +import * as Effect from 'effect/Effect'; +import * as Cause from 'effect/Cause'; +import * as Layer from 'effect/Layer'; +import * as Logger from 'effect/Logger'; +import * as Tracer from 'effect/Tracer'; +import * as References from 'effect/References'; +import { HttpRouter, HttpServerResponse } from 'effect/unstable/http'; +import { createServer } from 'http'; + +const SentryLive = Layer.mergeAll( + Sentry.effectLayer({ + dsn: process.env.E2E_TEST_DSN, + environment: 'qa', + debug: !!process.env.DEBUG, + tunnel: 'http://localhost:3031/', + tracesSampleRate: 1, + enableLogs: true, + }), + Logger.layer([Sentry.SentryEffectLogger]), + Layer.succeed(Tracer.Tracer, Sentry.SentryEffectTracer), + Layer.succeed(References.MinimumLogLevel, 'Debug'), +); + +const Routes = Layer.mergeAll( + HttpRouter.add('GET', '/test-success', HttpServerResponse.json({ version: 'v1' })), + + HttpRouter.add( + 'GET', + '/test-transaction', + Effect.gen(function* () { + yield* Effect.void.pipe(Effect.withSpan('test-span')); + return yield* HttpServerResponse.json({ status: 'ok' }); + }), + ), + + HttpRouter.add( + 'GET', + '/test-effect-span', + Effect.gen(function* () { + yield* Effect.gen(function* () { + yield* Effect.sleep('50 millis'); + yield* Effect.sleep('25 millis').pipe(Effect.withSpan('nested-span')); + }).pipe(Effect.withSpan('custom-effect-span', { kind: 'internal' })); + return yield* HttpServerResponse.json({ status: 'ok' }); + }), + ), + + HttpRouter.add( + 'GET', + '/test-error', + Effect.gen(function* () { + const exceptionId = Sentry.captureException(new Error('This is an error')); + yield* Effect.promise(() => Sentry.flush(2000)); + return yield* HttpServerResponse.json({ exceptionId }); + }), + ), + + HttpRouter.add( + 'GET', + '/test-exception/:id', + Effect.gen(function* () { + yield* Effect.sync(() => { + throw new Error('This is an exception with id 123'); + }); + return HttpServerResponse.empty(); + }).pipe( + Effect.catchCause(cause => { + const error = Cause.squash(cause); + Sentry.captureException(error); + return Effect.gen(function* () { + yield* Effect.promise(() => Sentry.flush(2000)); + return yield* HttpServerResponse.json({ error: String(error) }, { status: 500 }); + }); + }), + ), + ), + + HttpRouter.add( + 'GET', + '/test-effect-fail', + Effect.gen(function* () { + yield* Effect.fail(new Error('Effect failure')); + return HttpServerResponse.empty(); + }).pipe( + Effect.catchCause(cause => { + const error = Cause.squash(cause); + Sentry.captureException(error); + return Effect.gen(function* () { + yield* Effect.promise(() => Sentry.flush(2000)); + return yield* HttpServerResponse.json({ error: String(error) }, { status: 500 }); + }); + }), + ), + ), + + HttpRouter.add( + 'GET', + '/test-effect-die', + Effect.gen(function* () { + yield* Effect.die('Effect defect'); + return HttpServerResponse.empty(); + }).pipe( + Effect.catchCause(cause => { + const error = Cause.squash(cause); + Sentry.captureException(error); + return Effect.gen(function* () { + yield* Effect.promise(() => Sentry.flush(2000)); + return yield* HttpServerResponse.json({ error: String(error) }, { status: 500 }); + }); + }), + ), + ), + + HttpRouter.add( + 'GET', + '/test-log', + Effect.gen(function* () { + yield* Effect.logDebug('Debug log from Effect'); + yield* Effect.logInfo('Info log from Effect'); + yield* Effect.logWarning('Warning log from Effect'); + yield* Effect.logError('Error log from Effect'); + return yield* HttpServerResponse.json({ message: 'Logs sent' }); + }), + ), + + HttpRouter.add( + 'GET', + '/test-log-with-context', + Effect.gen(function* () { + yield* Effect.logInfo('Log with context').pipe( + Effect.annotateLogs('userId', '12345'), + Effect.annotateLogs('action', 'test'), + ); + return yield* HttpServerResponse.json({ message: 'Log with context sent' }); + }), + ), +); + +const HttpLive = HttpRouter.serve(Routes).pipe( + Layer.provide(NodeHttpServer.layer(() => createServer(), { port: 3030 })), + Layer.provide(SentryLive), +); + +NodeRuntime.runMain(Layer.launch(HttpLive)); diff --git a/dev-packages/e2e-tests/test-applications/effect-4-node/start-event-proxy.mjs b/dev-packages/e2e-tests/test-applications/effect-4-node/start-event-proxy.mjs new file mode 100644 index 000000000000..6874b711993a --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/effect-4-node/start-event-proxy.mjs @@ -0,0 +1,6 @@ +import { startEventProxyServer } from '@sentry-internal/test-utils'; + +startEventProxyServer({ + port: 3031, + proxyServerName: 'effect-4-node', +}); diff --git a/dev-packages/e2e-tests/test-applications/effect-4-node/tests/errors.test.ts b/dev-packages/e2e-tests/test-applications/effect-4-node/tests/errors.test.ts new file mode 100644 index 000000000000..f4d01534e60f --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/effect-4-node/tests/errors.test.ts @@ -0,0 +1,56 @@ +import { expect, test } from '@playwright/test'; +import { waitForError } from '@sentry-internal/test-utils'; + +test('Captures manually reported error', async ({ baseURL }) => { + const errorEventPromise = waitForError('effect-4-node', event => { + return !event.type && event.exception?.values?.[0]?.value === 'This is an error'; + }); + + const response = await fetch(`${baseURL}/test-error`); + const body = await response.json(); + + const errorEvent = await errorEventPromise; + + expect(body.exceptionId).toBeDefined(); + expect(errorEvent.exception?.values).toHaveLength(1); + expect(errorEvent.exception?.values?.[0]?.value).toBe('This is an error'); +}); + +test('Captures thrown exception', async ({ baseURL }) => { + const errorEventPromise = waitForError('effect-4-node', event => { + return !event.type && event.exception?.values?.[0]?.value === 'This is an exception with id 123'; + }); + + await fetch(`${baseURL}/test-exception/123`); + + const errorEvent = await errorEventPromise; + + expect(errorEvent.exception?.values).toHaveLength(1); + expect(errorEvent.exception?.values?.[0]?.value).toBe('This is an exception with id 123'); +}); + +test('Captures Effect.fail as error', async ({ baseURL }) => { + const errorEventPromise = waitForError('effect-4-node', event => { + return !event.type && event.exception?.values?.[0]?.value === 'Effect failure'; + }); + + await fetch(`${baseURL}/test-effect-fail`); + + const errorEvent = await errorEventPromise; + + expect(errorEvent.exception?.values).toHaveLength(1); + expect(errorEvent.exception?.values?.[0]?.value).toBe('Effect failure'); +}); + +test('Captures Effect.die as error', async ({ baseURL }) => { + const errorEventPromise = waitForError('effect-4-node', event => { + return !event.type && event.exception?.values?.[0]?.value?.includes('Effect defect'); + }); + + await fetch(`${baseURL}/test-effect-die`); + + const errorEvent = await errorEventPromise; + + expect(errorEvent.exception?.values).toHaveLength(1); + expect(errorEvent.exception?.values?.[0]?.value).toContain('Effect defect'); +}); diff --git a/dev-packages/e2e-tests/test-applications/effect-4-node/tests/logs.test.ts b/dev-packages/e2e-tests/test-applications/effect-4-node/tests/logs.test.ts new file mode 100644 index 000000000000..f7563576ad75 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/effect-4-node/tests/logs.test.ts @@ -0,0 +1,96 @@ +import { expect, test } from '@playwright/test'; +import { waitForEnvelopeItem } from '@sentry-internal/test-utils'; +import type { SerializedLogContainer } from '@sentry/core'; + +test('should send Effect debug logs', async ({ baseURL }) => { + const logEnvelopePromise = waitForEnvelopeItem('effect-4-node', envelope => { + return ( + envelope[0].type === 'log' && + (envelope[1] as SerializedLogContainer).items.some( + item => item.level === 'debug' && item.body === 'Debug log from Effect', + ) + ); + }); + + await fetch(`${baseURL}/test-log`); + + const logEnvelope = await logEnvelopePromise; + const logs = (logEnvelope[1] as SerializedLogContainer).items; + const debugLog = logs.find(log => log.level === 'debug' && log.body === 'Debug log from Effect'); + expect(debugLog).toBeDefined(); + expect(debugLog?.level).toBe('debug'); +}); + +test('should send Effect info logs', async ({ baseURL }) => { + const logEnvelopePromise = waitForEnvelopeItem('effect-4-node', envelope => { + return ( + envelope[0].type === 'log' && + (envelope[1] as SerializedLogContainer).items.some( + item => item.level === 'info' && item.body === 'Info log from Effect', + ) + ); + }); + + await fetch(`${baseURL}/test-log`); + + const logEnvelope = await logEnvelopePromise; + const logs = (logEnvelope[1] as SerializedLogContainer).items; + const infoLog = logs.find(log => log.level === 'info' && log.body === 'Info log from Effect'); + expect(infoLog).toBeDefined(); + expect(infoLog?.level).toBe('info'); +}); + +test('should send Effect warning logs', async ({ baseURL }) => { + const logEnvelopePromise = waitForEnvelopeItem('effect-4-node', envelope => { + return ( + envelope[0].type === 'log' && + (envelope[1] as SerializedLogContainer).items.some( + item => item.level === 'warn' && item.body === 'Warning log from Effect', + ) + ); + }); + + await fetch(`${baseURL}/test-log`); + + const logEnvelope = await logEnvelopePromise; + const logs = (logEnvelope[1] as SerializedLogContainer).items; + const warnLog = logs.find(log => log.level === 'warn' && log.body === 'Warning log from Effect'); + expect(warnLog).toBeDefined(); + expect(warnLog?.level).toBe('warn'); +}); + +test('should send Effect error logs', async ({ baseURL }) => { + const logEnvelopePromise = waitForEnvelopeItem('effect-4-node', envelope => { + return ( + envelope[0].type === 'log' && + (envelope[1] as SerializedLogContainer).items.some( + item => item.level === 'error' && item.body === 'Error log from Effect', + ) + ); + }); + + await fetch(`${baseURL}/test-log`); + + const logEnvelope = await logEnvelopePromise; + const logs = (logEnvelope[1] as SerializedLogContainer).items; + const errorLog = logs.find(log => log.level === 'error' && log.body === 'Error log from Effect'); + expect(errorLog).toBeDefined(); + expect(errorLog?.level).toBe('error'); +}); + +test('should send Effect logs with context attributes', async ({ baseURL }) => { + const logEnvelopePromise = waitForEnvelopeItem('effect-4-node', envelope => { + return ( + envelope[0].type === 'log' && + (envelope[1] as SerializedLogContainer).items.some(item => item.body === 'Log with context') + ); + }); + + await fetch(`${baseURL}/test-log-with-context`); + + const logEnvelope = await logEnvelopePromise; + const logs = (logEnvelope[1] as SerializedLogContainer).items; + const contextLog = logs.find(log => log.body === 'Log with context'); + expect(contextLog).toBeDefined(); + expect(contextLog?.level).toBe('info'); +}); diff --git a/dev-packages/e2e-tests/test-applications/effect-4-node/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/effect-4-node/tests/transactions.test.ts new file mode 100644 index 000000000000..5aeaf9b2a8ba --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/effect-4-node/tests/transactions.test.ts @@ -0,0 +1,99 @@ +import { expect, test } from '@playwright/test'; +import { waitForTransaction } from '@sentry-internal/test-utils'; + +test('Sends an HTTP transaction', async ({ baseURL }) => { + const transactionEventPromise = waitForTransaction('effect-4-node', transactionEvent => { + return transactionEvent?.transaction === 'http.server GET'; + }); + + await fetch(`${baseURL}/test-success`); + + const transactionEvent = await transactionEventPromise; + + expect(transactionEvent.transaction).toBe('http.server GET'); +}); + +test('Sends transaction with manual Effect span', async ({ baseURL }) => { + const transactionEventPromise = waitForTransaction('effect-4-node', transactionEvent => { + return ( + transactionEvent?.transaction === 'http.server GET' && + transactionEvent?.spans?.some(span => span.description === 'test-span') + ); + }); + + await fetch(`${baseURL}/test-transaction`); + + const transactionEvent = await transactionEventPromise; + + expect(transactionEvent.transaction).toBe('http.server GET'); + + const spans = transactionEvent.spans || []; + expect(spans).toEqual([ + expect.objectContaining({ + description: 'test-span', + }), + ]); +}); + +test('Sends Effect spans with correct parent-child structure', async ({ baseURL }) => { + const transactionEventPromise = waitForTransaction('effect-4-node', transactionEvent => { + return ( + transactionEvent?.transaction === 'http.server GET' && + transactionEvent?.spans?.some(span => span.description === 'custom-effect-span') + ); + }); + + await fetch(`${baseURL}/test-effect-span`); + + const transactionEvent = await transactionEventPromise; + + expect(transactionEvent.transaction).toBe('http.server GET'); + + expect(transactionEvent).toEqual( + expect.objectContaining({ + contexts: expect.objectContaining({ + trace: expect.objectContaining({ + origin: 'auto.http.effect', + }), + }), + spans: [ + expect.objectContaining({ + description: 'custom-effect-span', + origin: 'auto.function.effect', + }), + expect.objectContaining({ + description: 'nested-span', + origin: 'auto.function.effect', + }), + ], + sdk: expect.objectContaining({ + name: 'sentry.javascript.effect', + packages: [ + expect.objectContaining({ + name: 'npm:@sentry/effect', + }), + expect.objectContaining({ + name: 'npm:@sentry/node-light', + }), + ], + }), + }), + ); + + const parentSpan = transactionEvent.spans?.[0]?.span_id; + const nestedSpan = transactionEvent.spans?.[1]?.parent_span_id; + + expect(nestedSpan).toBe(parentSpan); +}); + +test('Sends transaction for error route', async ({ baseURL }) => { + const transactionEventPromise = waitForTransaction('effect-4-node', transactionEvent => { + return transactionEvent?.transaction === 'http.server GET'; + }); + + await fetch(`${baseURL}/test-error`); + + const transactionEvent = await transactionEventPromise; + + expect(transactionEvent.transaction).toBe('http.server GET'); +}); diff --git a/dev-packages/e2e-tests/test-applications/effect-4-node/tsconfig.json b/dev-packages/e2e-tests/test-applications/effect-4-node/tsconfig.json new file mode 100644 index 000000000000..2cc9aca23e0e --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/effect-4-node/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "outDir": "dist", + "rootDir": "src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "declaration": false + }, + "include": ["src"] +} diff --git a/packages/effect/README.md b/packages/effect/README.md index 78b2f6471dc0..bfe3c51ce8dc 100644 --- a/packages/effect/README.md +++ b/packages/effect/README.md @@ -6,11 +6,16 @@ > NOTICE: This package is in alpha state and may be subject to breaking changes. +`@sentry/effect` supports both Effect v3 and Effect v4 (beta). The integration +auto-detects the installed Effect version at runtime, but the layer composition +APIs differ between the two major versions, so the setup code is slightly +different. + ## Getting Started This SDK does not have docs yet. Stay tuned. -## Usage +## Usage with Effect v3 ```typescript import * as Sentry from '@sentry/effect/server'; @@ -33,16 +38,45 @@ const MainLive = HttpLive.pipe(Layer.provide(SentryLive)); MainLive.pipe(Layer.launch, NodeRuntime.runMain); ``` -The `effectLayer` function initializes Sentry. To enable Effect instrumentation, compose with: +## Usage with Effect v4 -- `Layer.setTracer(Sentry.SentryEffectTracer)` - Effect spans traced as Sentry spans -- `Logger.replace(Logger.defaultLogger, Sentry.SentryEffectLogger)` - Effect logs forwarded to Sentry -- `Sentry.SentryEffectMetricsLayer` - Effect metrics sent to Sentry +Effect v4 reorganized the `Tracer` and `Logger` layer APIs, so the wiring looks +slightly different. The `effectLayer`, `SentryEffectTracer`, +`SentryEffectLogger`, and `SentryEffectMetricsLayer` exports themselves are the +same. -## Links +```typescript +import * as Sentry from '@sentry/effect/server'; +import { NodeHttpServer, NodeRuntime } from '@effect/platform-node'; +import * as Layer from 'effect/Layer'; +import * as Logger from 'effect/Logger'; +import * as Tracer from 'effect/Tracer'; +import { HttpRouter } from 'effect/unstable/http'; +import { createServer } from 'http'; +import { Routes } from './Routes.js'; + +const SentryLive = Layer.mergeAll( + Sentry.effectLayer({ + dsn: '__DSN__', + tracesSampleRate: 1.0, + enableLogs: true, + }), + Layer.succeed(Tracer.Tracer, Sentry.SentryEffectTracer), + Logger.layer([Sentry.SentryEffectLogger]), + Sentry.SentryEffectMetricsLayer, +); - +const HttpLive = HttpRouter.serve(Routes).pipe( + Layer.provide(NodeHttpServer.layer(() => createServer(), { port: 3030 })), + Layer.provide(SentryLive), +); + +NodeRuntime.runMain(Layer.launch(HttpLive)); +``` + +## Links +- [Official SDK Docs](https://docs.sentry.io/platforms/javascript/guides/effect/) - [Sentry.io](https://sentry.io/?utm_source=github&utm_medium=npm_effect) - [Sentry Discord Server](https://discord.gg/Ww9hbqr) - [Stack Overflow](https://stackoverflow.com/questions/tagged/sentry) diff --git a/packages/effect/package.json b/packages/effect/package.json index 412f884eca1a..669630577640 100644 --- a/packages/effect/package.json +++ b/packages/effect/package.json @@ -62,7 +62,7 @@ "@sentry/node-core": "10.49.0" }, "peerDependencies": { - "effect": "^3.0.0" + "effect": "^3.0.0 || ^4.0.0-beta.50" }, "peerDependenciesMeta": { "effect": { @@ -70,8 +70,8 @@ } }, "devDependencies": { - "@effect/vitest": "^0.23.9", - "effect": "^3.21.0" + "@effect/vitest": "^4.0.0-beta.50", + "effect": "^4.0.0-beta.50" }, "scripts": { "build": "run-p build:transpile build:types", diff --git a/packages/effect/src/logger.ts b/packages/effect/src/logger.ts index 833f5b6b7e95..7421bebc0dbe 100644 --- a/packages/effect/src/logger.ts +++ b/packages/effect/src/logger.ts @@ -1,5 +1,20 @@ import { logger as sentryLogger } from '@sentry/core'; import * as Logger from 'effect/Logger'; +import type * as LogLevel from 'effect/LogLevel'; + +function getLogLevelTag(logLevel: LogLevel.LogLevel): LogLevel.LogLevel | 'Warning' { + // Effect v4: logLevel is a string literal directly + if (typeof logLevel === 'string') { + return logLevel; + } + + // Effect v3: logLevel has _tag property + if (logLevel && typeof logLevel === 'object' && '_tag' in logLevel) { + return (logLevel as { _tag: LogLevel.LogLevel })._tag; + } + + return 'Info'; +} /** * Effect Logger that sends logs to Sentry. @@ -15,14 +30,17 @@ export const SentryEffectLogger = Logger.make(({ logLevel, message }) => { msg = JSON.stringify(message); } - switch (logLevel._tag) { + const tag = getLogLevelTag(logLevel); + + switch (tag) { case 'Fatal': sentryLogger.fatal(msg); break; case 'Error': sentryLogger.error(msg); break; - case 'Warning': + case 'Warning': // Effect v3 + case 'Warn': // Effect v4 sentryLogger.warn(msg); break; case 'Info': @@ -38,6 +56,6 @@ export const SentryEffectLogger = Logger.make(({ logLevel, message }) => { case 'None': break; default: - logLevel satisfies never; + tag satisfies never; } }); diff --git a/packages/effect/src/metrics.ts b/packages/effect/src/metrics.ts index 82daf5e67a5d..764149009be5 100644 --- a/packages/effect/src/metrics.ts +++ b/packages/effect/src/metrics.ts @@ -1,66 +1,75 @@ import { metrics as sentryMetrics } from '@sentry/core'; +import * as Context from 'effect/Context'; import * as Effect from 'effect/Effect'; -import type * as Layer from 'effect/Layer'; -import { scopedDiscard } from 'effect/Layer'; +import * as Layer from 'effect/Layer'; import * as Metric from 'effect/Metric'; -import * as MetricKeyType from 'effect/MetricKeyType'; -import type * as MetricPair from 'effect/MetricPair'; -import * as MetricState from 'effect/MetricState'; import * as Schedule from 'effect/Schedule'; type MetricAttributes = Record; -function labelsToAttributes(labels: ReadonlyArray<{ key: string; value: string }>): MetricAttributes { - return labels.reduce((acc, label) => ({ ...acc, [label.key]: label.value }), {}); +// ============================================================================= +// Effect v3 Types (vendored - not exported from effect@3.x) +// ============================================================================= + +interface V3MetricLabel { + key: string; + value: string; } -function sendMetricToSentry(pair: MetricPair.MetricPair.Untyped): void { - const { metricKey, metricState } = pair; - const name = metricKey.name; - const attributes = labelsToAttributes(metricKey.tags); +interface V3MetricPair { + metricKey: { + name: string; + tags: ReadonlyArray; + keyType: { _tag: string }; + }; + metricState: { + count?: number | bigint; + value?: number; + sum?: number; + min?: number; + max?: number; + occurrences?: Map; + }; +} - if (MetricState.isCounterState(metricState)) { - const value = Number(metricState.count); - sentryMetrics.count(name, value, { attributes }); - } else if (MetricState.isGaugeState(metricState)) { - const value = Number(metricState.value); - sentryMetrics.gauge(name, value, { attributes }); - } else if (MetricState.isHistogramState(metricState)) { - sentryMetrics.gauge(`${name}.sum`, metricState.sum, { attributes }); - sentryMetrics.gauge(`${name}.count`, metricState.count, { attributes }); - sentryMetrics.gauge(`${name}.min`, metricState.min, { attributes }); - sentryMetrics.gauge(`${name}.max`, metricState.max, { attributes }); - } else if (MetricState.isSummaryState(metricState)) { - sentryMetrics.gauge(`${name}.sum`, metricState.sum, { attributes }); - sentryMetrics.gauge(`${name}.count`, metricState.count, { attributes }); - sentryMetrics.gauge(`${name}.min`, metricState.min, { attributes }); - sentryMetrics.gauge(`${name}.max`, metricState.max, { attributes }); - } else if (MetricState.isFrequencyState(metricState)) { - for (const [word, count] of metricState.occurrences) { - sentryMetrics.count(name, count, { - attributes: { ...attributes, word }, - }); - } - } +// Effect v3 `MetricState` implementations brand themselves with a `Symbol.for(...)` TypeId +// rather than a string `_tag`. We use these globally-registered symbols to classify state +// instances returned by `Metric.unsafeSnapshot()` without importing `effect/MetricState` +// (the module does not exist in Effect v4). +const V3_COUNTER_STATE_TYPE_ID = Symbol.for('effect/MetricState/Counter'); +const V3_GAUGE_STATE_TYPE_ID = Symbol.for('effect/MetricState/Gauge'); +const V3_HISTOGRAM_STATE_TYPE_ID = Symbol.for('effect/MetricState/Histogram'); +const V3_SUMMARY_STATE_TYPE_ID = Symbol.for('effect/MetricState/Summary'); +const V3_FREQUENCY_STATE_TYPE_ID = Symbol.for('effect/MetricState/Frequency'); + +function labelsToAttributes(labels: ReadonlyArray): MetricAttributes { + return labels.reduce((acc, label) => ({ ...acc, [label.key]: label.value }), {}); } -function getMetricId(pair: MetricPair.MetricPair.Untyped): string { +function getMetricIdV3(pair: V3MetricPair): string { const tags = pair.metricKey.tags.map(t => `${t.key}=${t.value}`).join(','); return `${pair.metricKey.name}:${tags}`; } -function sendDeltaMetricToSentry( - pair: MetricPair.MetricPair.Untyped, - previousCounterValues: Map, -): void { +function getMetricIdV4(snapshot: Metric.Metric.Snapshot): string { + const attrs = snapshot.attributes + ? Object.entries(snapshot.attributes) + .map(([k, v]) => `${k}=${v}`) + .join(',') + : ''; + return `${snapshot.id}:${attrs}`; +} + +function sendV3MetricToSentry(pair: V3MetricPair, previousCounterValues: Map): void { const { metricKey, metricState } = pair; const name = metricKey.name; const attributes = labelsToAttributes(metricKey.tags); - const metricId = getMetricId(pair); + const metricId = getMetricIdV3(pair); - if (MetricState.isCounterState(metricState)) { - const currentValue = Number(metricState.count); + const state = metricState as unknown as Record; + if (state[V3_COUNTER_STATE_TYPE_ID] !== undefined) { + const currentValue = Number(metricState.count); const previousValue = previousCounterValues.get(metricId) ?? 0; const delta = currentValue - previousValue; @@ -69,41 +78,92 @@ function sendDeltaMetricToSentry( } previousCounterValues.set(metricId, currentValue); - } else { - sendMetricToSentry(pair); + } else if (state[V3_GAUGE_STATE_TYPE_ID] !== undefined) { + const value = Number(metricState.value); + sentryMetrics.gauge(name, value, { attributes }); + } else if (state[V3_HISTOGRAM_STATE_TYPE_ID] !== undefined || state[V3_SUMMARY_STATE_TYPE_ID] !== undefined) { + sentryMetrics.gauge(`${name}.sum`, metricState.sum ?? 0, { attributes }); + sentryMetrics.gauge(`${name}.count`, Number(metricState.count ?? 0), { attributes }); + sentryMetrics.gauge(`${name}.min`, metricState.min ?? 0, { attributes }); + sentryMetrics.gauge(`${name}.max`, metricState.max ?? 0, { attributes }); + } else if (state[V3_FREQUENCY_STATE_TYPE_ID] !== undefined && metricState.occurrences) { + for (const [word, count] of metricState.occurrences) { + sentryMetrics.count(name, count, { + attributes: { ...attributes, word }, + }); + } } } -/** - * Flushes all Effect metrics to Sentry. - * @param previousCounterValues - Map tracking previous counter values for delta calculation - */ -function flushMetricsToSentry(previousCounterValues: Map): void { - const snapshot = Metric.unsafeSnapshot(); +function sendV4MetricToSentry(snapshot: Metric.Metric.Snapshot, previousCounterValues: Map): void { + const name = snapshot.id; + const attributes: MetricAttributes = snapshot.attributes ? { ...snapshot.attributes } : {}; + const metricId = getMetricIdV4(snapshot); + + switch (snapshot.type) { + case 'Counter': { + const currentValue = Number(snapshot.state.count); + const previousValue = previousCounterValues.get(metricId) ?? 0; + const delta = currentValue - previousValue; + + if (delta > 0) { + sentryMetrics.count(name, delta, { attributes }); + } - snapshot.forEach((pair: MetricPair.MetricPair.Untyped) => { - if (MetricKeyType.isCounterKey(pair.metricKey.keyType)) { - sendDeltaMetricToSentry(pair, previousCounterValues); - } else { - sendMetricToSentry(pair); + previousCounterValues.set(metricId, currentValue); + break; + } + case 'Gauge': { + const value = Number(snapshot.state.value); + sentryMetrics.gauge(name, value, { attributes }); + break; + } + case 'Histogram': + case 'Summary': { + sentryMetrics.gauge(`${name}.sum`, snapshot.state.sum ?? 0, { attributes }); + sentryMetrics.gauge(`${name}.count`, snapshot.state.count ?? 0, { attributes }); + sentryMetrics.gauge(`${name}.min`, snapshot.state.min ?? 0, { attributes }); + sentryMetrics.gauge(`${name}.max`, snapshot.state.max ?? 0, { attributes }); + break; + } + case 'Frequency': { + for (const [word, count] of snapshot.state.occurrences) { + sentryMetrics.count(name, count, { + attributes: { ...attributes, word }, + }); + } + break; } - }); + } } -/** - * Creates a metrics flusher with its own isolated state for delta tracking. - * Useful for testing scenarios where you need to control the lifecycle. - * @internal - */ -export function createMetricsFlusher(): { - flush: () => void; - clear: () => void; -} { - const previousCounterValues = new Map(); - return { - flush: () => flushMetricsToSentry(previousCounterValues), - clear: () => previousCounterValues.clear(), - }; +// ============================================================================= +// Effect v3 snapshot function type (vendored - not exported from effect@3.x) +// ============================================================================= + +type V3UnsafeSnapshotFn = () => ReadonlyArray; + +// Use bracket notation to avoid Webpack static analysis flagging missing exports +// This is important for Effect v3 compatibility. +const MetricModule = Metric; +const snapshotUnsafe = MetricModule['snapshotUnsafe'] as typeof Metric.snapshotUnsafe | undefined; +// @ts-expect-error - unsafeSnapshot is not exported from effect@3.x +const unsafeSnapshot = MetricModule['unsafeSnapshot'] as V3UnsafeSnapshotFn | undefined; + +function flushMetricsToSentry(previousCounterValues: Map): void { + if (snapshotUnsafe) { + // Effect v4 + const snapshots = snapshotUnsafe(Context.empty()); + for (const snapshot of snapshots) { + sendV4MetricToSentry(snapshot, previousCounterValues); + } + } else if (unsafeSnapshot) { + // Effect v3 + const snapshots = unsafeSnapshot(); + for (const pair of snapshots) { + sendV3MetricToSentry(pair, previousCounterValues); + } + } } function createMetricsReporterEffect(previousCounterValues: Map): Effect.Effect { @@ -120,7 +180,7 @@ function createMetricsReporterEffect(previousCounterValues: Map) * The layer manages its own state for delta counter calculations, * which is automatically cleaned up when the layer is finalized. */ -export const SentryEffectMetricsLayer: Layer.Layer = scopedDiscard( +export const SentryEffectMetricsLayer: Layer.Layer = Layer.effectDiscard( Effect.gen(function* () { const previousCounterValues = new Map(); diff --git a/packages/effect/src/tracer.ts b/packages/effect/src/tracer.ts index f755101e4417..a3149b8e7096 100644 --- a/packages/effect/src/tracer.ts +++ b/packages/effect/src/tracer.ts @@ -32,6 +32,46 @@ function isSentrySpan(span: EffectTracer.AnySpan): span is SentrySpanLike { return SENTRY_SPAN_SYMBOL in span; } +function getErrorMessage(exit: Exit.Exit): string | undefined { + if (!Exit.isFailure(exit)) { + return undefined; + } + + const cause = exit.cause as unknown; + + // Effect v4: cause.reasons is an array of Reason objects + if ( + cause && + typeof cause === 'object' && + 'reasons' in cause && + Array.isArray((cause as { reasons: unknown }).reasons) + ) { + const reasons = (cause as { reasons: Array<{ _tag?: string; error?: unknown; defect?: unknown }> }).reasons; + for (const reason of reasons) { + if (reason._tag === 'Fail' && reason.error !== undefined) { + return String(reason.error); + } + if (reason._tag === 'Die' && reason.defect !== undefined) { + return String(reason.defect); + } + } + return 'internal_error'; + } + + // Effect v3: cause has _tag directly + if (cause && typeof cause === 'object' && '_tag' in cause) { + const v3Cause = cause as { _tag: string; error?: unknown; defect?: unknown }; + if (v3Cause._tag === 'Fail') { + return String(v3Cause.error); + } + if (v3Cause._tag === 'Die') { + return String(v3Cause.defect); + } + } + + return 'internal_error'; +} + class SentrySpanWrapper implements SentrySpanLike { public readonly [SENTRY_SPAN_SYMBOL]: true; public readonly _tag: 'Span'; @@ -43,6 +83,7 @@ class SentrySpanWrapper implements SentrySpanLike { public readonly links: Array; public status: EffectTracer.SpanStatus; public readonly sentrySpan: Span; + public readonly annotations: Context.Context; public constructor( public readonly name: string, @@ -59,6 +100,7 @@ class SentrySpanWrapper implements SentrySpanLike { this.parent = parent; this.links = [...links]; this.sentrySpan = existingSpan; + this.annotations = context; const spanContext = this.sentrySpan.spanContext(); this.spanId = spanContext.spanId; @@ -96,9 +138,7 @@ class SentrySpanWrapper implements SentrySpanLike { } if (Exit.isFailure(exit)) { - const cause = exit.cause; - const message = - cause._tag === 'Fail' ? String(cause.error) : cause._tag === 'Die' ? String(cause.defect) : 'internal_error'; + const message = getErrorMessage(exit) ?? 'internal_error'; this.sentrySpan.setStatus({ code: 2, message }); } else { this.sentrySpan.setStatus({ code: 1 }); @@ -139,21 +179,71 @@ function createSentrySpan( return new SentrySpanWrapper(name, parent, context, links, startTime, kind, newSpan); } -const makeSentryTracer = (): EffectTracer.Tracer => - EffectTracer.make({ - span(name, parent, context, links, startTime, kind) { +// Check if we're running Effect v4 by checking the Exit/Cause structure +// In v4, causes have a 'reasons' array +// In v3, causes have '_tag' directly on the cause object +const isEffectV4 = (() => { + try { + const testExit = Exit.fail('test') as unknown as { cause?: unknown }; + const cause = testExit.cause; + // v4 causes have 'reasons' array, v3 causes have '_tag' directly + if (cause && typeof cause === 'object' && 'reasons' in cause) { + return true; + } + return false; + } catch { + return false; + } +})(); + +const makeSentryTracerV3 = (): EffectTracer.Tracer => { + // Effect v3 API: span(name, parent, context, links, startTime, kind) + return EffectTracer.make({ + span( + name: string, + parent: Option.Option, + context: Context.Context, + links: ReadonlyArray, + startTime: bigint, + kind: EffectTracer.SpanKind, + ) { return createSentrySpan(name, parent, context, links, startTime, kind); }, - context(execution, fiber) { + context(execution: () => unknown, fiber: { currentSpan?: EffectTracer.AnySpan }) { const currentSpan = fiber.currentSpan; if (currentSpan === undefined || !isSentrySpan(currentSpan)) { return execution(); } return withActiveSpan(currentSpan.sentrySpan, execution); }, + } as unknown as EffectTracer.Tracer); +}; + +const makeSentryTracerV4 = (): EffectTracer.Tracer => { + const EFFECT_EVALUATE = '~effect/Effect/evaluate' as const; + + return EffectTracer.make({ + span(options) { + return createSentrySpan( + options.name, + options.parent, + options.annotations, + options.links, + options.startTime, + options.kind, + ); + }, + context(primitive, fiber) { + const currentSpan = fiber.currentSpan; + if (currentSpan === undefined || !isSentrySpan(currentSpan)) { + return primitive[EFFECT_EVALUATE](fiber); + } + return withActiveSpan(currentSpan.sentrySpan, () => primitive[EFFECT_EVALUATE](fiber)); + }, }); +}; /** * Effect Layer that sets up the Sentry tracer for Effect spans. */ -export const SentryEffectTracer = makeSentryTracer(); +export const SentryEffectTracer = isEffectV4 ? makeSentryTracerV4() : makeSentryTracerV3(); diff --git a/packages/effect/test/layer.test.ts b/packages/effect/test/layer.test.ts index 1874fe9b0f53..255d751799d5 100644 --- a/packages/effect/test/layer.test.ts +++ b/packages/effect/test/layer.test.ts @@ -1,7 +1,8 @@ import { describe, expect, it } from '@effect/vitest'; import * as sentryCore from '@sentry/core'; import { getClient, getCurrentScope, getIsolationScope, SDK_VERSION } from '@sentry/core'; -import { Effect, Layer, Logger, LogLevel } from 'effect'; +import { Effect, Layer, Logger } from 'effect'; +import * as References from 'effect/References'; import { afterEach, beforeEach, vi } from 'vitest'; import * as sentryClient from '../src/index.client'; import * as sentryServer from '../src/index.server'; @@ -109,7 +110,7 @@ describe.each([ ), ); - it.effect('layer can be composed with tracer layer', () => + it.effect('layer can be composed with tracer', () => Effect.gen(function* () { const startInactiveSpanMock = vi.spyOn(sentryCore, 'startInactiveSpan'); @@ -120,32 +121,30 @@ describe.each([ expect(result).toBe(84); expect(startInactiveSpanMock).toHaveBeenCalledWith(expect.objectContaining({ name: 'computation' })); }).pipe( + Effect.withTracer(SentryEffectTracer), Effect.provide( - Layer.mergeAll( - effectLayer({ - dsn: TEST_DSN, - transport: getMockTransport(), - }), - Layer.setTracer(SentryEffectTracer), - ), + effectLayer({ + dsn: TEST_DSN, + transport: getMockTransport(), + }), ), ), ); - it.effect('layer can be composed with logger layer', () => + it.effect('layer can be composed with logger', () => Effect.gen(function* () { yield* Effect.logInfo('test log'); const result = yield* Effect.succeed('logged'); expect(result).toBe('logged'); }).pipe( + Effect.provideService(References.MinimumLogLevel, 'All'), Effect.provide( Layer.mergeAll( effectLayer({ dsn: TEST_DSN, transport: getMockTransport(), }), - Logger.replace(Logger.defaultLogger, SentryEffectLogger), - Logger.minimumLogLevel(LogLevel.All), + Logger.layer([SentryEffectLogger]), ), ), ), @@ -164,15 +163,15 @@ describe.each([ expect(result).toBe(84); expect(startInactiveSpanMock).toHaveBeenCalledWith(expect.objectContaining({ name: 'computation' })); }).pipe( + Effect.withTracer(SentryEffectTracer), + Effect.provideService(References.MinimumLogLevel, 'All'), Effect.provide( Layer.mergeAll( effectLayer({ dsn: TEST_DSN, transport: getMockTransport(), }), - Layer.setTracer(SentryEffectTracer), - Logger.replace(Logger.defaultLogger, SentryEffectLogger), - Logger.minimumLogLevel(LogLevel.All), + Logger.layer([SentryEffectLogger]), ), ), ), diff --git a/packages/effect/test/logger.test.ts b/packages/effect/test/logger.test.ts index c372784b483f..5069514fc2c7 100644 --- a/packages/effect/test/logger.test.ts +++ b/packages/effect/test/logger.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it } from '@effect/vitest'; import * as sentryCore from '@sentry/core'; -import { Effect, Layer, Logger, LogLevel } from 'effect'; +import { Effect, Logger } from 'effect'; +import * as References from 'effect/References'; import { afterEach, vi } from 'vitest'; import { SentryEffectLogger } from '../src/logger'; @@ -25,10 +26,10 @@ describe('SentryEffectLogger', () => { vi.clearAllMocks(); }); - const loggerLayer = Layer.mergeAll( - Logger.replace(Logger.defaultLogger, SentryEffectLogger), - Logger.minimumLogLevel(LogLevel.All), - ); + const loggerLayer = Logger.layer([SentryEffectLogger]); + + const withAllLogLevels = (effect: Effect.Effect) => + Effect.provideService(effect, References.MinimumLogLevel, 'All'); it.effect('forwards fatal logs to Sentry', () => Effect.gen(function* () { @@ -62,14 +63,14 @@ describe('SentryEffectLogger', () => { Effect.gen(function* () { yield* Effect.logDebug('This is a debug message'); expect(sentryCore.logger.debug).toHaveBeenCalledWith('This is a debug message'); - }).pipe(Effect.provide(loggerLayer)), + }).pipe(withAllLogLevels, Effect.provide(loggerLayer)), ); it.effect('forwards trace logs to Sentry', () => Effect.gen(function* () { yield* Effect.logTrace('This is a trace message'); expect(sentryCore.logger.trace).toHaveBeenCalledWith('This is a trace message'); - }).pipe(Effect.provide(loggerLayer)), + }).pipe(withAllLogLevels, Effect.provide(loggerLayer)), ); it.effect('handles object messages by stringifying', () => diff --git a/packages/effect/test/metrics.test.ts b/packages/effect/test/metrics.test.ts index 8c2b092b967f..a8d5a9813fa9 100644 --- a/packages/effect/test/metrics.test.ts +++ b/packages/effect/test/metrics.test.ts @@ -1,8 +1,10 @@ import { describe, expect, it } from '@effect/vitest'; import * as sentryCore from '@sentry/core'; -import { Duration, Effect, Metric, MetricBoundaries, MetricLabel } from 'effect'; +import * as Context from 'effect/Context'; +import { Duration, Effect, Layer, Metric } from 'effect'; +import { TestClock } from 'effect/testing'; import { afterEach, beforeEach, vi } from 'vitest'; -import { createMetricsFlusher } from '../src/metrics'; +import { SentryEffectMetricsLayer } from '../src/metrics'; describe('SentryEffectMetricsLayer', () => { const mockCount = vi.fn(); @@ -24,12 +26,12 @@ describe('SentryEffectMetricsLayer', () => { Effect.gen(function* () { const counter = Metric.counter('test_counter'); - yield* Metric.increment(counter); - yield* Metric.increment(counter); - yield* Metric.incrementBy(counter, 5); + yield* Metric.update(counter, 1); + yield* Metric.update(counter, 1); + yield* Metric.update(counter, 5); - const snapshot = Metric.unsafeSnapshot(); - const counterMetric = snapshot.find(p => p.metricKey.name === 'test_counter'); + const snapshot = Metric.snapshotUnsafe(Context.empty()); + const counterMetric = snapshot.find(p => p.id === 'test_counter'); expect(counterMetric).toBeDefined(); }), @@ -39,10 +41,10 @@ describe('SentryEffectMetricsLayer', () => { Effect.gen(function* () { const gauge = Metric.gauge('test_gauge'); - yield* Metric.set(gauge, 42); + yield* Metric.update(gauge, 42); - const snapshot = Metric.unsafeSnapshot(); - const gaugeMetric = snapshot.find(p => p.metricKey.name === 'test_gauge'); + const snapshot = Metric.snapshotUnsafe(Context.empty()); + const gaugeMetric = snapshot.find(p => p.id === 'test_gauge'); expect(gaugeMetric).toBeDefined(); }), @@ -50,14 +52,16 @@ describe('SentryEffectMetricsLayer', () => { it.effect('creates histogram metrics', () => Effect.gen(function* () { - const histogram = Metric.histogram('test_histogram', MetricBoundaries.linear({ start: 0, width: 10, count: 10 })); + const histogram = Metric.histogram('test_histogram', { + boundaries: Metric.linearBoundaries({ start: 0, width: 10, count: 10 }), + }); yield* Metric.update(histogram, 5); yield* Metric.update(histogram, 15); yield* Metric.update(histogram, 25); - const snapshot = Metric.unsafeSnapshot(); - const histogramMetric = snapshot.find(p => p.metricKey.name === 'test_histogram'); + const snapshot = Metric.snapshotUnsafe(Context.empty()); + const histogramMetric = snapshot.find(p => p.id === 'test_histogram'); expect(histogramMetric).toBeDefined(); }), @@ -65,8 +69,7 @@ describe('SentryEffectMetricsLayer', () => { it.effect('creates summary metrics', () => Effect.gen(function* () { - const summary = Metric.summary({ - name: 'test_summary', + const summary = Metric.summary('test_summary', { maxAge: '1 minute', maxSize: 100, error: 0.01, @@ -77,8 +80,8 @@ describe('SentryEffectMetricsLayer', () => { yield* Metric.update(summary, 20); yield* Metric.update(summary, 30); - const snapshot = Metric.unsafeSnapshot(); - const summaryMetric = snapshot.find(p => p.metricKey.name === 'test_summary'); + const snapshot = Metric.snapshotUnsafe(Context.empty()); + const summaryMetric = snapshot.find(p => p.id === 'test_summary'); expect(summaryMetric).toBeDefined(); }), @@ -92,39 +95,41 @@ describe('SentryEffectMetricsLayer', () => { yield* Metric.update(frequency, 'bar'); yield* Metric.update(frequency, 'foo'); - const snapshot = Metric.unsafeSnapshot(); - const frequencyMetric = snapshot.find(p => p.metricKey.name === 'test_frequency'); + const snapshot = Metric.snapshotUnsafe(Context.empty()); + const frequencyMetric = snapshot.find(p => p.id === 'test_frequency'); expect(frequencyMetric).toBeDefined(); }), ); - it.effect('supports metrics with labels', () => + it.effect('supports metrics with attributes', () => Effect.gen(function* () { const counter = Metric.counter('labeled_counter').pipe( - Metric.taggedWithLabels([MetricLabel.make('env', 'test'), MetricLabel.make('service', 'my-service')]), + Metric.withAttributes({ env: 'test', service: 'my-service' }), ); - yield* Metric.increment(counter); + yield* Metric.update(counter, 1); - const snapshot = Metric.unsafeSnapshot(); - const labeledMetric = snapshot.find(p => p.metricKey.name === 'labeled_counter'); + const snapshot = Metric.snapshotUnsafe(Context.empty()); + const labeledMetric = snapshot.find(p => p.id === 'labeled_counter'); expect(labeledMetric).toBeDefined(); - const tags = labeledMetric?.metricKey.tags ?? []; - expect(tags.some(t => t.key === 'env' && t.value === 'test')).toBe(true); - expect(tags.some(t => t.key === 'service' && t.value === 'my-service')).toBe(true); + const attrs = labeledMetric?.attributes ?? {}; + expect(attrs['env']).toBe('test'); + expect(attrs['service']).toBe('my-service'); }), ); - it.effect('tracks Effect durations with timer metric', () => + it.effect('tracks Effect durations with histogram metric', () => Effect.gen(function* () { - const timer = Metric.timerWithBoundaries('operation_duration', [10, 50, 100, 500, 1000]); + const histogram = Metric.histogram('operation_duration', { + boundaries: Metric.linearBoundaries({ start: 10, width: 100, count: 10 }), + }); - yield* Effect.succeed('done').pipe(Metric.trackDuration(timer)); + yield* Metric.update(histogram, Duration.millis(50)); - const snapshot = Metric.unsafeSnapshot(); - const timerMetric = snapshot.find(p => p.metricKey.name === 'operation_duration'); + const snapshot = Metric.snapshotUnsafe(Context.empty()); + const timerMetric = snapshot.find(p => p.id === 'operation_duration'); expect(timerMetric).toBeDefined(); }), @@ -140,7 +145,7 @@ describe('SentryEffectMetricsLayer', () => { ); }); -describe('createMetricsFlusher', () => { +describe('SentryEffectMetricsLayer flushing', () => { const mockCount = vi.fn(); const mockGauge = vi.fn(); const mockDistribution = vi.fn(); @@ -156,58 +161,54 @@ describe('createMetricsFlusher', () => { vi.restoreAllMocks(); }); + const TestLayer = SentryEffectMetricsLayer.pipe(Layer.provideMerge(TestClock.layer())); + it.effect('sends counter metrics to Sentry', () => Effect.gen(function* () { - const flusher = createMetricsFlusher(); const counter = Metric.counter('flush_test_counter'); - yield* Metric.increment(counter); - yield* Metric.incrementBy(counter, 4); + yield* Metric.update(counter, 1); + yield* Metric.update(counter, 4); - flusher.flush(); + yield* TestClock.adjust('10 seconds'); expect(mockCount).toHaveBeenCalledWith('flush_test_counter', 5, { attributes: {} }); - }), + }).pipe(Effect.provide(TestLayer)), ); it.effect('sends gauge metrics to Sentry', () => Effect.gen(function* () { - const flusher = createMetricsFlusher(); const gauge = Metric.gauge('flush_test_gauge'); - yield* Metric.set(gauge, 42); + yield* Metric.update(gauge, 42); - flusher.flush(); + yield* TestClock.adjust('10 seconds'); expect(mockGauge).toHaveBeenCalledWith('flush_test_gauge', 42, { attributes: {} }); - }), + }).pipe(Effect.provide(TestLayer)), ); it.effect('sends histogram metrics to Sentry', () => Effect.gen(function* () { - const flusher = createMetricsFlusher(); - const histogram = Metric.histogram( - 'flush_test_histogram', - MetricBoundaries.linear({ start: 0, width: 10, count: 5 }), - ); + const histogram = Metric.histogram('flush_test_histogram', { + boundaries: Metric.linearBoundaries({ start: 0, width: 10, count: 5 }), + }); yield* Metric.update(histogram, 5); yield* Metric.update(histogram, 15); - flusher.flush(); + yield* TestClock.adjust('10 seconds'); expect(mockGauge).toHaveBeenCalledWith('flush_test_histogram.sum', expect.any(Number), { attributes: {} }); expect(mockGauge).toHaveBeenCalledWith('flush_test_histogram.count', expect.any(Number), { attributes: {} }); expect(mockGauge).toHaveBeenCalledWith('flush_test_histogram.min', expect.any(Number), { attributes: {} }); expect(mockGauge).toHaveBeenCalledWith('flush_test_histogram.max', expect.any(Number), { attributes: {} }); - }), + }).pipe(Effect.provide(TestLayer)), ); it.effect('sends summary metrics to Sentry', () => Effect.gen(function* () { - const flusher = createMetricsFlusher(); - const summary = Metric.summary({ - name: 'flush_test_summary', + const summary = Metric.summary('flush_test_summary', { maxAge: '1 minute', maxSize: 100, error: 0.01, @@ -218,104 +219,74 @@ describe('createMetricsFlusher', () => { yield* Metric.update(summary, 20); yield* Metric.update(summary, 30); - flusher.flush(); + yield* TestClock.adjust('10 seconds'); expect(mockGauge).toHaveBeenCalledWith('flush_test_summary.sum', 60, { attributes: {} }); expect(mockGauge).toHaveBeenCalledWith('flush_test_summary.count', 3, { attributes: {} }); expect(mockGauge).toHaveBeenCalledWith('flush_test_summary.min', 10, { attributes: {} }); expect(mockGauge).toHaveBeenCalledWith('flush_test_summary.max', 30, { attributes: {} }); - }), + }).pipe(Effect.provide(TestLayer)), ); it.effect('sends frequency metrics to Sentry', () => Effect.gen(function* () { - const flusher = createMetricsFlusher(); const frequency = Metric.frequency('flush_test_frequency'); yield* Metric.update(frequency, 'apple'); yield* Metric.update(frequency, 'banana'); yield* Metric.update(frequency, 'apple'); - flusher.flush(); + yield* TestClock.adjust('10 seconds'); expect(mockCount).toHaveBeenCalledWith('flush_test_frequency', 2, { attributes: { word: 'apple' } }); expect(mockCount).toHaveBeenCalledWith('flush_test_frequency', 1, { attributes: { word: 'banana' } }); - }), + }).pipe(Effect.provide(TestLayer)), ); - it.effect('sends metrics with labels as attributes to Sentry', () => + it.effect('sends metrics with attributes to Sentry', () => Effect.gen(function* () { - const flusher = createMetricsFlusher(); const gauge = Metric.gauge('flush_test_labeled_gauge').pipe( - Metric.taggedWithLabels([MetricLabel.make('env', 'production'), MetricLabel.make('region', 'us-east')]), + Metric.withAttributes({ env: 'production', region: 'us-east' }), ); - yield* Metric.set(gauge, 100); + yield* Metric.update(gauge, 100); - flusher.flush(); + yield* TestClock.adjust('10 seconds'); expect(mockGauge).toHaveBeenCalledWith('flush_test_labeled_gauge', 100, { attributes: { env: 'production', region: 'us-east' }, }); - }), + }).pipe(Effect.provide(TestLayer)), ); it.effect('sends counter delta values on subsequent flushes', () => Effect.gen(function* () { - const flusher = createMetricsFlusher(); const counter = Metric.counter('flush_test_delta_counter'); - yield* Metric.incrementBy(counter, 10); - flusher.flush(); + yield* Metric.update(counter, 10); + yield* TestClock.adjust('10 seconds'); mockCount.mockClear(); - yield* Metric.incrementBy(counter, 5); - flusher.flush(); + yield* Metric.update(counter, 5); + yield* TestClock.adjust('10 seconds'); expect(mockCount).toHaveBeenCalledWith('flush_test_delta_counter', 5, { attributes: {} }); - }), + }).pipe(Effect.provide(TestLayer)), ); it.effect('does not send counter when delta is zero', () => Effect.gen(function* () { - const flusher = createMetricsFlusher(); const counter = Metric.counter('flush_test_zero_delta'); - yield* Metric.incrementBy(counter, 10); - flusher.flush(); + yield* Metric.update(counter, 10); + yield* TestClock.adjust('10 seconds'); mockCount.mockClear(); - flusher.flush(); + yield* TestClock.adjust('10 seconds'); expect(mockCount).not.toHaveBeenCalledWith('flush_test_zero_delta', 0, { attributes: {} }); - }), + }).pipe(Effect.provide(TestLayer)), ); - - it.effect('clear() resets delta tracking state', () => - Effect.gen(function* () { - const flusher = createMetricsFlusher(); - const counter = Metric.counter('flush_test_clear_counter'); - - yield* Metric.incrementBy(counter, 10); - flusher.flush(); - - mockCount.mockClear(); - flusher.clear(); - - flusher.flush(); - - expect(mockCount).toHaveBeenCalledWith('flush_test_clear_counter', 10, { attributes: {} }); - }), - ); - - it('each flusher has isolated state', () => { - const flusher1 = createMetricsFlusher(); - const flusher2 = createMetricsFlusher(); - - expect(flusher1).not.toBe(flusher2); - expect(flusher1.flush).not.toBe(flusher2.flush); - expect(flusher1.clear).not.toBe(flusher2.clear); - }); }); diff --git a/packages/effect/test/tracer.test.ts b/packages/effect/test/tracer.test.ts index 9583e7d12c5b..81d8cae64f42 100644 --- a/packages/effect/test/tracer.test.ts +++ b/packages/effect/test/tracer.test.ts @@ -1,11 +1,11 @@ import { describe, expect, it } from '@effect/vitest'; import * as sentryCore from '@sentry/core'; import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; -import { Effect, Layer } from 'effect'; +import { Effect } from 'effect'; import { afterEach, vi } from 'vitest'; import { SentryEffectTracer } from '../src/tracer'; -const TracerLayer = Layer.setTracer(SentryEffectTracer); +const withSentryTracer = (effect: Effect.Effect) => Effect.withTracer(effect, SentryEffectTracer); describe('SentryEffectTracer', () => { afterEach(() => { @@ -24,7 +24,7 @@ describe('SentryEffectTracer', () => { ); expect(capturedSpanName).toBe('effect-span-executed'); - }).pipe(Effect.provide(TracerLayer)), + }).pipe(withSentryTracer), ); it.effect('creates spans with correct attributes', () => @@ -32,7 +32,7 @@ describe('SentryEffectTracer', () => { const result = yield* Effect.withSpan('my-operation')(Effect.succeed('success')); expect(result).toBe('success'); - }).pipe(Effect.provide(TracerLayer)), + }).pipe(withSentryTracer), ); it.effect('handles nested spans', () => @@ -45,7 +45,7 @@ describe('SentryEffectTracer', () => { ); expect(result).toBe('outer-inner-result'); - }).pipe(Effect.provide(TracerLayer)), + }).pipe(withSentryTracer), ); it.effect('propagates span context through Effect fibers', () => @@ -62,27 +62,30 @@ describe('SentryEffectTracer', () => { ); expect(results).toEqual(['parent-start', 'child-1', 'child-2', 'parent-end']); - }).pipe(Effect.provide(TracerLayer)), + }).pipe(withSentryTracer), ); it.effect('handles span failures correctly', () => Effect.gen(function* () { const result = yield* Effect.withSpan('failing-span')(Effect.fail('expected-error')).pipe( - Effect.catchAll(e => Effect.succeed(`caught: ${e}`)), + Effect.catchCause(cause => { + const error = cause.reasons[0]?._tag === 'Fail' ? cause.reasons[0].error : 'unknown'; + return Effect.succeed(`caught: ${error}`); + }), ); expect(result).toBe('caught: expected-error'); - }).pipe(Effect.provide(TracerLayer)), + }).pipe(withSentryTracer), ); it.effect('handles span with defects (die)', () => Effect.gen(function* () { const result = yield* Effect.withSpan('defect-span')(Effect.die('defect-value')).pipe( - Effect.catchAllDefect(d => Effect.succeed(`caught-defect: ${d}`)), + Effect.catchDefect(d => Effect.succeed(`caught-defect: ${d}`)), ); expect(result).toBe('caught-defect: defect-value'); - }).pipe(Effect.provide(TracerLayer)), + }).pipe(withSentryTracer), ); it.effect('works with Effect.all for parallel operations', () => @@ -96,7 +99,7 @@ describe('SentryEffectTracer', () => { ); expect(results).toEqual([1, 2, 3]); - }).pipe(Effect.provide(TracerLayer)), + }).pipe(withSentryTracer), ); it.effect('supports span annotations', () => @@ -107,7 +110,7 @@ describe('SentryEffectTracer', () => { ); expect(result).toBe('annotated'); - }).pipe(Effect.provide(TracerLayer)), + }).pipe(withSentryTracer), ); it.effect('sets span status to ok on success', () => @@ -130,7 +133,7 @@ describe('SentryEffectTracer', () => { expect(setStatusCalls).toContainEqual({ code: 1 }); mockStartInactiveSpan.mockRestore(); - }).pipe(Effect.provide(TracerLayer)), + }).pipe(withSentryTracer), ); it.effect('sets span status to error on failure', () => @@ -148,12 +151,12 @@ describe('SentryEffectTracer', () => { } as unknown as sentryCore.Span; }); - yield* Effect.withSpan('error-span')(Effect.fail('test-error')).pipe(Effect.catchAll(() => Effect.void)); + yield* Effect.withSpan('error-span')(Effect.fail('test-error')).pipe(Effect.catchCause(() => Effect.void)); expect(setStatusCalls).toContainEqual({ code: 2, message: 'test-error' }); mockStartInactiveSpan.mockRestore(); - }).pipe(Effect.provide(TracerLayer)), + }).pipe(withSentryTracer), ); it.effect('sets span status to error on defect', () => @@ -171,12 +174,12 @@ describe('SentryEffectTracer', () => { } as unknown as sentryCore.Span; }); - yield* Effect.withSpan('defect-span')(Effect.die('fatal-defect')).pipe(Effect.catchAllDefect(() => Effect.void)); + yield* Effect.withSpan('defect-span')(Effect.die('fatal-defect')).pipe(Effect.catchDefect(() => Effect.void)); expect(setStatusCalls).toContainEqual({ code: 2, message: 'fatal-defect' }); mockStartInactiveSpan.mockRestore(); - }).pipe(Effect.provide(TracerLayer)), + }).pipe(withSentryTracer), ); it.effect('propagates Sentry span context via withActiveSpan', () => @@ -197,7 +200,7 @@ describe('SentryEffectTracer', () => { expect(withActiveSpanCalls.length).toBeGreaterThan(0); mockWithActiveSpan.mockRestore(); - }).pipe(Effect.provide(TracerLayer)), + }).pipe(withSentryTracer), ); it.effect('sets origin to auto.function.effect for regular spans', () => @@ -222,7 +225,7 @@ describe('SentryEffectTracer', () => { expect(capturedAttributes?.[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toBe('auto.function.effect'); mockStartInactiveSpan.mockRestore(); - }).pipe(Effect.provide(TracerLayer)), + }).pipe(withSentryTracer), ); it.effect('sets origin to auto.http.effect for http.server spans', () => @@ -247,7 +250,7 @@ describe('SentryEffectTracer', () => { expect(capturedAttributes?.[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toBe('auto.http.effect'); mockStartInactiveSpan.mockRestore(); - }).pipe(Effect.provide(TracerLayer)), + }).pipe(withSentryTracer), ); it.effect('sets origin to auto.http.effect for http.client spans', () => @@ -272,7 +275,7 @@ describe('SentryEffectTracer', () => { expect(capturedAttributes?.[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toBe('auto.http.effect'); mockStartInactiveSpan.mockRestore(); - }).pipe(Effect.provide(TracerLayer)), + }).pipe(withSentryTracer), ); it.effect('can be used with Effect.withTracer', () => diff --git a/yarn.lock b/yarn.lock index cc45a89c5703..7731d582e748 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3229,10 +3229,10 @@ dependencies: "@edge-runtime/primitives" "6.0.0" -"@effect/vitest@^0.23.9": - version "0.23.13" - resolved "https://registry.yarnpkg.com/@effect/vitest/-/vitest-0.23.13.tgz#17edf9d8e3443f080ff8fe93bd37b023612a07a4" - integrity sha512-F3x2phMXuVzqWexdcYp8v0z1qQHkKxp2UaHNbqZaEjPEp8FBz/iMwbi6iS/oIWzLfGF8XqdP8BGJptvGIJONNw== +"@effect/vitest@^4.0.0-beta.50": + version "4.0.0-beta.50" + resolved "https://registry.yarnpkg.com/@effect/vitest/-/vitest-4.0.0-beta.50.tgz#c3945b4a0206fa07160896b641445e16eb5d3214" + integrity sha512-bju/iCLZB8oHsVia1i6olo9ZntkZ5TrqmsINudFsRkZfHhu5UuTR3vjic29wykZpPXXONX1wKO0KZZCk+stcKg== "@ember-data/rfc395-data@^0.0.4": version "0.0.4" @@ -5314,6 +5314,36 @@ dependencies: sparse-bitfield "^3.0.3" +"@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.3": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.3.tgz#9edec61b22c3082018a79f6d1c30289ddf3d9d11" + integrity sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw== + +"@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.3": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-3.0.3.tgz#33677a275204898ad8acbf62734fc4dc0b6a4855" + integrity sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw== + +"@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.3": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-3.0.3.tgz#19edf7cdc2e7063ee328403c1d895a86dd28f4bb" + integrity sha512-YxQL+ax0XqBJDZiKimS2XQaf+2wDGVa1enVRGzEvLLVFeqa5kx2bWbtcSXgsxjQB7nRqqIGFIcLteF/sHeVtQg== + +"@msgpackr-extract/msgpackr-extract-linux-arm@3.0.3": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-3.0.3.tgz#94fb0543ba2e28766c3fc439cabbe0440ae70159" + integrity sha512-fg0uy/dG/nZEXfYilKoRe7yALaNmHoYeIoJuJ7KJ+YyU2bvY8vPv27f7UKhGRpY6euFYqEVhxCFZgAUNQBM3nw== + +"@msgpackr-extract/msgpackr-extract-linux-x64@3.0.3": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-3.0.3.tgz#4a0609ab5fe44d07c9c60a11e4484d3c38bbd6e3" + integrity sha512-cvwNfbP07pKUfq1uH+S6KJ7dT9K8WOE4ZiAcsrSes+UY55E/0jLYc+vq+DO7jlmqRb5zAggExKm0H7O/CBaesg== + +"@msgpackr-extract/msgpackr-extract-win32-x64@3.0.3": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.3.tgz#0aa5502d547b57abfc4ac492de68e2006e417242" + integrity sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ== + "@napi-rs/wasm-runtime@0.2.4": version "0.2.4" resolved "https://registry.yarnpkg.com/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.4.tgz#d27788176f250d86e498081e3c5ff48a17606918" @@ -8563,10 +8593,10 @@ resolved "https://registry.yarnpkg.com/@speed-highlight/core/-/core-1.2.14.tgz#5d7fe87410d2d779bd0b7680f7a706466f363314" integrity sha512-G4ewlBNhUtlLvrJTb88d2mdy2KRijzs4UhnlrOSRT4bmjh/IqNElZa3zkrZ+TC47TwtlDWzVLFADljF1Ijp5hA== -"@standard-schema/spec@^1.0.0": - version "1.0.0" - resolved "https://registry.yarnpkg.com/@standard-schema/spec/-/spec-1.0.0.tgz#f193b73dc316c4170f2e82a881da0f550d551b9c" - integrity sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA== +"@standard-schema/spec@^1.0.0", "@standard-schema/spec@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@standard-schema/spec/-/spec-1.1.0.tgz#a79b55dbaf8604812f52d140b2c9ab41bc150bb8" + integrity sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w== "@supabase/auth-js@2.69.1": version "2.69.1" @@ -14912,7 +14942,7 @@ detect-libc@^1.0.3: resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b" integrity sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg== -detect-libc@^2.0.0, detect-libc@^2.0.2, detect-libc@^2.0.3, detect-libc@^2.0.4, detect-libc@^2.1.2: +detect-libc@^2.0.0, detect-libc@^2.0.1, detect-libc@^2.0.2, detect-libc@^2.0.3, detect-libc@^2.0.4, detect-libc@^2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.1.2.tgz#689c5dcdc1900ef5583a4cb9f6d7b473742074ad" integrity sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ== @@ -15315,13 +15345,21 @@ effect@3.16.12: "@standard-schema/spec" "^1.0.0" fast-check "^3.23.1" -effect@^3.21.0: - version "3.21.0" - resolved "https://registry.yarnpkg.com/effect/-/effect-3.21.0.tgz#ce222ce8f785b9e63f104b9a4ead985e7965f2c0" - integrity sha512-PPN80qRokCd1f015IANNhrwOnLO7GrrMQfk4/lnZRE/8j7UPWrNNjPV0uBrZutI/nHzernbW+J0hdqQysHiSnQ== - dependencies: - "@standard-schema/spec" "^1.0.0" - fast-check "^3.23.1" +effect@^4.0.0-beta.50: + version "4.0.0-beta.50" + resolved "https://registry.yarnpkg.com/effect/-/effect-4.0.0-beta.50.tgz#c4fbc42adad53428242b8002390bde69b48feb0d" + integrity sha512-UsENighZms6LWDSnF/05F9JinDAewV3sGXHAt9M7+dL3VnoFZIwduFxXvmFc7QJm7iV1s7rB98hv1SD3ALA9qg== + dependencies: + "@standard-schema/spec" "^1.1.0" + fast-check "^4.6.0" + find-my-way-ts "^0.1.6" + ini "^6.0.0" + kubernetes-types "^1.30.0" + msgpackr "^1.11.9" + multipasta "^0.2.7" + toml "^4.1.1" + uuid "^13.0.0" + yaml "^2.8.3" ejs@^3.1.7: version "3.1.8" @@ -17431,6 +17469,13 @@ fast-check@^3.23.1: dependencies: pure-rand "^6.1.0" +fast-check@^4.6.0: + version "4.7.0" + resolved "https://registry.yarnpkg.com/fast-check/-/fast-check-4.7.0.tgz#36c0051b9c968965e8970e88e63eee946fe45f8f" + integrity sha512-NsZRtqvSSoCP0HbNjUD+r1JH8zqZalyp6gLY9e7OYs7NK9b6AHOs2baBFeBG7bVNsuoukh89x2Yg3rPsul8ziQ== + dependencies: + pure-rand "^8.0.0" + fast-content-type-parse@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/fast-content-type-parse/-/fast-content-type-parse-3.0.0.tgz#5590b6c807cc598be125e6740a9fde589d2b7afb" @@ -17756,6 +17801,11 @@ find-index@^1.1.0: resolved "https://registry.yarnpkg.com/find-index/-/find-index-1.1.1.tgz#4b221f8d46b7f8bea33d8faed953f3ca7a081cbc" integrity sha512-XYKutXMrIK99YMUPf91KX5QVJoG31/OsgftD6YoTPAObfQIxM4ziA9f0J1AsqKhJmo+IeaIPP0CFopTD4bdUBw== +find-my-way-ts@^0.1.6: + version "0.1.6" + resolved "https://registry.yarnpkg.com/find-my-way-ts/-/find-my-way-ts-0.1.6.tgz#37f7b8433d0f61e7fe7290772240b0c133b0ebf2" + integrity sha512-a85L9ZoXtNAey3Y6Z+eBWW658kO/MwR7zIafkIUPUMf3isZG0NCs2pjW2wtjxAKuJPxMAsHUIP4ZPGv0o5gyTA== + find-up@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/find-up/-/find-up-2.1.0.tgz#45d1b7e506c717ddd482775a2b77920a3c0c57a7" @@ -19626,6 +19676,11 @@ ini@^2.0.0: resolved "https://registry.yarnpkg.com/ini/-/ini-2.0.0.tgz#e5fd556ecdd5726be978fa1001862eacb0a94bc5" integrity sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA== +ini@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/ini/-/ini-6.0.0.tgz#efc7642b276f6a37d22fdf56ef50889d7146bf30" + integrity sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ== + injection-js@^2.4.0: version "2.4.0" resolved "https://registry.yarnpkg.com/injection-js/-/injection-js-2.4.0.tgz#ebe8871b1a349f23294eaa751bbd8209a636e754" @@ -20934,6 +20989,11 @@ knitwork@^1.2.0, knitwork@^1.3.0: resolved "https://registry.yarnpkg.com/knitwork/-/knitwork-1.3.0.tgz#4a0d0b0d45378cac909ee1117481392522bd08a4" integrity sha512-4LqMNoONzR43B1W0ek0fhXMsDNW/zxa1NdFAVMY+k28pgZLovR4G3PB5MrpTxCy1QaZCqNoiaKPr5w5qZHfSNw== +kubernetes-types@^1.30.0: + version "1.30.0" + resolved "https://registry.yarnpkg.com/kubernetes-types/-/kubernetes-types-1.30.0.tgz#f686cacb08ffc5f7e89254899c2153c723420116" + integrity sha512-Dew1okvhM/SQcIa2rcgujNndZwU8VnSapDgdxlYoB84ZlpAD43U6KLAFqYo17ykSFGHNPrg0qry0bP+GJd9v7Q== + kuler@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/kuler/-/kuler-2.0.0.tgz#e2c570a3800388fb44407e851531c1d670b061b3" @@ -22908,6 +22968,27 @@ ms@2.1.3, ms@^2.0.0, ms@^2.1.1, ms@^2.1.3: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== +msgpackr-extract@^3.0.2: + version "3.0.3" + resolved "https://registry.yarnpkg.com/msgpackr-extract/-/msgpackr-extract-3.0.3.tgz#e9d87023de39ce714872f9e9504e3c1996d61012" + integrity sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA== + dependencies: + node-gyp-build-optional-packages "5.2.2" + optionalDependencies: + "@msgpackr-extract/msgpackr-extract-darwin-arm64" "3.0.3" + "@msgpackr-extract/msgpackr-extract-darwin-x64" "3.0.3" + "@msgpackr-extract/msgpackr-extract-linux-arm" "3.0.3" + "@msgpackr-extract/msgpackr-extract-linux-arm64" "3.0.3" + "@msgpackr-extract/msgpackr-extract-linux-x64" "3.0.3" + "@msgpackr-extract/msgpackr-extract-win32-x64" "3.0.3" + +msgpackr@^1.11.9: + version "1.11.9" + resolved "https://registry.yarnpkg.com/msgpackr/-/msgpackr-1.11.9.tgz#1aa99ed379a066374ac82b62f8ad70723bbd3a59" + integrity sha512-FkoAAyyA6HM8wL882EcEyFZ9s7hVADSwG9xrVx3dxxNQAtgADTrJoEWivID82Iv1zWDsv/OtbrrcZAzGzOMdNw== + optionalDependencies: + msgpackr-extract "^3.0.2" + multer@2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/multer/-/multer-2.0.2.tgz#08a8aa8255865388c387aaf041426b0c87bf58dd" @@ -22929,6 +23010,11 @@ multicast-dns@^7.2.5: dns-packet "^5.2.2" thunky "^1.0.2" +multipasta@^0.2.7: + version "0.2.7" + resolved "https://registry.yarnpkg.com/multipasta/-/multipasta-0.2.7.tgz#fa8fb38be65eb951fa57cad9e8e758107946eee9" + integrity sha512-KPA58d68KgGil15oDqXjkUBEBYc00XvbPj5/X+dyzeo/lWm9Nc25pQRlf1D+gv4OpK7NM0J1odrbu9JNNGvynA== + mustache@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/mustache/-/mustache-4.2.0.tgz#e5892324d60a12ec9c2a73359edca52972bf6f64" @@ -23339,6 +23425,13 @@ node-forge@^1, node-forge@^1.3.1: resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-1.4.0.tgz#1c7b7d8bdc2d078739f58287d589d903a11b2fc2" integrity sha512-LarFH0+6VfriEhqMMcLX2F7SwSXeWwnEAJEsYm5QKWchiVYVvJyV9v7UDvUv+w5HO23ZpQTXDv/GxdDdMyOuoQ== +node-gyp-build-optional-packages@5.2.2: + version "5.2.2" + resolved "https://registry.yarnpkg.com/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.2.2.tgz#522f50c2d53134d7f3a76cd7255de4ab6c96a3a4" + integrity sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw== + dependencies: + detect-libc "^2.0.1" + node-gyp-build@^4.2.2: version "4.6.0" resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.6.0.tgz#0c52e4cbf54bbd28b709820ef7b6a3c2d6209055" @@ -26085,6 +26178,11 @@ pure-rand@^6.1.0: resolved "https://registry.yarnpkg.com/pure-rand/-/pure-rand-6.1.0.tgz#d173cf23258231976ccbdb05247c9787957604f2" integrity sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA== +pure-rand@^8.0.0: + version "8.4.0" + resolved "https://registry.yarnpkg.com/pure-rand/-/pure-rand-8.4.0.tgz#1d9e26e9c0555486e08ae300d02796af8dec1cd0" + integrity sha512-IoM8YF/jY0hiugFo/wOWqfmarlE6J0wc6fDK1PhftMk7MGhVZl88sZimmqBBFomLOCSmcCCpsfj7wXASCpvK9A== + qs@^6.14.0, qs@^6.14.1, qs@^6.4.0, qs@~6.14.0, qs@~6.14.1: version "6.14.2" resolved "https://registry.yarnpkg.com/qs/-/qs-6.14.2.tgz#b5634cf9d9ad9898e31fba3504e866e8efb6798c" @@ -29619,6 +29717,11 @@ token-types@^6.1.1: "@tokenizer/token" "^0.3.0" ieee754 "^1.2.1" +toml@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/toml/-/toml-4.1.1.tgz#ab8248d0403ba2c02ffcf8515b42f0dcf0d6d1b5" + integrity sha512-EBJnVBr3dTXdA89WVFoAIPUqkBjxPMwRqsfuo1r240tKFHXv3zgca4+NJib/h6TyvGF7vOawz0jGuryJCdNHrw== + totalist@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/totalist/-/totalist-3.0.0.tgz#4ef9c58c5f095255cdc3ff2a0a55091c57a3a1bd" @@ -30672,6 +30775,11 @@ uuid@^11.1.0: resolved "https://registry.yarnpkg.com/uuid/-/uuid-11.1.0.tgz#9549028be1753bb934fc96e2bca09bb4105ae912" integrity sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A== +uuid@^13.0.0: + version "13.0.0" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-13.0.0.tgz#263dc341b19b4d755eb8fe36b78d95a6b65707e8" + integrity sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w== + uuid@^9.0.0, uuid@^9.0.1: version "9.0.1" resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.1.tgz#e188d4c8853cc722220392c424cd637f32293f30" @@ -31880,7 +31988,7 @@ yam@^1.0.0: fs-extra "^4.0.2" lodash.merge "^4.6.0" -yaml@2.8.3, yaml@^2.6.0, yaml@^2.8.0: +yaml@2.8.3, yaml@^2.6.0, yaml@^2.8.0, yaml@^2.8.3: version "2.8.3" resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.8.3.tgz#a0d6bd2efb3dd03c59370223701834e60409bd7d" integrity sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==