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
+
+
+
+
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==