From 3c32fdf98b8ac652b970cd59ff1bc0446e7d987c Mon Sep 17 00:00:00 2001 From: s1gr1d Date: Mon, 8 Apr 2024 09:46:16 +0200 Subject: [PATCH] Do not sample `options` and `head` requests --- packages/opentelemetry/src/sampler.ts | 13 ++++- packages/opentelemetry/test/trace.test.ts | 71 +++++++++++++++++++++++ 2 files changed, 83 insertions(+), 1 deletion(-) diff --git a/packages/opentelemetry/src/sampler.ts b/packages/opentelemetry/src/sampler.ts index 228c22bbb29d..e611d6a43395 100644 --- a/packages/opentelemetry/src/sampler.ts +++ b/packages/opentelemetry/src/sampler.ts @@ -8,13 +8,14 @@ import type { Client, SpanAttributes } from '@sentry/types'; import { logger } from '@sentry/utils'; import { SENTRY_TRACE_STATE_SAMPLED_NOT_RECORDING } from './constants'; +import { SemanticAttributes } from '@opentelemetry/semantic-conventions'; import { DEBUG_BUILD } from './debug-build'; import { getPropagationContextFromSpan } from './propagator'; import { getSamplingDecision } from './utils/getSamplingDecision'; import { setIsSetup } from './utils/setupCheck'; /** - * A custom OTEL sampler that uses Sentry sampling rates to make it's decision + * A custom OTEL sampler that uses Sentry sampling rates to make its decision */ export class SentrySampler implements Sampler { private _client: Client; @@ -72,6 +73,16 @@ export class SentrySampler implements Sampler { [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: sampleRate, }; + const method = `${spanAttributes[SemanticAttributes.HTTP_METHOD]}`.toUpperCase(); + if (method === 'OPTIONS' || method === 'HEAD') { + DEBUG_BUILD && logger.log(`[Tracing] Not sampling span because HTTP method is '${method}' for ${spanName}`); + return { + decision: SamplingDecision.NOT_RECORD, + attributes, + traceState: traceState.set(SENTRY_TRACE_STATE_SAMPLED_NOT_RECORDING, '1'), + }; + } + if (!sampled) { return { decision: SamplingDecision.NOT_RECORD, diff --git a/packages/opentelemetry/test/trace.test.ts b/packages/opentelemetry/test/trace.test.ts index f02bd4b5673e..a871fe1fbeba 100644 --- a/packages/opentelemetry/test/trace.test.ts +++ b/packages/opentelemetry/test/trace.test.ts @@ -20,6 +20,7 @@ import { import type { Event, Scope } from '@sentry/types'; import { makeTraceState } from '../src/propagator'; +import { SemanticAttributes } from '@opentelemetry/semantic-conventions'; import { continueTrace, startInactiveSpan, startSpan, startSpanManual } from '../src/trace'; import type { AbstractSpan } from '../src/types'; import { getDynamicSamplingContextFromSpan } from '../src/utils/dynamicSamplingContext'; @@ -1358,6 +1359,76 @@ describe('trace (sampling)', () => { }); }); +describe('HTTP methods (sampling)', () => { + beforeEach(() => { + mockSdkInit({ enableTracing: true }); + }); + + afterEach(() => { + cleanupOtel(); + }); + + it('does sample when HTTP method is other than OPTIONS or HEAD', () => { + const spanGET = startSpanManual( + { name: 'test span', attributes: { [SemanticAttributes.HTTP_METHOD]: 'GET' } }, + span => { + return span; + }, + ); + expect(spanIsSampled(spanGET)).toBe(true); + expect(getSamplingDecision(spanGET.spanContext())).toBe(true); + + const spanPOST = startSpanManual( + { name: 'test span', attributes: { [SemanticAttributes.HTTP_METHOD]: 'POST' } }, + span => { + return span; + }, + ); + expect(spanIsSampled(spanPOST)).toBe(true); + expect(getSamplingDecision(spanPOST.spanContext())).toBe(true); + + const spanPUT = startSpanManual( + { name: 'test span', attributes: { [SemanticAttributes.HTTP_METHOD]: 'PUT' } }, + span => { + return span; + }, + ); + expect(spanIsSampled(spanPUT)).toBe(true); + expect(getSamplingDecision(spanPUT.spanContext())).toBe(true); + + const spanDELETE = startSpanManual( + { name: 'test span', attributes: { [SemanticAttributes.HTTP_METHOD]: 'DELETE' } }, + span => { + return span; + }, + ); + expect(spanIsSampled(spanDELETE)).toBe(true); + expect(getSamplingDecision(spanDELETE.spanContext())).toBe(true); + }); + + it('does not sample when HTTP method is OPTIONS', () => { + const span = startSpanManual( + { name: 'test span', attributes: { [SemanticAttributes.HTTP_METHOD]: 'OPTIONS' } }, + span => { + return span; + }, + ); + expect(spanIsSampled(span)).toBe(false); + expect(getSamplingDecision(span.spanContext())).toBe(false); + }); + + it('does not sample when HTTP method is HEAD', () => { + const span = startSpanManual( + { name: 'test span', attributes: { [SemanticAttributes.HTTP_METHOD]: 'HEAD' } }, + span => { + return span; + }, + ); + expect(spanIsSampled(span)).toBe(false); + expect(getSamplingDecision(span.spanContext())).toBe(false); + }); +}); + describe('continueTrace', () => { beforeEach(() => { mockSdkInit({ enableTracing: true });