diff --git a/dev-packages/e2e-tests/test-applications/effect-browser/.gitignore b/dev-packages/e2e-tests/test-applications/effect-browser/.gitignore
new file mode 100644
index 000000000000..bd66327c3b4a
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/effect-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-browser/.npmrc b/dev-packages/e2e-tests/test-applications/effect-browser/.npmrc
new file mode 100644
index 000000000000..070f80f05092
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/effect-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-browser/build.mjs b/dev-packages/e2e-tests/test-applications/effect-browser/build.mjs
new file mode 100644
index 000000000000..63c63597d4fe
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/effect-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-browser/package.json b/dev-packages/e2e-tests/test-applications/effect-browser/package.json
new file mode 100644
index 000000000000..6c2e7e63ced8
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/effect-browser/package.json
@@ -0,0 +1,42 @@
+{
+ "name": "effect-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": "^3.19.19",
+ "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": {
+ "extends": "../../package.json"
+ }
+}
diff --git a/dev-packages/e2e-tests/test-applications/effect-browser/playwright.config.mjs b/dev-packages/e2e-tests/test-applications/effect-browser/playwright.config.mjs
new file mode 100644
index 000000000000..31f2b913b58b
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/effect-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-browser/public/index.html b/dev-packages/e2e-tests/test-applications/effect-browser/public/index.html
new file mode 100644
index 000000000000..19d5c3d99a2f
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/effect-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-browser/src/index.js b/dev-packages/e2e-tests/test-applications/effect-browser/src/index.js
new file mode 100644
index 000000000000..c5d4645814ea
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/effect-browser/src/index.js
@@ -0,0 +1,87 @@
+// @ts-check
+import * as Sentry from '@sentry/effect';
+import { Cause, Effect, Layer, Logger, LogLevel, Runtime } from 'effect';
+
+const LogLevelLive = Logger.minimumLogLevel(LogLevel.Debug);
+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,
+ enableEffectLogs: true,
+ }),
+ LogLevelLive,
+);
+
+const runtime = Layer.toRuntime(AppLayer).pipe(Effect.scoped, Effect.runSync);
+
+const runEffect = fn => Runtime.runPromise(runtime)(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-browser/start-event-proxy.mjs b/dev-packages/e2e-tests/test-applications/effect-browser/start-event-proxy.mjs
new file mode 100644
index 000000000000..a86a1bd91404
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/effect-browser/start-event-proxy.mjs
@@ -0,0 +1,6 @@
+import { startEventProxyServer } from '@sentry-internal/test-utils';
+
+startEventProxyServer({
+ port: 3031,
+ proxyServerName: 'effect-browser',
+});
diff --git a/dev-packages/e2e-tests/test-applications/effect-browser/tests/errors.test.ts b/dev-packages/e2e-tests/test-applications/effect-browser/tests/errors.test.ts
new file mode 100644
index 000000000000..80589f683c28
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/effect-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-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-browser', async transactionEvent => {
+ return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'pageload';
+ });
+
+ const errorEventPromise = waitForError('effect-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-browser/tests/logs.test.ts b/dev-packages/e2e-tests/test-applications/effect-browser/tests/logs.test.ts
new file mode 100644
index 000000000000..f81bc249cbd8
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/effect-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-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-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-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-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-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-browser/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/effect-browser/tests/transactions.test.ts
new file mode 100644
index 000000000000..dbbb8fa7ddf3
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/effect-browser/tests/transactions.test.ts
@@ -0,0 +1,123 @@
+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 => {
+ 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-browser', async transactionEvent => {
+ return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'pageload';
+ });
+
+ const navigationTransactionPromise = waitForTransaction('effect-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-browser', transactionEvent => {
+ return transactionEvent?.contexts?.trace?.op === 'pageload';
+ });
+
+ const transactionPromise = waitForTransaction('effect-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',
+ data: expect.objectContaining({
+ 'sentry.op': 'internal',
+ }),
+ }),
+ );
+
+ 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-browser/tsconfig.json b/dev-packages/e2e-tests/test-applications/effect-browser/tsconfig.json
new file mode 100644
index 000000000000..cb69f25b8d50
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/effect-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-node/.gitignore b/dev-packages/e2e-tests/test-applications/effect-node/.gitignore
new file mode 100644
index 000000000000..f06235c460c2
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/effect-node/.gitignore
@@ -0,0 +1,2 @@
+node_modules
+dist
diff --git a/dev-packages/e2e-tests/test-applications/effect-node/.npmrc b/dev-packages/e2e-tests/test-applications/effect-node/.npmrc
new file mode 100644
index 000000000000..070f80f05092
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/effect-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-node/package.json b/dev-packages/e2e-tests/test-applications/effect-node/package.json
new file mode 100644
index 000000000000..621a017d3020
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/effect-node/package.json
@@ -0,0 +1,29 @@
+{
+ "name": "effect-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": "^0.94.5",
+ "@effect/platform-node": "^0.104.1",
+ "@sentry/effect": "latest || *",
+ "@types/node": "^18.19.1",
+ "effect": "^3.19.19",
+ "typescript": "~5.0.0"
+ },
+ "devDependencies": {
+ "@playwright/test": "~1.56.0",
+ "@sentry-internal/test-utils": "link:../../../test-utils"
+ },
+ "volta": {
+ "extends": "../../package.json"
+ }
+}
diff --git a/dev-packages/e2e-tests/test-applications/effect-node/playwright.config.mjs b/dev-packages/e2e-tests/test-applications/effect-node/playwright.config.mjs
new file mode 100644
index 000000000000..31f2b913b58b
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/effect-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-node/src/app.ts b/dev-packages/e2e-tests/test-applications/effect-node/src/app.ts
new file mode 100644
index 000000000000..9e4a40e585c3
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/effect-node/src/app.ts
@@ -0,0 +1,101 @@
+import * as Sentry from '@sentry/effect';
+import { HttpRouter, HttpServer, HttpServerResponse } from '@effect/platform';
+import { NodeHttpServer, NodeRuntime } from '@effect/platform-node';
+import { Cause, Effect, Layer, Logger, LogLevel } from 'effect';
+import { createServer } from 'http';
+
+const SentryLive = Sentry.effectLayer({
+ dsn: process.env.E2E_TEST_DSN,
+ environment: 'qa',
+ debug: !!process.env.DEBUG,
+ tunnel: 'http://localhost:3031/',
+ tracesSampleRate: 1,
+ enableLogs: true,
+ enableEffectLogs: true,
+});
+
+const router = HttpRouter.empty.pipe(
+ HttpRouter.get('/test-success', HttpServerResponse.json({ version: 'v1' })),
+
+ HttpRouter.get(
+ '/test-transaction',
+ Effect.gen(function* () {
+ yield* Effect.void.pipe(Effect.withSpan('test-span'));
+ return yield* HttpServerResponse.json({ status: 'ok' });
+ }),
+ ),
+
+ HttpRouter.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.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.get(
+ '/test-exception/:id',
+ Effect.sync(() => {
+ throw new Error('This is an exception with id 123');
+ }),
+ ),
+
+ HttpRouter.get('/test-effect-fail', Effect.fail(new Error('Effect failure'))),
+
+ HttpRouter.get('/test-effect-die', Effect.die('Effect defect')),
+
+ HttpRouter.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.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' });
+ }),
+ ),
+
+ HttpRouter.catchAllCause(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 });
+ });
+ }),
+);
+
+const LogLevelLive = Logger.minimumLogLevel(LogLevel.Debug);
+
+const ServerLive = router.pipe(
+ HttpServer.serve(),
+ HttpServer.withLogAddress,
+ Layer.provide(NodeHttpServer.layer(createServer, { port: 3030 })),
+ Layer.provide(SentryLive),
+ Layer.provide(LogLevelLive),
+);
+
+ServerLive.pipe(Layer.launch, NodeRuntime.runMain);
diff --git a/dev-packages/e2e-tests/test-applications/effect-node/start-event-proxy.mjs b/dev-packages/e2e-tests/test-applications/effect-node/start-event-proxy.mjs
new file mode 100644
index 000000000000..41eb647958b7
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/effect-node/start-event-proxy.mjs
@@ -0,0 +1,6 @@
+import { startEventProxyServer } from '@sentry-internal/test-utils';
+
+startEventProxyServer({
+ port: 3031,
+ proxyServerName: 'effect-node',
+});
diff --git a/dev-packages/e2e-tests/test-applications/effect-node/tests/errors.test.ts b/dev-packages/e2e-tests/test-applications/effect-node/tests/errors.test.ts
new file mode 100644
index 000000000000..3b7da230c0e0
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/effect-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-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-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-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-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-node/tests/logs.test.ts b/dev-packages/e2e-tests/test-applications/effect-node/tests/logs.test.ts
new file mode 100644
index 000000000000..85f5840e14a8
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/effect-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-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-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-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-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-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-node/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/effect-node/tests/transactions.test.ts
new file mode 100644
index 000000000000..1f510f7d074c
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/effect-node/tests/transactions.test.ts
@@ -0,0 +1,91 @@
+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 => {
+ return (
+ transactionEvent?.contexts?.trace?.op === 'http.server' &&
+ transactionEvent?.transaction?.includes('/test-success')
+ );
+ });
+
+ await fetch(`${baseURL}/test-success`);
+
+ const transactionEvent = await transactionEventPromise;
+
+ expect(transactionEvent.contexts?.trace?.op).toBe('http.server');
+ expect(transactionEvent.transaction).toContain('/test-success');
+});
+
+test('Sends transaction with manual Effect span', async ({ baseURL }) => {
+ const transactionEventPromise = waitForTransaction('effect-node', transactionEvent => {
+ return (
+ transactionEvent?.contexts?.trace?.op === 'http.server' &&
+ transactionEvent?.transaction?.includes('/test-transaction')
+ );
+ });
+
+ await fetch(`${baseURL}/test-transaction`);
+
+ const transactionEvent = await transactionEventPromise;
+
+ expect(transactionEvent.contexts?.trace?.op).toBe('http.server');
+ expect(transactionEvent.transaction).toContain('/test-transaction');
+
+ const spans = transactionEvent.spans || [];
+ expect(spans).toContainEqual(
+ expect.objectContaining({
+ description: 'test-span',
+ }),
+ );
+});
+
+test('Sends Effect spans with correct parent-child structure', async ({ baseURL }) => {
+ const transactionEventPromise = waitForTransaction('effect-node', transactionEvent => {
+ return (
+ transactionEvent?.contexts?.trace?.op === 'http.server' &&
+ transactionEvent?.transaction?.includes('/test-effect-span')
+ );
+ });
+
+ await fetch(`${baseURL}/test-effect-span`);
+
+ const transactionEvent = await transactionEventPromise;
+
+ expect(transactionEvent.contexts?.trace?.op).toBe('http.server');
+ expect(transactionEvent.transaction).toContain('/test-effect-span');
+
+ const spans = transactionEvent.spans || [];
+
+ expect(spans).toContainEqual(
+ expect.objectContaining({
+ description: 'custom-effect-span',
+ op: 'internal',
+ }),
+ );
+
+ 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);
+});
+
+test('Sends transaction for error route', async ({ baseURL }) => {
+ const transactionEventPromise = waitForTransaction('effect-node', transactionEvent => {
+ return (
+ transactionEvent?.contexts?.trace?.op === 'http.server' && transactionEvent?.transaction?.includes('/test-error')
+ );
+ });
+
+ await fetch(`${baseURL}/test-error`);
+
+ const transactionEvent = await transactionEventPromise;
+
+ expect(transactionEvent.contexts?.trace?.op).toBe('http.server');
+ expect(transactionEvent.transaction).toContain('/test-error');
+});
diff --git a/dev-packages/e2e-tests/test-applications/effect-node/tsconfig.json b/dev-packages/e2e-tests/test-applications/effect-node/tsconfig.json
new file mode 100644
index 000000000000..2cc9aca23e0e
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/effect-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"]
+}