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-3-browser/start-event-proxy.mjs b/dev-packages/e2e-tests/test-applications/effect-3-browser/start-event-proxy.mjs new file mode 100644 index 000000000000..6da20fa0890e --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/effect-3-browser/start-event-proxy.mjs @@ -0,0 +1,6 @@ +import { startEventProxyServer } from '@sentry-internal/test-utils'; + +startEventProxyServer({ + port: 3031, + 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 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-browser/start-event-proxy.mjs b/dev-packages/e2e-tests/test-applications/effect-4-node/start-event-proxy.mjs similarity index 75% rename from dev-packages/e2e-tests/test-applications/effect-browser/start-event-proxy.mjs rename to dev-packages/e2e-tests/test-applications/effect-4-node/start-event-proxy.mjs index a86a1bd91404..6874b711993a 100644 --- a/dev-packages/e2e-tests/test-applications/effect-browser/start-event-proxy.mjs +++ b/dev-packages/e2e-tests/test-applications/effect-4-node/start-event-proxy.mjs @@ -2,5 +2,5 @@ import { startEventProxyServer } from '@sentry-internal/test-utils'; startEventProxyServer({ port: 3031, - proxyServerName: 'effect-browser', + 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==