From cec2081b8e51bb1436b49f850575cafa54195e77 Mon Sep 17 00:00:00 2001 From: Kev Date: Wed, 5 Nov 2025 16:13:34 -0500 Subject: [PATCH 1/6] feat(tracemetrics): Add sample_rate parameter to metric API calls This allows for a sample_rate (0, 1.0] to be sent on a per metric basis. Refs LOGS-497 --- packages/core/src/metrics/internal.ts | 37 ++++ packages/core/src/metrics/public-api.ts | 25 ++- packages/core/src/types-hoist/clientreport.ts | 3 +- packages/core/src/types-hoist/metric.ts | 5 + packages/core/src/utils/lru.ts | 2 +- .../core/test/lib/metrics/public-api.test.ts | 175 +++++++++++++++++- 6 files changed, 237 insertions(+), 10 deletions(-) diff --git a/packages/core/src/metrics/internal.ts b/packages/core/src/metrics/internal.ts index efa204cac5a3..812d0a8800f0 100644 --- a/packages/core/src/metrics/internal.ts +++ b/packages/core/src/metrics/internal.ts @@ -77,6 +77,36 @@ function setMetricAttribute( } } +/** + * Validates and processes the sample_rate for a metric. + * + * @param metric - The metric containing the sample_rate to validate. + * @param client - The client to record dropped events with. + * @returns true if the sample_rate is valid, false if the metric should be dropped. + */ +function validateAndProcessSampleRate(metric: Metric, client: Client): boolean { + if (metric.sample_rate !== undefined) { + if (metric.sample_rate <= 0 || metric.sample_rate > 1.0) { + // Invalid sample rate - drop the metric entirely and record the lost event + client.recordDroppedEvent('invalid_sample_rate', 'metric'); + return false; + } + } + return true; +} + +/** + * Adds the sample_rate attribute to the metric attributes if needed. + * + * @param metric - The metric containing the sample_rate. + * @param attributes - The attributes object to modify. + */ +function addSampleRateAttribute(metric: Metric, attributes: Record): void { + if (metric.sample_rate !== undefined && metric.sample_rate !== 1.0) { + setMetricAttribute(attributes, 'sentry.client_sample_rate', metric.sample_rate); + } +} + /** * Captures a serialized metric event and adds it to the metric buffer for the given client. * @@ -133,6 +163,10 @@ export function _INTERNAL_captureMetric(beforeMetric: Metric, options?: Internal return; } + if (!validateAndProcessSampleRate(beforeMetric, client)) { + return; + } + const { release, environment, _experiments } = client.getOptions(); if (!_experiments?.enableMetrics) { DEBUG_BUILD && debug.warn('metrics option not enabled, metric will not be captured.'); @@ -145,6 +179,9 @@ export function _INTERNAL_captureMetric(beforeMetric: Metric, options?: Internal ...beforeMetric.attributes, }; + + addSampleRateAttribute(beforeMetric, processedMetricAttributes); + const { user: { id, email, username }, } = getMergedScopeData(currentScope); diff --git a/packages/core/src/metrics/public-api.ts b/packages/core/src/metrics/public-api.ts index e508fcb9e6d0..d17e6153416f 100644 --- a/packages/core/src/metrics/public-api.ts +++ b/packages/core/src/metrics/public-api.ts @@ -20,6 +20,11 @@ export interface MetricOptions { * The scope to capture the metric with. */ scope?: Scope; + + /** + * The sample rate for the metric. Must be a float between 0 (exclusive) and 1 (inclusive). + */ + sample_rate?: number; } /** @@ -32,7 +37,7 @@ export interface MetricOptions { */ function captureMetric(type: MetricType, name: string, value: number | string, options?: MetricOptions): void { _INTERNAL_captureMetric( - { type, name, value, unit: options?.unit, attributes: options?.attributes }, + { type, name, value, unit: options?.unit, attributes: options?.attributes, sample_rate: options?.sample_rate }, { scope: options?.scope }, ); } @@ -43,6 +48,7 @@ function captureMetric(type: MetricType, name: string, value: number | string, o * @param name - The name of the counter metric. * @param value - The value to increment by (defaults to 1). * @param options - Options for capturing the metric. + * @param options.sample_rate - Sample rate for the metric (0 < sample_rate <= 1.0). * * @example * @@ -56,14 +62,15 @@ function captureMetric(type: MetricType, name: string, value: number | string, o * }); * ``` * - * @example With custom value + * @example With custom value and sample rate * * ``` * Sentry.metrics.count('items.processed', 5, { * attributes: { * processor: 'batch-processor', * queue: 'high-priority' - * } + * }, + * sample_rate: 0.1 * }); * ``` */ @@ -77,6 +84,7 @@ export function count(name: string, value: number = 1, options?: MetricOptions): * @param name - The name of the gauge metric. * @param value - The current value of the gauge. * @param options - Options for capturing the metric. + * @param options.sample_rate - Sample rate for the metric (0 < sample_rate <= 1.0). * * @example * @@ -90,14 +98,15 @@ export function count(name: string, value: number = 1, options?: MetricOptions): * }); * ``` * - * @example Without unit + * @example With sample rate * * ``` * Sentry.metrics.gauge('active.connections', 42, { * attributes: { * server: 'api-1', * protocol: 'websocket' - * } + * }, + * sample_rate: 0.5 * }); * ``` */ @@ -111,6 +120,7 @@ export function gauge(name: string, value: number, options?: MetricOptions): voi * @param name - The name of the distribution metric. * @param value - The value to record in the distribution. * @param options - Options for capturing the metric. + * @param options.sample_rate - Sample rate for the metric (0 < sample_rate <= 1.0). * * @example * @@ -124,14 +134,15 @@ export function gauge(name: string, value: number, options?: MetricOptions): voi * }); * ``` * - * @example Without unit + * @example With sample rate * * ``` * Sentry.metrics.distribution('batch.size', 100, { * attributes: { * processor: 'batch-1', * type: 'async' - * } + * }, + * sample_rate: 0.25 * }); * ``` */ diff --git a/packages/core/src/types-hoist/clientreport.ts b/packages/core/src/types-hoist/clientreport.ts index 069adec43c62..a29e95f333dd 100644 --- a/packages/core/src/types-hoist/clientreport.ts +++ b/packages/core/src/types-hoist/clientreport.ts @@ -9,7 +9,8 @@ export type EventDropReason = | 'sample_rate' | 'send_error' | 'internal_sdk_error' - | 'buffer_overflow'; + | 'buffer_overflow' + | 'invalid_sample_rate'; export type Outcome = { reason: EventDropReason; diff --git a/packages/core/src/types-hoist/metric.ts b/packages/core/src/types-hoist/metric.ts index 9201243c4a38..fb8b8d9b20cb 100644 --- a/packages/core/src/types-hoist/metric.ts +++ b/packages/core/src/types-hoist/metric.ts @@ -25,6 +25,11 @@ export interface Metric { * Arbitrary structured data that stores information about the metric. */ attributes?: Record; + + /** + * The sample rate for the metric. Must be a float between 0 (exclusive) and 1 (inclusive). + */ + sample_rate?: number; } export type SerializedMetricAttributeValue = diff --git a/packages/core/src/utils/lru.ts b/packages/core/src/utils/lru.ts index 3158dff7d413..5afad5a4b7e0 100644 --- a/packages/core/src/utils/lru.ts +++ b/packages/core/src/utils/lru.ts @@ -28,7 +28,7 @@ export class LRUMap { if (this._cache.size >= this._maxSize) { // keys() returns an iterator in insertion order so keys().next() gives us the oldest key // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const nextKey = this._cache.keys().next().value!; + const nextKey = this._cache.keys().next().value; this._cache.delete(nextKey); } this._cache.set(key, value); diff --git a/packages/core/test/lib/metrics/public-api.test.ts b/packages/core/test/lib/metrics/public-api.test.ts index 42fe7c41ae4a..f0255085a8ef 100644 --- a/packages/core/test/lib/metrics/public-api.test.ts +++ b/packages/core/test/lib/metrics/public-api.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from 'vitest'; +import { describe, expect, it, vi } from 'vitest'; import { Scope } from '../../../src'; import { _INTERNAL_getMetricBuffer } from '../../../src/metrics/internal'; import { count, distribution, gauge } from '../../../src/metrics/public-api'; @@ -119,6 +119,123 @@ describe('Metrics Public API', () => { expect(_INTERNAL_getMetricBuffer(client)).toBeUndefined(); }); + + it('captures a counter metric with sample_rate', () => { + const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, _experiments: { enableMetrics: true } }); + const client = new TestClient(options); + const scope = new Scope(); + scope.setClient(client); + + count('api.requests', 1, { + scope, + sample_rate: 0.5, + }); + + const metricBuffer = _INTERNAL_getMetricBuffer(client); + expect(metricBuffer).toHaveLength(1); + expect(metricBuffer?.[0]).toEqual( + expect.objectContaining({ + name: 'api.requests', + type: 'counter', + value: 1, + attributes: { + 'sentry.client_sample_rate': { + value: 0.5, + type: 'double', + }, + }, + }), + ); + }); + + it('captures a counter metric with sample_rate and existing attributes', () => { + const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, _experiments: { enableMetrics: true } }); + const client = new TestClient(options); + const scope = new Scope(); + scope.setClient(client); + + count('api.requests', 1, { + scope, + sample_rate: 0.25, + attributes: { + endpoint: '/api/users', + method: 'GET', + }, + }); + + const metricBuffer = _INTERNAL_getMetricBuffer(client); + expect(metricBuffer).toHaveLength(1); + expect(metricBuffer?.[0]).toEqual( + expect.objectContaining({ + name: 'api.requests', + type: 'counter', + value: 1, + attributes: { + endpoint: { + value: '/api/users', + type: 'string', + }, + method: { + value: 'GET', + type: 'string', + }, + 'sentry.client_sample_rate': { + value: 0.25, + type: 'double', + }, + }, + }), + ); + }); + + it('drops metrics with sample_rate above 1', () => { + const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, _experiments: { enableMetrics: true } }); + const client = new TestClient(options); + const scope = new Scope(); + scope.setClient(client); + + count('api.requests', 1, { + scope, + sample_rate: 1.5, + }); + + const metricBuffer = _INTERNAL_getMetricBuffer(client); + expect(metricBuffer).toBeUndefined(); + }); + + it('drops metrics with sample_rate at or below 0', () => { + const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, _experiments: { enableMetrics: true } }); + const client = new TestClient(options); + const scope = new Scope(); + scope.setClient(client); + + count('api.requests', 1, { + scope, + sample_rate: 0, + }); + + const metricBuffer = _INTERNAL_getMetricBuffer(client); + expect(metricBuffer).toBeUndefined(); + }); + + it('records dropped event for invalid sample_rate values', () => { + const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, _experiments: { enableMetrics: true } }); + const client = new TestClient(options); + const scope = new Scope(); + scope.setClient(client); + + const recordDroppedEventSpy = vi.spyOn(client, 'recordDroppedEvent'); + + count('api.requests', 1, { scope, sample_rate: 0 }); + count('api.requests', 1, { scope, sample_rate: -0.5 }); + count('api.requests', 1, { scope, sample_rate: 1.5 }); + + expect(recordDroppedEventSpy).toHaveBeenCalledTimes(3); + expect(recordDroppedEventSpy).toHaveBeenCalledWith('invalid_sample_rate', 'metric'); + + const metricBuffer = _INTERNAL_getMetricBuffer(client); + expect(metricBuffer).toBeUndefined(); + }); }); describe('gauge', () => { @@ -209,6 +326,34 @@ describe('Metrics Public API', () => { expect(_INTERNAL_getMetricBuffer(client)).toBeUndefined(); }); + + it('captures a gauge metric with sample_rate', () => { + const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, _experiments: { enableMetrics: true } }); + const client = new TestClient(options); + const scope = new Scope(); + scope.setClient(client); + + gauge('memory.usage', 1024, { + scope, + sample_rate: 0.75, + }); + + const metricBuffer = _INTERNAL_getMetricBuffer(client); + expect(metricBuffer).toHaveLength(1); + expect(metricBuffer?.[0]).toEqual( + expect.objectContaining({ + name: 'memory.usage', + type: 'gauge', + value: 1024, + attributes: { + 'sentry.client_sample_rate': { + value: 0.75, + type: 'double', + }, + }, + }), + ); + }); }); describe('distribution', () => { @@ -299,6 +444,34 @@ describe('Metrics Public API', () => { expect(_INTERNAL_getMetricBuffer(client)).toBeUndefined(); }); + + it('captures a distribution metric with sample_rate', () => { + const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, _experiments: { enableMetrics: true } }); + const client = new TestClient(options); + const scope = new Scope(); + scope.setClient(client); + + distribution('task.duration', 500, { + scope, + sample_rate: 0.1, + }); + + const metricBuffer = _INTERNAL_getMetricBuffer(client); + expect(metricBuffer).toHaveLength(1); + expect(metricBuffer?.[0]).toEqual( + expect.objectContaining({ + name: 'task.duration', + type: 'distribution', + value: 500, + attributes: { + 'sentry.client_sample_rate': { + value: 0.1, + type: 'double', + }, + }, + }), + ); + }); }); describe('mixed metric types', () => { From 1d11dca1a70cf3b04c24aecf76a109b85579edbc Mon Sep 17 00:00:00 2001 From: Kev Date: Wed, 5 Nov 2025 17:15:32 -0500 Subject: [PATCH 2/6] ? --- packages/core/src/metrics/internal.ts | 1 - packages/core/src/utils/lru.ts | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/core/src/metrics/internal.ts b/packages/core/src/metrics/internal.ts index 812d0a8800f0..e36d2ccba847 100644 --- a/packages/core/src/metrics/internal.ts +++ b/packages/core/src/metrics/internal.ts @@ -87,7 +87,6 @@ function setMetricAttribute( function validateAndProcessSampleRate(metric: Metric, client: Client): boolean { if (metric.sample_rate !== undefined) { if (metric.sample_rate <= 0 || metric.sample_rate > 1.0) { - // Invalid sample rate - drop the metric entirely and record the lost event client.recordDroppedEvent('invalid_sample_rate', 'metric'); return false; } diff --git a/packages/core/src/utils/lru.ts b/packages/core/src/utils/lru.ts index 5afad5a4b7e0..3158dff7d413 100644 --- a/packages/core/src/utils/lru.ts +++ b/packages/core/src/utils/lru.ts @@ -28,7 +28,7 @@ export class LRUMap { if (this._cache.size >= this._maxSize) { // keys() returns an iterator in insertion order so keys().next() gives us the oldest key // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const nextKey = this._cache.keys().next().value; + const nextKey = this._cache.keys().next().value!; this._cache.delete(nextKey); } this._cache.set(key, value); From d219b743c46977c68ccfce89db56f246b2e9e711 Mon Sep 17 00:00:00 2001 From: Kev Date: Wed, 5 Nov 2025 17:17:04 -0500 Subject: [PATCH 3/6] Only check sample rate after enableMetrics --- packages/core/src/metrics/internal.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/core/src/metrics/internal.ts b/packages/core/src/metrics/internal.ts index e36d2ccba847..d0def345e693 100644 --- a/packages/core/src/metrics/internal.ts +++ b/packages/core/src/metrics/internal.ts @@ -162,16 +162,16 @@ export function _INTERNAL_captureMetric(beforeMetric: Metric, options?: Internal return; } - if (!validateAndProcessSampleRate(beforeMetric, client)) { - return; - } - const { release, environment, _experiments } = client.getOptions(); if (!_experiments?.enableMetrics) { DEBUG_BUILD && debug.warn('metrics option not enabled, metric will not be captured.'); return; } + if (!validateAndProcessSampleRate(beforeMetric, client)) { + return; + } + const [, traceContext] = _getTraceInfoFromScope(client, currentScope); const processedMetricAttributes = { From f86899a5b1e8a2f09a4366953874c2abd06050ae Mon Sep 17 00:00:00 2001 From: Kev Date: Wed, 5 Nov 2025 17:25:14 -0500 Subject: [PATCH 4/6] prettier --- packages/core/src/metrics/internal.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/core/src/metrics/internal.ts b/packages/core/src/metrics/internal.ts index d0def345e693..37fceef72040 100644 --- a/packages/core/src/metrics/internal.ts +++ b/packages/core/src/metrics/internal.ts @@ -178,7 +178,6 @@ export function _INTERNAL_captureMetric(beforeMetric: Metric, options?: Internal ...beforeMetric.attributes, }; - addSampleRateAttribute(beforeMetric, processedMetricAttributes); const { From a08460d1a94ac155a06bef1e17a1565d35398eb1 Mon Sep 17 00:00:00 2001 From: Kev Date: Wed, 5 Nov 2025 18:30:40 -0500 Subject: [PATCH 5/6] Perform sampling based on trace --- packages/core/src/metrics/internal.ts | 105 ++++++++++++------ .../core/test/lib/metrics/public-api.test.ts | 37 +++++- 2 files changed, 104 insertions(+), 38 deletions(-) diff --git a/packages/core/src/metrics/internal.ts b/packages/core/src/metrics/internal.ts index 37fceef72040..eba9719d7d99 100644 --- a/packages/core/src/metrics/internal.ts +++ b/packages/core/src/metrics/internal.ts @@ -94,6 +94,21 @@ function validateAndProcessSampleRate(metric: Metric, client: Client): boolean { return true; } +/** + * Checks if a metric should be sampled based on the sample rate and scope's sampleRand (random based on trace). + * + * @param metric - The metric containing the sample_rate. + * @param scope - The scope containing the sampleRand. + * @returns true if the metric should be sampled, false if it should be dropped. + */ +function shouldSampleMetric(metric: Metric, scope: Scope): boolean { + if (metric.sample_rate === undefined) { + return true; + } + const sampleRand = scope.getPropagationContext().sampleRand; + return sampleRand < metric.sample_rate; +} + /** * Adds the sample_rate attribute to the metric attributes if needed. * @@ -106,6 +121,53 @@ function addSampleRateAttribute(metric: Metric, attributes: Record { + const processedMetricAttributes = { + ...beforeMetric.attributes, + }; + + addSampleRateAttribute(beforeMetric, processedMetricAttributes); + + const { + user: { id, email, username }, + } = getMergedScopeData(currentScope); + setMetricAttribute(processedMetricAttributes, 'user.id', id, false); + setMetricAttribute(processedMetricAttributes, 'user.email', email, false); + setMetricAttribute(processedMetricAttributes, 'user.name', username, false); + + const { release, environment } = client.getOptions(); + setMetricAttribute(processedMetricAttributes, 'sentry.release', release); + setMetricAttribute(processedMetricAttributes, 'sentry.environment', environment); + + const { name, version } = client.getSdkMetadata()?.sdk ?? {}; + setMetricAttribute(processedMetricAttributes, 'sentry.sdk.name', name); + setMetricAttribute(processedMetricAttributes, 'sentry.sdk.version', version); + + const replay = client.getIntegrationByName< + Integration & { + getReplayId: (onlyIfSampled?: boolean) => string; + getRecordingMode: () => 'session' | 'buffer' | undefined; + } + >('Replay'); + + const replayId = replay?.getReplayId(true); + setMetricAttribute(processedMetricAttributes, 'sentry.replay_id', replayId); + + if (replayId && replay?.getRecordingMode() === 'buffer') { + setMetricAttribute(processedMetricAttributes, 'sentry._internal.replay_is_buffering', true); + } + + return processedMetricAttributes; +} + /** * Captures a serialized metric event and adds it to the metric buffer for the given client. * @@ -162,7 +224,7 @@ export function _INTERNAL_captureMetric(beforeMetric: Metric, options?: Internal return; } - const { release, environment, _experiments } = client.getOptions(); + const { _experiments } = client.getOptions(); if (!_experiments?.enableMetrics) { DEBUG_BUILD && debug.warn('metrics option not enabled, metric will not be captured.'); return; @@ -172,44 +234,13 @@ export function _INTERNAL_captureMetric(beforeMetric: Metric, options?: Internal return; } - const [, traceContext] = _getTraceInfoFromScope(client, currentScope); - - const processedMetricAttributes = { - ...beforeMetric.attributes, - }; - - addSampleRateAttribute(beforeMetric, processedMetricAttributes); - - const { - user: { id, email, username }, - } = getMergedScopeData(currentScope); - setMetricAttribute(processedMetricAttributes, 'user.id', id, false); - setMetricAttribute(processedMetricAttributes, 'user.email', email, false); - setMetricAttribute(processedMetricAttributes, 'user.name', username, false); - - setMetricAttribute(processedMetricAttributes, 'sentry.release', release); - setMetricAttribute(processedMetricAttributes, 'sentry.environment', environment); - - const { name, version } = client.getSdkMetadata()?.sdk ?? {}; - setMetricAttribute(processedMetricAttributes, 'sentry.sdk.name', name); - setMetricAttribute(processedMetricAttributes, 'sentry.sdk.version', version); - - const replay = client.getIntegrationByName< - Integration & { - getReplayId: (onlyIfSampled?: boolean) => string; - getRecordingMode: () => 'session' | 'buffer' | undefined; - } - >('Replay'); - - const replayId = replay?.getReplayId(true); - - setMetricAttribute(processedMetricAttributes, 'sentry.replay_id', replayId); - - if (replayId && replay?.getRecordingMode() === 'buffer') { - // We send this so we can identify cases where the replayId is attached but the replay itself might not have been sent to Sentry - setMetricAttribute(processedMetricAttributes, 'sentry._internal.replay_is_buffering', true); + if (!shouldSampleMetric(beforeMetric, currentScope)) { + return; } + const [, traceContext] = _getTraceInfoFromScope(client, currentScope); + const processedMetricAttributes = processMetricAttributes(beforeMetric, currentScope, client); + const metric: Metric = { ...beforeMetric, attributes: processedMetricAttributes, diff --git a/packages/core/test/lib/metrics/public-api.test.ts b/packages/core/test/lib/metrics/public-api.test.ts index f0255085a8ef..116ee994e3ca 100644 --- a/packages/core/test/lib/metrics/public-api.test.ts +++ b/packages/core/test/lib/metrics/public-api.test.ts @@ -120,11 +120,15 @@ describe('Metrics Public API', () => { expect(_INTERNAL_getMetricBuffer(client)).toBeUndefined(); }); - it('captures a counter metric with sample_rate', () => { + it('captures a counter metric with sample_rate when sampled', () => { const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, _experiments: { enableMetrics: true } }); const client = new TestClient(options); const scope = new Scope(); scope.setClient(client); + scope.setPropagationContext({ + traceId: '86f39e84263a4de99c326acab3bfe3bd', + sampleRand: 0.25, + }); count('api.requests', 1, { scope, @@ -148,11 +152,34 @@ describe('Metrics Public API', () => { ); }); + it('drops counter metric with sample_rate when not sampled', () => { + const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, _experiments: { enableMetrics: true } }); + const client = new TestClient(options); + const scope = new Scope(); + scope.setClient(client); + scope.setPropagationContext({ + traceId: '86f39e84263a4de99c326acab3bfe3bd', + sampleRand: 0.75, + }); + + count('api.requests', 1, { + scope, + sample_rate: 0.5, + }); + + const metricBuffer = _INTERNAL_getMetricBuffer(client); + expect(metricBuffer).toBeUndefined(); + }); + it('captures a counter metric with sample_rate and existing attributes', () => { const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, _experiments: { enableMetrics: true } }); const client = new TestClient(options); const scope = new Scope(); scope.setClient(client); + scope.setPropagationContext({ + traceId: '86f39e84263a4de99c326acab3bfe3bd', + sampleRand: 0.1, + }); count('api.requests', 1, { scope, @@ -332,6 +359,10 @@ describe('Metrics Public API', () => { const client = new TestClient(options); const scope = new Scope(); scope.setClient(client); + scope.setPropagationContext({ + traceId: '86f39e84263a4de99c326acab3bfe3bd', + sampleRand: 0.5, + }); gauge('memory.usage', 1024, { scope, @@ -450,6 +481,10 @@ describe('Metrics Public API', () => { const client = new TestClient(options); const scope = new Scope(); scope.setClient(client); + scope.setPropagationContext({ + traceId: '86f39e84263a4de99c326acab3bfe3bd', + sampleRand: 0.05, + }); distribution('task.duration', 500, { scope, From 77ce587d9648ca50db49301e631304f949828201 Mon Sep 17 00:00:00 2001 From: Kev Date: Wed, 5 Nov 2025 18:41:44 -0500 Subject: [PATCH 6/6] Only add sample rate if sampling was performed --- packages/core/src/metrics/internal.ts | 33 +++++++++++++++++---------- 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/packages/core/src/metrics/internal.ts b/packages/core/src/metrics/internal.ts index eba9719d7d99..feb939ca682f 100644 --- a/packages/core/src/metrics/internal.ts +++ b/packages/core/src/metrics/internal.ts @@ -99,24 +99,31 @@ function validateAndProcessSampleRate(metric: Metric, client: Client): boolean { * * @param metric - The metric containing the sample_rate. * @param scope - The scope containing the sampleRand. - * @returns true if the metric should be sampled, false if it should be dropped. + * @returns An object with sampled (boolean) and samplingPerformed (boolean) flags. */ -function shouldSampleMetric(metric: Metric, scope: Scope): boolean { +function shouldSampleMetric(metric: Metric, scope: Scope): { sampled: boolean; samplingPerformed: boolean } { if (metric.sample_rate === undefined) { - return true; + return { sampled: true, samplingPerformed: false }; } - const sampleRand = scope.getPropagationContext().sampleRand; - return sampleRand < metric.sample_rate; + + const propagationContext = scope.getPropagationContext(); + if (!propagationContext || propagationContext.sampleRand === undefined) { + return { sampled: true, samplingPerformed: false }; + } + + const sampleRand = propagationContext.sampleRand; + return { sampled: sampleRand < metric.sample_rate, samplingPerformed: true }; } /** - * Adds the sample_rate attribute to the metric attributes if needed. + * Adds the sample_rate attribute to the metric attributes if sampling was actually performed. * * @param metric - The metric containing the sample_rate. * @param attributes - The attributes object to modify. + * @param samplingPerformed - Whether sampling was actually performed. */ -function addSampleRateAttribute(metric: Metric, attributes: Record): void { - if (metric.sample_rate !== undefined && metric.sample_rate !== 1.0) { +function addSampleRateAttribute(metric: Metric, attributes: Record, samplingPerformed: boolean): void { + if (metric.sample_rate !== undefined && metric.sample_rate !== 1.0 && samplingPerformed) { setMetricAttribute(attributes, 'sentry.client_sample_rate', metric.sample_rate); } } @@ -127,14 +134,15 @@ function addSampleRateAttribute(metric: Metric, attributes: Record { +function processMetricAttributes(beforeMetric: Metric, currentScope: Scope, client: Client, samplingPerformed: boolean): Record { const processedMetricAttributes = { ...beforeMetric.attributes, }; - addSampleRateAttribute(beforeMetric, processedMetricAttributes); + addSampleRateAttribute(beforeMetric, processedMetricAttributes, samplingPerformed); const { user: { id, email, username }, @@ -234,12 +242,13 @@ export function _INTERNAL_captureMetric(beforeMetric: Metric, options?: Internal return; } - if (!shouldSampleMetric(beforeMetric, currentScope)) { + const { sampled, samplingPerformed } = shouldSampleMetric(beforeMetric, currentScope); + if (!sampled) { return; } const [, traceContext] = _getTraceInfoFromScope(client, currentScope); - const processedMetricAttributes = processMetricAttributes(beforeMetric, currentScope, client); + const processedMetricAttributes = processMetricAttributes(beforeMetric, currentScope, client, samplingPerformed); const metric: Metric = { ...beforeMetric,