Skip to content

Commit e90d6d3

Browse files
committed
feat(core): Apply ignoreSpans when span streaming is enabled
1 parent 5963170 commit e90d6d3

File tree

21 files changed

+768
-8
lines changed

21 files changed

+768
-8
lines changed
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import * as Sentry from '@sentry/browser';
2+
3+
window.Sentry = Sentry;
4+
5+
Sentry.init({
6+
dsn: 'https://public@dsn.ingest.sentry.io/1337',
7+
integrations: [Sentry.spanStreamingIntegration()],
8+
ignoreSpans: [/ignore/],
9+
tracesSampleRate: 1,
10+
});
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
Sentry.startSpan({ name: 'parent-span' }, () => {
2+
Sentry.startSpan({ name: 'keep-me' }, () => {});
3+
4+
// This child matches ignoreSpans — should be dropped
5+
Sentry.startSpan({ name: 'ignore-child' }, () => {
6+
// Grandchild should be reparented to 'parent-span'
7+
Sentry.startSpan({ name: 'grandchild-1' }, () => {});
8+
Sentry.startSpan({ name: 'grandchild-2' }, () => {});
9+
});
10+
11+
Sentry.startSpan({ name: 'another-keeper' }, () => {});
12+
});
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { expect } from '@playwright/test';
2+
import { sentryTest } from '../../../../utils/fixtures';
3+
import {
4+
envelopeRequestParser,
5+
hidePage,
6+
shouldSkipTracingTest,
7+
waitForClientReportRequest,
8+
} from '../../../../utils/helpers';
9+
import { waitForStreamedSpans } from '../../../../utils/spanUtils';
10+
import type { ClientReport } from '@sentry/core';
11+
12+
sentryTest(
13+
'ignored child spans are dropped and their children are reparented to the grandparent',
14+
async ({ getLocalTestUrl, page }) => {
15+
if (shouldSkipTracingTest()) {
16+
sentryTest.skip();
17+
}
18+
19+
const spansPromise = waitForStreamedSpans(page, spans => !!spans?.find(s => s.name === 'parent-span'));
20+
21+
const clientReportPromise = waitForClientReportRequest(page);
22+
23+
const url = await getLocalTestUrl({ testDir: __dirname });
24+
await page.goto(url);
25+
26+
const spans = await spansPromise;
27+
28+
await hidePage(page);
29+
30+
const clientReport = envelopeRequestParser<ClientReport>(await clientReportPromise);
31+
32+
const segmentSpanId = spans.find(s => s.name === 'parent-span')?.span_id;
33+
34+
expect(spans.some(s => s.name === 'keep-me')).toBe(true);
35+
expect(spans.some(s => s.name === 'another-keeper')).toBe(true);
36+
37+
expect(spans.some(s => s.name?.includes('ignore'))).toBe(false);
38+
39+
const grandchild1 = spans.find(s => s.name === 'grandchild-1');
40+
const grandchild2 = spans.find(s => s.name === 'grandchild-2');
41+
expect(grandchild1).toBeDefined();
42+
expect(grandchild2).toBeDefined();
43+
44+
expect(grandchild1?.parent_span_id).toBe(segmentSpanId);
45+
expect(grandchild2?.parent_span_id).toBe(segmentSpanId);
46+
47+
expect(clientReport.discarded_events).toEqual([
48+
{
49+
category: 'span',
50+
quantity: 1,
51+
reason: 'ignored',
52+
},
53+
]);
54+
},
55+
);
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import * as Sentry from '@sentry/browser';
2+
3+
window.Sentry = Sentry;
4+
5+
Sentry.init({
6+
dsn: 'https://public@dsn.ingest.sentry.io/1337',
7+
integrations: [Sentry.spanStreamingIntegration()],
8+
ignoreSpans: [/ignore/],
9+
tracesSampleRate: 1,
10+
debug: true,
11+
});
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// This segment span matches ignoreSpans — should NOT produce a transaction
2+
Sentry.startSpan({ name: 'ignore-segment' }, () => {
3+
Sentry.startSpan({ name: 'child-of-ignored-segment' }, () => {});
4+
});
5+
6+
setTimeout(() => {
7+
// This segment span does NOT match — should produce a transaction
8+
Sentry.startSpan({ name: 'normal-segment' }, () => {
9+
Sentry.startSpan({ name: 'child-span' }, () => {});
10+
});
11+
}, 1000);
12+
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { expect } from '@playwright/test';
2+
import { sentryTest } from '../../../../utils/fixtures';
3+
import {
4+
envelopeRequestParser,
5+
hidePage,
6+
shouldSkipTracingTest,
7+
waitForClientReportRequest,
8+
} from '../../../../utils/helpers';
9+
import { observeStreamedSpan, waitForStreamedSpans } from '../../../../utils/spanUtils';
10+
import type { ClientReport } from '@sentry/core';
11+
12+
sentryTest('ignored segment span drops entire trace', async ({ getLocalTestUrl, page }) => {
13+
if (shouldSkipTracingTest()) {
14+
sentryTest.skip();
15+
}
16+
17+
const url = await getLocalTestUrl({ testDir: __dirname });
18+
19+
observeStreamedSpan(page, span => {
20+
if (span.name === 'ignore-segment' || span.name === 'child-of-ignored-segment') {
21+
throw new Error('Ignored span found');
22+
}
23+
return false; // means we keep on looking for unwanted spans
24+
});
25+
26+
const spansPromise = waitForStreamedSpans(page, spans => !!spans?.find(s => s.name === 'normal-segment'));
27+
28+
const clientReportPromise = waitForClientReportRequest(page);
29+
30+
await page.goto(url);
31+
32+
expect((await spansPromise)?.length).toBe(2);
33+
34+
await hidePage(page);
35+
36+
const clientReport = envelopeRequestParser<ClientReport>(await clientReportPromise);
37+
38+
expect(clientReport.discarded_events).toEqual([
39+
{
40+
category: 'span',
41+
quantity: 2, // segment + child span
42+
reason: 'ignored',
43+
},
44+
]);
45+
});
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
const { loggingTransport } = require('@sentry-internal/node-integration-tests');
2+
const Sentry = require('@sentry/node');
3+
4+
Sentry.init({
5+
dsn: 'https://public@dsn.ingest.sentry.io/1337',
6+
release: '1.0',
7+
tracePropagationTargets: [/^(?!.*test).*$/],
8+
tracesSampleRate: 1.0,
9+
transport: loggingTransport,
10+
traceLifecycle: 'stream',
11+
ignoreSpans: ['middleware - expressInit', 'custom-to-drop'],
12+
clientReportFlushInterval: 1_000,
13+
});
14+
15+
const express = require('express');
16+
const cors = require('cors');
17+
const { startExpressServerAndSendPortToRunner } = require('@sentry-internal/node-integration-tests');
18+
19+
const app = express();
20+
21+
app.use(cors());
22+
23+
app.get('/test/express', (_req, res) => {
24+
Sentry.startSpan({
25+
name: 'custom-to-drop',
26+
op: 'custom',
27+
}, () => {
28+
Sentry.startSpan({
29+
name: 'custom',
30+
op: 'custom',
31+
}, () => {});
32+
});
33+
res.send({ response: 'response 1' });
34+
});
35+
36+
Sentry.setupExpressErrorHandler(app);
37+
38+
startExpressServerAndSendPortToRunner(app);
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import { afterAll, describe, expect, test } from 'vitest';
2+
import { cleanupChildProcesses, createRunner } from '../../../../utils/runner';
3+
import { SEMANTIC_ATTRIBUTE_SENTRY_OP } from '@sentry/core';
4+
5+
describe('filtering child spans with ignoreSpans (streaming)', () => {
6+
afterAll(() => {
7+
cleanupChildProcesses();
8+
});
9+
10+
describe('CJS', () => {
11+
test('child spans are dropped and remaining spans correctly parented', async () => {
12+
const runner = createRunner(__dirname, 'server.js')
13+
.expect({
14+
span: container => {
15+
// 5 spans: 1 root, 2 middleware, 1 request handler, 1 custom
16+
// Would be 7 if we didn't ignore the 'middleware - expressInit' and 'custom-to-drop' spans
17+
expect(container.items).toHaveLength(5);
18+
const getSpan = (name: string, op: string) =>
19+
container.items.find(
20+
item => item.name === name && item.attributes?.[SEMANTIC_ATTRIBUTE_SENTRY_OP]?.value === op,
21+
);
22+
const queryMiddlewareSpan = getSpan('query', 'middleware.express');
23+
const corsMiddlewareSpan = getSpan('corsMiddleware', 'middleware.express');
24+
const requestHandlerSpan = getSpan('/test/express', 'request_handler.express');
25+
const httpServerSpan = getSpan('GET /test/express', 'http.server');
26+
const customSpan = getSpan('custom', 'custom');
27+
28+
expect(queryMiddlewareSpan).toBeDefined();
29+
expect(corsMiddlewareSpan).toBeDefined();
30+
expect(requestHandlerSpan).toBeDefined();
31+
expect(httpServerSpan).toBeDefined();
32+
expect(customSpan).toBeDefined();
33+
34+
expect(customSpan?.parent_span_id).toBe(requestHandlerSpan?.span_id);
35+
expect(requestHandlerSpan?.parent_span_id).toBe(httpServerSpan?.span_id);
36+
expect(queryMiddlewareSpan?.parent_span_id).toBe(httpServerSpan?.span_id);
37+
expect(corsMiddlewareSpan?.parent_span_id).toBe(httpServerSpan?.span_id);
38+
expect(httpServerSpan?.parent_span_id).toBeUndefined();
39+
},
40+
})
41+
.start();
42+
43+
runner.makeRequest('get', '/test/express');
44+
45+
await runner.completed();
46+
});
47+
48+
test('client report contains discarded spans', async () => {
49+
const runner = createRunner(__dirname, 'server.js')
50+
.ignore('span')
51+
.unignore('client_report')
52+
.expect({
53+
client_report: {
54+
discarded_events: [
55+
{
56+
category: 'span',
57+
quantity: 2,
58+
reason: 'ignored',
59+
},
60+
],
61+
},
62+
})
63+
.start();
64+
65+
runner.makeRequest('get', '/test/express');
66+
67+
await runner.completed();
68+
});
69+
});
70+
});
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
const { loggingTransport } = require('@sentry-internal/node-integration-tests');
2+
const Sentry = require('@sentry/node');
3+
4+
Sentry.init({
5+
dsn: 'https://public@dsn.ingest.sentry.io/1337',
6+
release: '1.0',
7+
tracePropagationTargets: [/^(?!.*test).*$/],
8+
tracesSampleRate: 1.0,
9+
transport: loggingTransport,
10+
traceLifecycle: 'stream',
11+
ignoreSpans: [/\/health/],
12+
clientReportFlushInterval: 1_000,
13+
});
14+
15+
const express = require('express');
16+
const cors = require('cors');
17+
const { startExpressServerAndSendPortToRunner } = require('@sentry-internal/node-integration-tests');
18+
19+
const app = express();
20+
21+
app.use(cors());
22+
23+
app.get('/health', (_req, res) => {
24+
res.send({ status: 'ok' });
25+
});
26+
27+
app.get('/ok', (_req, res) => {
28+
res.send({ status: 'ok' });
29+
});
30+
31+
Sentry.setupExpressErrorHandler(app);
32+
33+
startExpressServerAndSendPortToRunner(app);
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { afterAll, describe, expect, test } from 'vitest';
2+
import { cleanupChildProcesses, createRunner } from '../../../../utils/runner';
3+
import { SEMANTIC_ATTRIBUTE_SENTRY_OP } from '@sentry/core';
4+
5+
describe('filtering segment spans with ignoreSpans (streaming)', () => {
6+
afterAll(() => {
7+
cleanupChildProcesses();
8+
});
9+
10+
describe('CJS', () => {
11+
test('segment spans matching ignoreSpans are dropped including all children', async () => {
12+
const runner = createRunner(__dirname, 'server.js')
13+
.unignore('client_report')
14+
.expect({
15+
client_report: {
16+
discarded_events: [
17+
{
18+
category: 'span',
19+
quantity: 1,
20+
reason: 'ignored',
21+
},
22+
],
23+
},
24+
})
25+
.expect({
26+
span: {
27+
items: expect.arrayContaining([
28+
expect.objectContaining({
29+
name: 'GET /ok',
30+
attributes: expect.objectContaining({
31+
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: {
32+
value: 'http.server',
33+
type: 'string',
34+
},
35+
}),
36+
}),
37+
]),
38+
},
39+
})
40+
.start();
41+
42+
runner.makeRequest('get', '/health');
43+
runner.makeRequest('get', '/ok');
44+
45+
await runner.completed();
46+
});
47+
});
48+
});

0 commit comments

Comments
 (0)