8000 feat(otel): Ignore outgoing Sentry HTTP requests from otel integratio… · yongdamsh/sentry-javascript@b55dd07 · GitHub
[go: up one dir, main page]

Skip to content

Commit b55dd07

Browse files
authored
feat(otel): Ignore outgoing Sentry HTTP requests from otel integration (getsentry#6116)
1 parent f36c268 commit b55dd07

File tree

4 files changed

+154
-6
lines changed

4 files changed

+154
-6
lines changed

packages/opentelemetry-node/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@
3333
"@opentelemetry/core": "^1.7.0",
3434
"@opentelemetry/sdk-trace-base": "^1.7.0",
3535
"@opentelemetry/sdk-trace-node": "^1.7.0",
36-
"@opentelemetry/semantic-conventions": "^1.7.0"
36+
"@opentelemetry/semantic-conventions": "^1.7.0",
37+
"@sentry/node": "7.17.3"
3738
},
3839
"scripts": {
3940
"build": "run-p build:rollup build:types",

packages/opentelemetry-node/src/spanprocessor.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { Transaction } from '@sentry/tracing';
55
import { Span as SentrySpan, TransactionContext } from '@sentry/types';
66
import { logger } from '@sentry/utils';
77

8+
import { isSentryRequestSpan } from './utils/is-sentry-request';
89
import { mapOtelStatus } from './utils/map-otel-status';
910
import { parseSpanDescription } from './utils/parse-otel-span-description';
1011

@@ -33,9 +34,6 @@ export class SentrySpanProcessor implements OtelSpanProcessor {
3334
return;
3435
}
3536

36-
// TODO: handle sentry requests
37-
// if isSentryRequest(otelSpan) return;
38-
3937
const otelSpanId = otelSpan.spanContext().spanId;
4038
const otelParentSpanId = otelSpan.parentSpanId;
4139

@@ -79,6 +77,16 @@ export class SentrySpanProcessor implements OtelSpanProcessor {
7977
return;
8078
}
8179

80+
// Auto-instrumentation often captures outgoing HTTP requests
81+
// This means that Sentry HTTP requests created by this integration can, in turn, be captured by OTEL auto instrumentation,
82+
// leading to an infinite loop.
83+
// In this case, we do not want to finish the span, in order to avoid sending it to Sentry
84+
if (isSentryRequestSpan(otelSpan)) {
85+
// Make sure to remove any references, so this can be GCed
86+
this._map.delete(otelSpanId);
87+
return;
88+
}
89+
8290
if (sentrySpan instanceof Transaction) {
8391
updateTransactionWithOtelData(sentrySpan, otelSpan);
8492
finishTransactionWithContextFromOtelData(sentrySpan, otelSpan);
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { Span as OtelSpan } from '@opentelemetry/sdk-trace-base';
2+
import { SemanticAttributes } from '@opentelemetry/semantic-conventions';
3+
import { getCurrentHub } from '@sentry/core';
4+
5+
/**
6+
*
7+
* @param otelSpan Checks wheter a given OTEL Span is an http request to sentry.
8+
* @returns boolean
9+
*/
10+
export function isSentryRequestSpan(otelSpan: OtelSpan): boolean {
11+
const { attributes } = otelSpan;
12+
13+
const httpUrl = attributes[SemanticAttributes.HTTP_URL];
14+
15+
if (!httpUrl) {
16+
return false;
17+
}
18+
19+
return isSentryRequestUrl(httpUrl.toString());
20+
}
21+
22+
/**
23+
* Checks whether given url points to Sentry server
24+
* @param url url to verify
25+
*/
26+
function isSentryRequestUrl(url: string): boolean {
27+
const dsn = getCurrentHub().getClient()?.getDsn();
28+
return dsn ? url.includes(dsn.host) : false;
29+
}

packages/opentelemetry-node/test/spanprocessor.test.ts

Lines changed: 112 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,16 @@ import { Resource } from '@opentelemetry/resources';
44
import { Span as OtelSpan } from '@opentelemetry/sdk-trace-base';
55
import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node';
66
import { SemanticAttributes, SemanticResourceAttributes } from '@opentelemetry/semantic-conventions';
7-
import { Hub, makeMain } from '@sentry/core';
7+
import { createTransport, Hub, makeMain } from '@sentry/core';
8+
import { NodeClient } from '@sentry/node';
89
import { addExtensionMethods, Span as SentrySpan, SpanStatusType, Transaction } from '@sentry/tracing';
910
import { Contexts, Scope } from '@sentry/types';
11+
import { resolvedSyncPromise } from '@sentry/utils';
1012

1113
import { SENTRY_SPAN_PROCESSOR_MAP, SentrySpanProcessor } from '../src/spanprocessor';
1214

15+
const SENTRY_DSN = 'https://0@0.ingest.sentry.io/0';
16+
1317
// Integration Test of SentrySpanProcessor
1418

1519
beforeAll(() => {
@@ -22,7 +26,13 @@ describe('SentrySpanProcessor', () => {
2226
let spanProcessor: SentrySpanProcessor;
2327

2428
beforeEach(() => {
25-
hub = new Hub();
29+
const client = new NodeClient({
30+
dsn: SENTRY_DSN,
31+
integrations: [],
32+
transport: () => createTransport({ recordDroppedEvent: () => undefined }, _ => resolvedSyncPromise({})),
33+
stackParser: () => [],
34+
});
35+
hub = new Hub(client);
2636
makeMain(hub);
2737

2838
spanProcessor = new SentrySpanProcessor();
@@ -561,6 +571,106 @@ describe('SentrySpanProcessor', () => {
561571
});
562572
});
563573
});
574+
575+
describe('skip sentry requests', () => {
576+
it('does not finish transaction for Sentry request', async () => {
577+
const otelSpan = provider.getTracer('default').startSpan('POST to sentry', {
578+
attributes: {
579+
[SemanticAttributes.HTTP_METHOD]: 'POST',
580+
[SemanticAttributes.HTTP_URL]: `${SENTRY_DSN}/sub/route`,
581+
},
582+
}) as OtelSpan;
583+
584+
const sentrySpanTransaction = getSpanForOtelSpan(otelSpan) as Transaction | undefined;
585+
expect(sentrySpanTransaction).toBeDefined();
586+
587+
otelSpan.end();
588+
589+
expect(sentrySpanTransaction?.endTimestamp).toBeUndefined();
590+
591+
// Ensure it is still removed from map!
592+
expect(getSpanForOtelSpan(otelSpan)).toBeUndefined();
593+
});
594+
595+
it('finishes transaction for non-Sentry request', async () => {
596+
const otelSpan = provider.getTracer('default').startSpan('POST to sentry', {
597+
attributes: {
598+
[SemanticAttributes.HTTP_METHOD]: 'POST',
599+
[SemanticAttributes.HTTP_URL]: 'https://other.sentry.io/sub/route',
600+
},
601+
}) as OtelSpan;
602+
603+
const sentrySpanTransaction = getSpanForOtelSpan(otelSpan) as Transaction | undefined;
604+
expect(sentrySpanTransaction).toBeDefined();
605+
606+
otelSpan.end();
607+
608+
expect(sentrySpanTransaction?.endTimestamp).toBeDefined();
609+
});
610+
611+
it('does not finish spans for Sentry request', async () => {
612+
const tracer = provider.getTracer('default');
613+
614+
tracer.startActiveSpan('GET /users', () => {
615+
tracer.startActiveSpan(
616+
'SELECT * FROM users;',
617+
{
618+
attributes: {
619+
[SemanticAttributes.HTTP_METHOD]: 'POST',
620+
[SemanticAttributes.HTTP_URL]: `${SENTRY_DSN}/sub/route`,
621+
},
622+
},
623+
child => {
624+
const childOtelSpan = child as OtelSpan;
625+
626+
const sentrySpan = getSpanForOtelSpan(childOtelSpan);
627+
expect(sentrySpan).toBeDefined();
628+
629+
childOtelSpan.end();
630+
631+
expect(sentrySpan?.endTimestamp).toBeUndefined();
632+
633+
// Ensure it is still removed from map!
634+
expect(getSpanForOtelSpan(childOtelSpan)).toBeUndefined();
635+
},
636+
);
637+
});
638+
});
639+
640+
it('handles child spans of Sentry requests normally', async () => {
641+
const tracer = provider.getTracer('default');
642+
643+
tracer.startActiveSpan('GET /users', () => {
644+
tracer.startActiveSpan(
645+
'SELECT * FROM users;',
646+
{
647+
attributes: {
648+
[SemanticAttributes.HTTP_METHOD]: 'POST',
649+
[SemanticAttributes.HTTP_URL]: `${SENTRY_DSN}/sub/route`,
650+
},
651+
},
652+
child => {
653+
const childOtelSpan = child as OtelSpan;
654+
655+
const grandchildSpan = tracer.startSpan('child 1');
656+
657+
const sentrySpan = getSpanForOtelSpan(childOtelSpan);
658+
expect(sentrySpan).toBeDefined();
659+
660+
const sentryGrandchildSpan = getSpanForOtelSpan(grandchildSpan);
661+
expect(sentryGrandchildSpan).toBeDefined();
662+
663+
grandchildSpan.end();
664+
665+
childOtelSpan.end();
666+
667+
expect(sentryGrandchildSpan?.endTimestamp).toBeDefined();
668+
expect(sentrySpan?.endTimestamp).toBeUndefined();
669+
},
670+
);
671+
});
672+
});
673+
});
564674
});
565675

566676
// OTEL expects a custom date format

0 commit comments

Comments
 (0)
0