Skip to content

Commit 4d61a6d

Browse files
authored
test: Add e2e for node with custom otel sampler (#13034)
1 parent 36f62bb commit 4d61a6d

File tree

12 files changed

+348
-0
lines changed

12 files changed

+348
-0
lines changed

.github/workflows/build.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -990,6 +990,7 @@ jobs:
990990
'node-express-esm-without-loader',
991991
'node-express-cjs-preload',
992992
'node-otel-sdk-node',
993+
'node-otel-custom-sampler',
993994
'ember-classic',
994995
'ember-embroider',
995996
'nextjs-app-dir',
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
dist
2+
.vscode
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: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
{
2+
"name": "node-otel-custom-sampler",
3+
"version": "1.0.0",
4+
"private": true,
5+
"scripts": {
6+
"build": "tsc",
7+
"start": "node dist/app.js",
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+
"@opentelemetry/api": "^1.9.0",
15+
"@opentelemetry/sdk-trace-node": "^1.25.1",
16+
"@sentry/node": "latest || *",
17+
"@sentry/opentelemetry": "latest || *",
18+
"@sentry/types": "latest || *",
19+
"@types/express": "4.17.17",
20+
"@types/node": "18.15.1",
21+
"express": "4.19.2",
22+
"typescript": "4.9.5"
23+
},
24+
"devDependencies": {
25+
"@playwright/test": "^1.44.1",
26+
"@sentry-internal/test-utils": "link:../../../test-utils"
27+
},
28+
"volta": {
29+
"extends": "../../package.json"
30+
}
31+
}
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: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import './instrument';
2+
3+
import * as Sentry from '@sentry/node';
4+
import express from 'express';
5+
6+
const PORT = 3030;
7+
const app = express();
8+
9+
const wait = (duration: number) => {
10+
return new Promise<void>(res => {
11+
setTimeout(() => res(), duration);
12+
});
13+
};
14+
15+
app.get('/task', async (_req, res) => {
16+
await Sentry.startSpan({ name: 'Long task', op: 'custom.op' }, async () => {
17+
await wait(200);
18+
});
19+
res.send('ok');
20+
});
21+
22+
app.get('/unsampled/task', async (_req, res) => {
23+
await wait(200);
24+
res.send('ok');
25+
});
26+
27+
app.get('/test-error', async function (req, res) {
28+
const exceptionId = Sentry.captureException(new Error('This is an error'));
29+
30+
await Sentry.flush(2000);
31+
32+
res.send({ exceptionId });
33+
});
34+
35+
app.get('/test-exception/:id', function (req, _res) {
36+
throw new Error(`This is an exception with id ${req.params.id}`);
37+
});
38+
39+
Sentry.setupExpressErrorHandler(app);
40+
41+
app.use(function onError(err: unknown, req: any, res: any, next: any) {
42+
// The error id is attached to `res.sentry` to be returned
43+
// and optionally displayed to the user for support.
44+
res.statusCode = 500;
45+
res.end(res.sentry + '\n');
46+
});
47+
48+
app.listen(PORT, () => {
49+
console.log('App listening on ', PORT);
50+
});
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { Attributes, Context, Link, SpanKind } from '@opentelemetry/api';
2+
import { Sampler, SamplingResult } from '@opentelemetry/sdk-trace-node';
3+
import { wrapSamplingDecision } from '@sentry/opentelemetry';
4+
5+
export class CustomSampler implements Sampler {
6+
public shouldSample(
7+
context: Context,
8+
_traceId: string,
9+
_spanName: string,
10+
_spanKind: SpanKind,
11+
attributes: Attributes,
12+
_links: Link[],
13+
): SamplingResult {
14+
const route = attributes['http.route'];
15+
const target = attributes['http.target'];
16+
const decision =
17+
(typeof route === 'string' && route.includes('/unsampled')) ||
18+
(typeof target === 'string' && target.includes('/unsampled'))
19+
? 0
20+
: 1;
21+
return wrapSamplingDecision({
22+
decision,
23+
context,
24+
spanAttributes: attributes,
25+
});
26+
}
27+
28+
public toString(): string {
29+
return CustomSampler.name;
30+
}
31+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node';
2+
import * as Sentry from '@sentry/node';
3+
import { SentryPropagator, SentrySpanProcessor } from '@sentry/opentelemetry';
4+
import { CustomSampler } from './custom-sampler';
5+
6+
Sentry.init({
7+
environment: 'qa', // dynamic sampling bias to keep transactions
8+
dsn:
9+
process.env.E2E_TEST_DSN ||
10+
'https://3b6c388182fb435097f41d181be2b2ba@o4504321058471936.ingest.sentry.io/4504321066008576',
11+
includeLocalVariables: true,
12+
debug: !!process.env.DEBUG,
13+
tunnel: `http://localhost:3031/`, // proxy server
14+
skipOpenTelemetrySetup: true,
15+
// By defining _any_ sample rate, tracing intergations will be added by default
16+
tracesSampleRate: 0,
17+
});
18+
19+
const provider = new NodeTracerProvider({
20+
sampler: new CustomSampler(),
21+
});
22+
23+
provider.addSpanProcessor(new SentrySpanProcessor());
24+
25+
provider.register({
26+
propagator: new SentryPropagator(),
27+
contextManager: new Sentry.SentryContextManager(),
28+
});
29+
30+
Sentry.validateOpenTelemetrySetup();
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: 'node-otel-custom-sampler',
6+
});
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { expect, test } from '@playwright/test';
2+
import { waitForError } from '@sentry-internal/test-utils';
3+
4+
test('Sends correct error event', async ({ baseURL }) => {
5+
const errorEventPromise = waitForError('node-otel-custom-sampler', event => {
6+
return !event.type && event.exception?.values?.[0]?.value === 'This is an exception with id 123';
7+
});
8+
9+
await fetch(`${baseURL}/test-exception/123`);
10+
11+
const errorEvent = await errorEventPromise;
12+
13+
expect(errorEvent.exception?.values).toHaveLength(1);
14+
expect(errorEvent.exception?.values?.[0]?.value).toBe('This is an exception with id 123');
15+
16+
expect(errorEvent.request).toEqual({
17+
method: 'GET',
18+
cookies: {},
19+
headers: expect.any(Object),
20+
url: 'http://localhost:3030/test-exception/123',
21+
});
22+
23+
expect(errorEvent.transaction).toEqual('GET /test-exception/:id');
24+
25+
expect(errorEvent.contexts?.trace).toEqual({
26+
trace_id: expect.any(String),
27+
span_id: expect.any(String),
28+
});
29+
});

0 commit comments

Comments
 (0)