Skip to content

Commit 8c2378e

Browse files
authored
feat(effect): Add E2E tests for the @sentry/effect SDK (#19763)
This adds Node and Browser tests for the `@sentry/effect` SDK. I am not sure what to do with the browser part, as there is I guess no tree-shaking available right now. The basic usage for node and browser are the exact same, only the `effectLayer` has to be added into the runtime layer.
1 parent 3070535 commit 8c2378e

File tree

22 files changed

+990
-0
lines changed

22 files changed

+990
-0
lines changed
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# dependencies
2+
/node_modules
3+
/.pnp
4+
.pnp.js
5+
6+
# testing
7+
/coverage
8+
9+
# production
10+
/build
11+
/dist
12+
13+
# misc
14+
.DS_Store
15+
.env.local
16+
.env.development.local
17+
.env.test.local
18+
.env.production.local
19+
20+
npm-debug.log*
21+
yarn-debug.log*
22+
yarn-error.log*
23+
24+
/test-results/
25+
/playwright-report/
26+
/playwright/.cache/
27+
28+
!*.d.ts
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
@sentry:registry=http://127.0.0.1:4873
2+
@sentry-internal:registry=http://127.0.0.1:4873
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import * as path from 'path';
2+
import * as url from 'url';
3+
import HtmlWebpackPlugin from 'html-webpack-plugin';
4+
import TerserPlugin from 'terser-webpack-plugin';
5+
import webpack from 'webpack';
6+
7+
const __dirname = path.dirname(url.fileURLToPath(import.meta.url));
8+
9+
webpack(
10+
{
11+
entry: path.join(__dirname, 'src/index.js'),
12+
output: {
13+
path: path.join(__dirname, 'build'),
14+
filename: 'app.js',
15+
},
16+
optimization: {
17+
minimize: true,
18+
minimizer: [new TerserPlugin()],
19+
},
20+
plugins: [
21+
new webpack.EnvironmentPlugin(['E2E_TEST_DSN']),
22+
new HtmlWebpackPlugin({
23+
template: path.join(__dirname, 'public/index.html'),
24+
}),
25+
],
26+
performance: {
27+
hints: false,
28+
},
29+
mode: 'production',
30+
},
31+
(err, stats) => {
32+
if (err) {
33+
console.error(err.stack || err);
34+
if (err.details) {
35+
console.error(err.details);
36+
}
37+
return;
38+
}
39+
40+
const info = stats.toJson();
41+
42+
if (stats.hasErrors()) {
43+
console.error(info.errors);
44+
process.exit(1);
45+
}
46+
47+
if (stats.hasWarnings()) {
48+
console.warn(info.warnings);
49+
process.exit(1);
50+
}
51+
},
52+
);
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
{
2+
"name": "effect-browser-test-app",
3+
"version": "1.0.0",
4+
"private": true,
5+
"scripts": {
6+
"start": "serve -s build",
7+
"build": "node build.mjs",
8+
"test": "playwright test",
9+
"clean": "npx rimraf node_modules pnpm-lock.yaml",
10+
"test:build": "pnpm install && pnpm build",
11+
"test:assert": "pnpm test"
12+
},
13+
"dependencies": {
14+
"@sentry/effect": "latest || *",
15+
"@types/node": "^18.19.1",
16+
"effect": "^3.19.19",
17+
"typescript": "~5.0.0"
18+
},
19+
"devDependencies": {
20+
"@playwright/test": "~1.56.0",
21+
"@sentry-internal/test-utils": "link:../../../test-utils",
22+
"webpack": "^5.91.0",
23+
"serve": "14.0.1",
24+
"terser-webpack-plugin": "^5.3.10",
25+
"html-webpack-plugin": "^5.6.0"
26+
},
27+
"browserslist": {
28+
"production": [
29+
">0.2%",
30+
"not dead",
31+
"not op_mini all"
32+
],
33+
"development": [
34+
"last 1 chrome version",
35+
"last 1 firefox version",
36+
"last 1 safari version"
37+
]
38+
},
39+
"volta": {
40+
"extends": "../../package.json"
41+
}
42+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { getPlaywrightConfig } from '@sentry-internal/test-utils';
2+
3+
const config = getPlaywrightConfig({
4+
startCommand: `pnpm start`,
5+
});
6+
7+
export default config;
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
6+
<title>Effect Browser App</title>
7+
</head>
8+
<body>
9+
<h1>Effect Browser E2E Test</h1>
10+
11+
<div id="app">
12+
<section>
13+
<h2>Error Tests</h2>
14+
<input type="button" value="Capture Exception" id="exception-button" />
15+
</section>
16+
17+
<section>
18+
<h2>Effect Span Tests</h2>
19+
<input type="button" value="Create Effect Span" id="effect-span-button" />
20+
<span id="effect-span-result"></span>
21+
</section>
22+
23+
<section>
24+
<h2>Effect Failure Tests</h2>
25+
<input type="button" value="Effect.fail()" id="effect-fail-button" />
26+
<span id="effect-fail-result"></span>
27+
<br />
28+
<input type="button" value="Effect.die()" id="effect-die-button" />
29+
<span id="effect-die-result"></span>
30+
</section>
31+
32+
<section>
33+
<h2>Log Tests</h2>
34+
<input type="button" value="Send Logs" id="log-button" />
35+
<span id="log-result"></span>
36+
<br />
37+
<input type="button" value="Send Log with Context" id="log-context-button" />
38+
<span id="log-context-result"></span>
39+
</section>
40+
41+
<section id="navigation">
42+
<h2>Navigation Test</h2>
43+
<a id="navigation-link" href="#navigation-target">Navigation Link</a>
44+
<div id="navigation-target" style="margin-top: 50px">Navigated Element</div>
45+
</section>
46+
</div>
47+
</body>
48+
</html>
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
// @ts-check
2+
import * as Sentry from '@sentry/effect';
3+
import { Cause, Effect, Layer, Logger, LogLevel, Runtime } from 'effect';
4+
5+
const LogLevelLive = Logger.minimumLogLevel(LogLevel.Debug);
6+
const AppLayer = Layer.mergeAll(
7+
Sentry.effectLayer({
8+
dsn: process.env.E2E_TEST_DSN,
9+
integrations: [
10+
Sentry.browserTracingIntegration({
11+
_experiments: { enableInteractions: true },
12+
}),
13+
],
14+
tracesSampleRate: 1.0,
15+
release: 'e2e-test',
16+
environment: 'qa',
17+
tunnel: 'http://localhost:3031',
18+
enableLogs: true,
19+
enableEffectLogs: true,
20+
}),
21+
LogLevelLive,
22+
);
23+
24+
const runtime = Layer.toRuntime(AppLayer).pipe(Effect.scoped, Effect.runSync);
25+
26+
const runEffect = fn => Runtime.runPromise(runtime)(fn());
27+
28+
document.getElementById('exception-button')?.addEventListener('click', () => {
29+
throw new Error('I am an error!');
30+
});
31+
32+
document.getElementById('effect-span-button')?.addEventListener('click', async () => {
33+
await runEffect(() =>
34+
Effect.gen(function* () {
35+
yield* Effect.sleep('50 millis');
36+
yield* Effect.sleep('25 millis').pipe(Effect.withSpan('nested-span'));
37+
}).pipe(Effect.withSpan('custom-effect-span', { kind: 'internal' })),
38+
);
39+
const el = document.getElementById('effect-span-result');
40+
if (el) el.textContent = 'Span sent!';
41+
});
42+
43+
document.getElementById('effect-fail-button')?.addEventListener('click', async () => {
44+
try {
45+
await runEffect(() => Effect.fail(new Error('Effect failure')));
46+
} catch {
47+
const el = document.getElementById('effect-fail-result');
48+
if (el) el.textContent = 'Effect failed (expected)';
49+
}
50+
});
51+
52+
document.getElementById('effect-die-button')?.addEventListener('click', async () => {
53+
try {
54+
await runEffect(() => Effect.die('Effect defect'));
55+
} catch {
56+
const el = document.getElementById('effect-die-result');
57+
if (el) el.textContent = 'Effect died (expected)';
58+
}
59+
});
60+
61+
document.getElementById('log-button')?.addEventListener('click', async () => {
62+
await runEffect(() =>
63+
Effect.gen(function* () {
64+
yield* Effect.logDebug('Debug log from Effect');
65+
yield* Effect.logInfo('Info log from Effect');
66+
yield* Effect.logWarning('Warning log from Effect');
67+
yield* Effect.logError('Error log from Effect');
68+
}),
69+
);
70+
const el = document.getElementById('log-result');
71+
if (el) el.textContent = 'Logs sent!';
72+
});
73+
74+
document.getElementById('log-context-button')?.addEventListener('click', async () => {
75+
await runEffect(() =>
76+
Effect.logInfo('Log with context').pipe(
77+
Effect.annotateLogs('userId', '12345'),
78+
Effect.annotateLogs('action', 'test'),
79+
),
80+
);
81+
const el = document.getElementById('log-context-result');
82+
if (el) el.textContent = 'Log with context sent!';
83+
});
84+
85+
document.getElementById('navigation-link')?.addEventListener('click', () => {
86+
document.getElementById('navigation-target')?.scrollIntoView({ behavior: 'smooth' });
87+
});
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { startEventProxyServer } from '@sentry-internal/test-utils';
2+
3+
startEventProxyServer({
4+
port: 3031,
5+
proxyServerName: 'effect-browser',
6+
});
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { expect, test } from '@playwright/test';
2+
import { waitForError, waitForTransaction } from '@sentry-internal/test-utils';
3+
4+
test('captures an error', async ({ page }) => {
5+
const errorEventPromise = waitForError('effect-browser', event => {
6+
return !event.type && event.exception?.values?.[0]?.value === 'I am an error!';
7+
});
8+
9+
await page.goto('/');
10+
11+
const exceptionButton = page.locator('id=exception-button');
12+
await exceptionButton.click();
13+
14+
const errorEvent = await errorEventPromise;
15+
16+
expect(errorEvent.exception?.values).toHaveLength(1);
17+
expect(errorEvent.exception?.values?.[0]?.value).toBe('I am an error!');
18+
expect(errorEvent.transaction).toBe('/');
19+
20+
expect(errorEvent.request).toEqual({
21+
url: 'http://localhost:3030/',
22+
headers: expect.any(Object),
23+
});
24+
25+
expect(errorEvent.contexts?.trace).toEqual({
26+
trace_id: expect.stringMatching(/[a-f0-9]{32}/),
27+
span_id: expect.stringMatching(/[a-f0-9]{16}/),
28+
});
29+
});
30+
31+
test('sets correct transactionName', async ({ page }) => {
32+
const transactionPromise = waitForTransaction('effect-browser', async transactionEvent => {
33+
return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'pageload';
34+
});
35+
36+
const errorEventPromise = waitForError('effect-browser', event => {
37+
return !event.type && event.exception?.values?.[0]?.value === 'I am an error!';
38+
});
39+
40+
await page.goto('/');
41+
const transactionEvent = await transactionPromise;
42+
43+
const exceptionButton = page.locator('id=exception-button');
44+
await exceptionButton.click();
45+
46+
const errorEvent = await errorEventPromise;
47+
48+
expect(errorEvent.exception?.values).toHaveLength(1);
49+
expect(errorEvent.exception?.values?.[0]?.value).toBe('I am an error!');
50+
expect(errorEvent.transaction).toEqual('/');
51+
52+
expect(errorEvent.contexts?.trace).toEqual({
53+
trace_id: transactionEvent.contexts?.trace?.trace_id,
54+
span_id: expect.not.stringContaining(transactionEvent.contexts?.trace?.span_id || ''),
55+
});
56+
});

0 commit comments

Comments
 (0)