From 97bafa8d2ce5da794fa1d6a4e8a2c33c956bb24b Mon Sep 17 00:00:00 2001 From: Harshit Singh Date: Fri, 30 Jan 2026 02:05:07 +0530 Subject: [PATCH 01/12] feat(metrics): initialise MULTIPLEXED_METRIC_ROUTING_KEY for routing --- packages/browser/src/index.ts | 1 + packages/core/src/index.ts | 2 +- packages/core/src/metrics/public-api.ts | 14 ++++++++++++-- packages/core/src/transports/multiplexed.ts | 13 ++++++++++++- packages/core/src/types-hoist/metric.ts | 13 +++++++++++++ 5 files changed, 39 insertions(+), 4 deletions(-) diff --git a/packages/browser/src/index.ts b/packages/browser/src/index.ts index 6e7c54198edc..e1941278e0f1 100644 --- a/packages/browser/src/index.ts +++ b/packages/browser/src/index.ts @@ -58,6 +58,7 @@ export { setHttpStatus, makeMultiplexedTransport, MULTIPLEXED_TRANSPORT_EXTRA_KEY, + MULTIPLEXED_METRIC_ROUTING_KEY, moduleMetadataIntegration, supabaseIntegration, instrumentSupabaseClient, diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 25c018af2d8a..ec5119302652 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -54,7 +54,7 @@ export { ServerRuntimeClient } from './server-runtime-client'; export { initAndBind, setCurrentClient } from './sdk'; export { createTransport } from './transports/base'; export { makeOfflineTransport } from './transports/offline'; -export { makeMultiplexedTransport, MULTIPLEXED_TRANSPORT_EXTRA_KEY } from './transports/multiplexed'; +export { makeMultiplexedTransport, MULTIPLEXED_TRANSPORT_EXTRA_KEY, MULTIPLEXED_METRIC_ROUTING_KEY } from './transports/multiplexed'; export { getIntegrationsToSetup, addIntegration, defineIntegration, installedIntegrations } from './integration'; export { _INTERNAL_skipAiProviderWrapping, diff --git a/packages/core/src/metrics/public-api.ts b/packages/core/src/metrics/public-api.ts index 7dcfe74dfdb0..eaae5d47d6e4 100644 --- a/packages/core/src/metrics/public-api.ts +++ b/packages/core/src/metrics/public-api.ts @@ -1,6 +1,7 @@ import type { Scope } from '../scope'; -import type { Metric, MetricType } from '../types-hoist/metric'; +import type { Metric, MetricRoutingInfo, MetricType } from '../types-hoist/metric'; import { _INTERNAL_captureMetric } from './internal'; +import { MULTIPLEXED_METRIC_ROUTING_KEY } from '../transports/multiplexed'; /** * Options for capturing a metric. @@ -20,6 +21,12 @@ export interface MetricOptions { * The scope to capture the metric with. */ scope?: Scope; + + /** + * The routing information for multiplexed transport. + * Each metric can be sent to multiple DSNs. + */ + routing?: Array; } /** @@ -31,8 +38,11 @@ export interface MetricOptions { * @param options - Options for capturing the metric. */ function captureMetric(type: MetricType, name: string, value: number, options?: MetricOptions): void { + const attributes = options?.routing + ? { ...options.attributes, [MULTIPLEXED_METRIC_ROUTING_KEY]: options.routing } + : options?.attributes; _INTERNAL_captureMetric( - { type, name, value, unit: options?.unit, attributes: options?.attributes }, + { type, name, value, unit: options?.unit, attributes }, { scope: options?.scope }, ); } diff --git a/packages/core/src/transports/multiplexed.ts b/packages/core/src/transports/multiplexed.ts index 41426b4a5d5a..336db4811f50 100644 --- a/packages/core/src/transports/multiplexed.ts +++ b/packages/core/src/transports/multiplexed.ts @@ -4,6 +4,7 @@ import type { Event } from '../types-hoist/event'; import type { BaseTransportOptions, Transport, TransportMakeRequestResponse } from '../types-hoist/transport'; import { dsnFromString } from '../utils/dsn'; import { createEnvelope, forEachEnvelopeItem } from '../utils/envelope'; +import type { SerializedMetric } from '../types-hoist/metric'; interface MatchParam { /** The envelope to be sent */ @@ -16,6 +17,7 @@ interface MatchParam { * @param types Defaults to ['event'] */ getEvent(types?: EnvelopeItemType[]): Event | undefined; + getMetric(): SerializedMetric | undefined; } type RouteTo = { dsn: string; release: string }; @@ -27,6 +29,8 @@ type Matcher = (param: MatchParam) => (string | RouteTo)[]; */ export const MULTIPLEXED_TRANSPORT_EXTRA_KEY = 'MULTIPLEXED_TRANSPORT_EXTRA_KEY'; +export const MULTIPLEXED_METRIC_ROUTING_KEY = 'sentry.routing'; + /** * Gets an event from an envelope. * @@ -109,6 +113,13 @@ export function makeMultiplexedTransport( ) { return event.extra[MULTIPLEXED_TRANSPORT_EXTRA_KEY]; } + const metric = args.getMetric(); + if ( + metric?.attributes?.[MULTIPLEXED_METRIC_ROUTING_KEY] && + Array.isArray(metric.attributes[MULTIPLEXED_METRIC_ROUTING_KEY]) + ) { + return metric.attributes[MULTIPLEXED_METRIC_ROUTING_KEY] as RouteTo[]; + } return []; }); @@ -142,7 +153,7 @@ export function makeMultiplexedTransport( return eventFromEnvelope(envelope, eventTypes); } - const transports = actualMatcher({ envelope, getEvent }) + const transports = actualMatcher({ envelope, getEvent, getMetric: () => undefined }) .map(result => { if (typeof result === 'string') { return getTransport(result, undefined); diff --git a/packages/core/src/types-hoist/metric.ts b/packages/core/src/types-hoist/metric.ts index 976fc9fe863f..58a3d26ad0fb 100644 --- a/packages/core/src/types-hoist/metric.ts +++ b/packages/core/src/types-hoist/metric.ts @@ -72,6 +72,7 @@ export interface SerializedMetric { /** * Arbitrary structured data that stores information about the metric. + * This can contain routing information via the `MULTIPLEXED_METRIC_ROUTING_KEY` key. */ attributes?: Attributes; } @@ -79,3 +80,15 @@ export interface SerializedMetric { export type SerializedMetricContainer = { items: Array; }; + +export interface MetricRoutingInfo { + /** + * The DSN of the Sentry project to send the metric to. + */ + dsn: string; + + /** + * The release of the Sentry project to send the metric to. + */ + release?: string; +} From 9b635b15aaf65940af23d0a13ebf5dc739a72f7c Mon Sep 17 00:00:00 2001 From: Harshit Singh Date: Fri, 30 Jan 2026 20:45:26 +0530 Subject: [PATCH 02/12] fix(metrics): resolve linting errors --- packages/core/src/index.ts | 6 +++++- packages/core/src/metrics/public-api.ts | 9 +++------ 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 18d942ffa348..1a5ccae8e060 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -55,7 +55,11 @@ export { ServerRuntimeClient } from './server-runtime-client'; export { initAndBind, setCurrentClient } from './sdk'; export { createTransport } from './transports/base'; export { makeOfflineTransport } from './transports/offline'; -export { makeMultiplexedTransport, MULTIPLEXED_TRANSPORT_EXTRA_KEY, MULTIPLEXED_METRIC_ROUTING_KEY } from './transports/multiplexed'; +export { + makeMultiplexedTransport, + MULTIPLEXED_TRANSPORT_EXTRA_KEY, + MULTIPLEXED_METRIC_ROUTING_KEY, +} from './transports/multiplexed'; export { getIntegrationsToSetup, addIntegration, defineIntegration, installedIntegrations } from './integration'; export { _INTERNAL_skipAiProviderWrapping, diff --git a/packages/core/src/metrics/public-api.ts b/packages/core/src/metrics/public-api.ts index eaae5d47d6e4..637ecb684427 100644 --- a/packages/core/src/metrics/public-api.ts +++ b/packages/core/src/metrics/public-api.ts @@ -39,12 +39,9 @@ export interface MetricOptions { */ function captureMetric(type: MetricType, name: string, value: number, options?: MetricOptions): void { const attributes = options?.routing - ? { ...options.attributes, [MULTIPLEXED_METRIC_ROUTING_KEY]: options.routing } - : options?.attributes; - _INTERNAL_captureMetric( - { type, name, value, unit: options?.unit, attributes }, - { scope: options?.scope }, - ); + ? { ...options.attributes, [MULTIPLEXED_METRIC_ROUTING_KEY]: options.routing } + : options?.attributes; + _INTERNAL_captureMetric({ type, name, value, unit: options?.unit, attributes }, { scope: options?.scope }); } /** From 00f559fa03732cb8e6bcb6ee59fa6036bb63921b Mon Sep 17 00:00:00 2001 From: Harshit Singh Date: Sat, 31 Jan 2026 17:01:14 +0530 Subject: [PATCH 03/12] fix(metrics): updated build files and added striping logic --- packages/core/src/index.ts | 1 + packages/core/src/metrics/internal.ts | 25 +++++++++++-- packages/core/src/transports/multiplexed.ts | 40 +++++++++++++++++++-- 3 files changed, 61 insertions(+), 5 deletions(-) diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 1a5ccae8e060..3ef91fbe25b9 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -59,6 +59,7 @@ export { makeMultiplexedTransport, MULTIPLEXED_TRANSPORT_EXTRA_KEY, MULTIPLEXED_METRIC_ROUTING_KEY, + metricFromEnvelope, } from './transports/multiplexed'; export { getIntegrationsToSetup, addIntegration, defineIntegration, installedIntegrations } from './integration'; export { diff --git a/packages/core/src/metrics/internal.ts b/packages/core/src/metrics/internal.ts index bdd13d884967..219b44975bea 100644 --- a/packages/core/src/metrics/internal.ts +++ b/packages/core/src/metrics/internal.ts @@ -5,7 +5,7 @@ import { getClient, getCurrentScope, getIsolationScope } from '../currentScopes' import { DEBUG_BUILD } from '../debug-build'; import type { Scope } from '../scope'; import type { Integration } from '../types-hoist/integration'; -import type { Metric, SerializedMetric } from '../types-hoist/metric'; +import type { Metric, MetricRoutingInfo, SerializedMetric } from '../types-hoist/metric'; import type { User } from '../types-hoist/user'; import { debug } from '../utils/debug-logger'; import { getCombinedScopeData } from '../utils/scopeData'; @@ -13,6 +13,7 @@ import { _getSpanForScope } from '../utils/spanOnScope'; import { timestampInSeconds } from '../utils/time'; import { _getTraceInfoFromScope } from '../utils/trace-info'; import { createMetricEnvelope } from './envelope'; +import { MULTIPLEXED_METRIC_ROUTING_KEY } from '../transports/multiplexed'; const MAX_METRIC_BUFFER_SIZE = 1000; @@ -73,6 +74,26 @@ export interface InternalCaptureMetricOptions { * A function to capture the serialized metric. */ captureSerializedMetric?: (client: Client, metric: SerializedMetric) => void; + + /** + * The routing information for the metric. + */ + routing?: Array; +} + +/** + * A helper function which strips the routing information from the attributes. + * It is used to prevent the routing information from being sent to Sentry. + * @param attributes - The attributes to strip the routing information from. + * @returns The attributes without the routing information. + */ +function _stripRoutingAttributes( + attributes: Record | undefined, +): Record | undefined { + if (!attributes) return attributes; + + const { [MULTIPLEXED_METRIC_ROUTING_KEY]: _routing, ...rest } = attributes; + return rest; } /** @@ -145,7 +166,7 @@ function _buildSerializedMetric( value: metric.value, attributes: { ...serializeAttributes(scopeAttributes), - ...serializeAttributes(metric.attributes, 'skip-undefined'), + ...serializeAttributes(_stripRoutingAttributes(metric.attributes), 'skip-undefined'), }, }; } diff --git a/packages/core/src/transports/multiplexed.ts b/packages/core/src/transports/multiplexed.ts index 336db4811f50..325dc8bb2f95 100644 --- a/packages/core/src/transports/multiplexed.ts +++ b/packages/core/src/transports/multiplexed.ts @@ -4,7 +4,7 @@ import type { Event } from '../types-hoist/event'; import type { BaseTransportOptions, Transport, TransportMakeRequestResponse } from '../types-hoist/transport'; import { dsnFromString } from '../utils/dsn'; import { createEnvelope, forEachEnvelopeItem } from '../utils/envelope'; -import type { SerializedMetric } from '../types-hoist/metric'; +import type { SerializedMetric, SerializedMetricContainer } from '../types-hoist/metric'; interface MatchParam { /** The envelope to be sent */ @@ -51,7 +51,28 @@ export function eventFromEnvelope(env: Envelope, types: EnvelopeItemType[]): Eve } /** - * Creates a transport that overrides the release on all events. + * Gets a metric from an envelope. + * + * This is only exported for use in tests and advanced use cases. + */ +export function metricFromEnvelope(env: Envelope): SerializedMetric | undefined { + let metric: SerializedMetric | undefined; + + forEachEnvelopeItem(env, (item, type) => { + if (type === 'trace_metric') { + const container = Array.isArray(item) ? (item[1] as SerializedMetricContainer) : undefined; + if (container && container.items && Array.isArray(container.items) && container.items.length > 0) { + metric = container.items[0]; + } + } + return !!metric; + }); + + return metric; +} + +/** + * Creates a transport that overrides the release on all events and metrics. */ function makeOverrideReleaseTransport( createTransport: (options: TO) => Transport, @@ -68,6 +89,15 @@ function makeOverrideReleaseTransport( if (event) { event.release = release; } + const metric = metricFromEnvelope(envelope); + if (metric) { + // This is mainly for tracking/debugging purposes + if (!metric.attributes) { + metric.attributes = {}; + } + metric.attributes['sentry.release'] = { type: 'string', value: release }; + } + return transport.send(envelope); }, }; @@ -153,7 +183,11 @@ export function makeMultiplexedTransport( return eventFromEnvelope(envelope, eventTypes); } - const transports = actualMatcher({ envelope, getEvent, getMetric: () => undefined }) + function getMetric(): SerializedMetric | undefined { + return metricFromEnvelope(envelope); + } + + const transports = actualMatcher({ envelope, getEvent, getMetric }) .map(result => { if (typeof result === 'string') { return getTransport(result, undefined); From c2faff67328a298d80a9c2c70126d8a459b49331 Mon Sep 17 00:00:00 2001 From: Harshit Singh Date: Sat, 31 Jan 2026 17:08:53 +0530 Subject: [PATCH 04/12] fix(metrics): linting errors --- packages/core/src/metrics/internal.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/core/src/metrics/internal.ts b/packages/core/src/metrics/internal.ts index 219b44975bea..a3217c2966e4 100644 --- a/packages/core/src/metrics/internal.ts +++ b/packages/core/src/metrics/internal.ts @@ -87,9 +87,7 @@ export interface InternalCaptureMetricOptions { * @param attributes - The attributes to strip the routing information from. * @returns The attributes without the routing information. */ -function _stripRoutingAttributes( - attributes: Record | undefined, -): Record | undefined { +function _stripRoutingAttributes(attributes: Record | undefined): Record | undefined { if (!attributes) return attributes; const { [MULTIPLEXED_METRIC_ROUTING_KEY]: _routing, ...rest } = attributes; From 8a82bfd5e80f23985810150cadbc5d71d433a91e Mon Sep 17 00:00:00 2001 From: Harshit Singh Date: Sun, 1 Feb 2026 00:21:23 +0530 Subject: [PATCH 05/12] fix(metrics): add tests for multiplex --- .../core/test/lib/metrics/internal.test.ts | 82 +++++++++ .../test/lib/transports/multiplexed.test.ts | 163 +++++++++++++++++- 2 files changed, 244 insertions(+), 1 deletion(-) diff --git a/packages/core/test/lib/metrics/internal.test.ts b/packages/core/test/lib/metrics/internal.test.ts index 434f4b6c8289..71a27ab7f7d7 100644 --- a/packages/core/test/lib/metrics/internal.test.ts +++ b/packages/core/test/lib/metrics/internal.test.ts @@ -1050,3 +1050,85 @@ describe('_INTERNAL_captureMetric', () => { }); }); }); + +describe('routing attribute stripping', () => { + it('strips routing attributes before serialization', () => { + const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN }); + const client = new TestClient(options); + const scope = new Scope(); + scope.setClient(client); + + _INTERNAL_captureMetric( + { + type: 'counter', + name: 'test.metric', + value: 1, + attributes: { + 'sentry.routing': [{ dsn: 'https://test.dsn', release: 'v1.0.0' }], + normalAttribute: 'value', + }, + }, + { scope }, + ); + + const buffer = _INTERNAL_getMetricBuffer(client); + expect(buffer).toHaveLength(1); + expect(buffer?.[0]?.attributes).toEqual({ + normalAttribute: { + type: 'string', + value: 'value', + }, + }); + expect(buffer?.[0]?.attributes).not.toHaveProperty('sentry.routing'); + }); + + it('handles missing attributes when stripping routing', () => { + const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN }); + const client = new TestClient(options); + const scope = new Scope(); + scope.setClient(client); + + _INTERNAL_captureMetric( + { + type: 'counter', + name: 'test.metric', + value: 1, + }, + { scope }, + ); + + const buffer = _INTERNAL_getMetricBuffer(client); + expect(buffer).toHaveLength(1); + expect(buffer?.[0]?.attributes).toEqual({}); + }); + + it('preserves other attributes when routing is stripped', () => { + const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, release: 'v1.0.0', environment: 'production' }); + const client = new TestClient(options); + const scope = new Scope(); + scope.setClient(client); + + _INTERNAL_captureMetric( + { + type: 'counter', + name: 'test.metric', + value: 1, + attributes: { + 'sentry.routing': [{ dsn: 'https://test.dsn' }], + feature: 'cart', + userId: '12345', + }, + }, + { scope }, + ); + + const buffer = _INTERNAL_getMetricBuffer(client); + const attrs = buffer?.[0]?.attributes; + + expect(attrs).toHaveProperty('feature'); + expect(attrs).toHaveProperty('userId'); + expect(attrs).toHaveProperty('sentry.release'); + expect(attrs).toHaveProperty('sentry.environment'); + expect(attrs).not.toHaveProperty('sentry.routing'); + }); +}); diff --git a/packages/core/test/lib/transports/multiplexed.test.ts b/packages/core/test/lib/transports/multiplexed.test.ts index 105d54b17eea..6d51bc6317a2 100644 --- a/packages/core/test/lib/transports/multiplexed.test.ts +++ b/packages/core/test/lib/transports/multiplexed.test.ts @@ -8,11 +8,18 @@ import { makeMultiplexedTransport, parseEnvelope, } from '../../../src'; -import { eventFromEnvelope, MULTIPLEXED_TRANSPORT_EXTRA_KEY } from '../../../src/transports/multiplexed'; +import { + eventFromEnvelope, + MULTIPLEXED_TRANSPORT_EXTRA_KEY, + metricFromEnvelope, + MULTIPLEXED_METRIC_ROUTING_KEY, +} from '../../../src/transports/multiplexed'; import type { ClientReport } from '../../../src/types-hoist/clientreport'; import type { Envelope, EventEnvelope, EventItem } from '../../../src/types-hoist/envelope'; import type { TransactionEvent } from '../../../src/types-hoist/event'; import type { BaseTransportOptions, Transport } from '../../../src/types-hoist/transport'; +import type { SerializedMetric } from '../../../src/types-hoist/metric'; +import { createMetricEnvelope } from '../../../src/metrics/envelope'; const DSN1 = 'https://1234@5678.ingest.sentry.io/4321'; const DSN1_URL = getEnvelopeEndpointWithUrlEncodedAuth(dsnFromString(DSN1)!); @@ -321,3 +328,157 @@ describe('makeMultiplexedTransport() with default matcher', () => { await transport.send(envelope); }); }); + +describe('makeMultiplexedTransport with metrics', () => { + const METRIC: SerializedMetric = { + timestamp: 1234567890, + trace_id: 'trace123', + name: 'test.metric', + type: 'counter', + value: 1, + attributes: {}, + }; + + it('routes metrics to DSN specified in attributes', async () => { + expect.assertions(2); + + const makeTransport = makeMultiplexedTransport( + createTestTransport((url, _, env) => { + expect(url).toBe(DSN2_URL); + expect(env[0].dsn).toBe(DSN2); + }), + ); + + const metricWithRouting: SerializedMetric = { + ...METRIC, + attributes: { + [MULTIPLEXED_METRIC_ROUTING_KEY]: [{ dsn: DSN2, release: 'cart@1.0.0' }] as any, + }, + }; + + const envelope = createMetricEnvelope([metricWithRouting], undefined, undefined, undefined); + const transport = makeTransport({ url: DSN1_URL, ...transportOptions }); + await transport.send(envelope); + }); + + it('custom matcher can route metrics based on attributes', async () => { + expect.assertions(2); + + const makeTransport = makeMultiplexedTransport( + createTestTransport(url => { + expect(url).toBe(DSN2_URL); + }), + ({ getMetric }) => { + const metric = getMetric(); + const featureAttr = metric?.attributes?.feature as any; + expect(featureAttr?.value || featureAttr).toBe('cart'); + if ((featureAttr?.value || featureAttr) === 'cart') { + return [DSN2]; + } + return []; + }, + ); + + const metricWithFeature: SerializedMetric = { + ...METRIC, + attributes: { feature: { type: 'string', value: 'cart' } }, + }; + + const envelope = createMetricEnvelope([metricWithFeature], undefined, undefined, undefined); + const transport = makeTransport({ url: DSN1_URL, ...transportOptions }); + await transport.send(envelope); + }); + + it('falls back to default DSN when no metric routing info', async () => { + expect.assertions(1); + + const makeTransport = makeMultiplexedTransport( + createTestTransport(url => { + expect(url).toBe(DSN1_URL); + }), + ); + + const envelope = createMetricEnvelope([METRIC], undefined, undefined, undefined); + const transport = makeTransport({ url: DSN1_URL, ...transportOptions }); + await transport.send(envelope); + }); + + it('routes metrics to multiple DSNs', async () => { + expect.assertions(2); + + const makeTransport = makeMultiplexedTransport( + createTestTransport( + url => { + expect(url).toBe(DSN1_URL); + }, + url => { + expect(url).toBe(DSN2_URL); + }, + ), + ); + + const metricWithRouting: SerializedMetric = { + ...METRIC, + attributes: { + [MULTIPLEXED_METRIC_ROUTING_KEY]: [DSN1, DSN2] as any, + }, + }; + + const envelope = createMetricEnvelope([metricWithRouting], undefined, undefined, undefined); + const transport = makeTransport({ url: DSN1_URL, ...transportOptions }); + await transport.send(envelope); + }); + + it('uses MULTIPLEXED_METRIC_ROUTING_KEY for routing', async () => { + expect.assertions(3); + + const makeTransport = makeMultiplexedTransport( + createTestTransport((url, _, envelope) => { + expect(url).toBe(DSN2_URL); + const metric = metricFromEnvelope(envelope); + expect(metric?.attributes?.['sentry.release']).toBe('cart@1.0.0'); + expect(envelope[0].dsn).toBe(DSN2); + }), + ); + + const metricWithRouting: SerializedMetric = { + ...METRIC, + attributes: { + [MULTIPLEXED_METRIC_ROUTING_KEY]: [{ dsn: DSN2, release: 'cart@1.0.0' }] as any, + }, + }; + + const envelope = createMetricEnvelope([metricWithRouting], undefined, undefined, undefined); + const transport = makeTransport({ url: DSN1_URL, ...transportOptions }); + await transport.send(envelope); + }); +}); + +describe('metricFromEnvelope', () => { + it('extracts metric from trace_metric envelope', () => { + const metric: SerializedMetric = { + timestamp: 1234567890, + trace_id: 'trace123', + name: 'test.metric', + type: 'counter' as const, + value: 1, + attributes: { foo: { type: 'string', value: 'bar' } }, + }; + + const envelope = createMetricEnvelope([metric], undefined, undefined, undefined); + const extracted = metricFromEnvelope(envelope); + + expect(extracted).toEqual(metric); + }); + + it('returns undefined for non-metric envelopes', () => { + const extracted = metricFromEnvelope(ERROR_ENVELOPE); + expect(extracted).toBeUndefined(); + }); + + it('returns undefined for empty metric container', () => { + const envelope = createMetricEnvelope([], undefined, undefined, undefined); + const extracted = metricFromEnvelope(envelope); + expect(extracted).toBeUndefined(); + }); +}); From d989887488cd569e6e23f34224f51492ea201917 Mon Sep 17 00:00:00 2001 From: Harshit Singh Date: Mon, 2 Feb 2026 16:01:16 +0530 Subject: [PATCH 06/12] fix(metrics): fix failing core eslint tests --- packages/core/src/metrics/internal.ts | 3 ++- packages/core/src/metrics/public-api.ts | 2 +- packages/core/src/transports/multiplexed.ts | 7 ++++--- packages/core/test/lib/transports/multiplexed.test.ts | 8 ++++---- 4 files changed, 11 insertions(+), 9 deletions(-) diff --git a/packages/core/src/metrics/internal.ts b/packages/core/src/metrics/internal.ts index a3217c2966e4..ca11b401a864 100644 --- a/packages/core/src/metrics/internal.ts +++ b/packages/core/src/metrics/internal.ts @@ -4,6 +4,7 @@ import type { Client } from '../client'; import { getClient, getCurrentScope, getIsolationScope } from '../currentScopes'; import { DEBUG_BUILD } from '../debug-build'; import type { Scope } from '../scope'; +import { MULTIPLEXED_METRIC_ROUTING_KEY } from '../transports/multiplexed'; import type { Integration } from '../types-hoist/integration'; import type { Metric, MetricRoutingInfo, SerializedMetric } from '../types-hoist/metric'; import type { User } from '../types-hoist/user'; @@ -13,7 +14,6 @@ import { _getSpanForScope } from '../utils/spanOnScope'; import { timestampInSeconds } from '../utils/time'; import { _getTraceInfoFromScope } from '../utils/trace-info'; import { createMetricEnvelope } from './envelope'; -import { MULTIPLEXED_METRIC_ROUTING_KEY } from '../transports/multiplexed'; const MAX_METRIC_BUFFER_SIZE = 1000; @@ -90,6 +90,7 @@ export interface InternalCaptureMetricOptions { function _stripRoutingAttributes(attributes: Record | undefined): Record | undefined { if (!attributes) return attributes; + // eslint-disable-next-line @typescript-eslint/no-unused-vars const { [MULTIPLEXED_METRIC_ROUTING_KEY]: _routing, ...rest } = attributes; return rest; } diff --git a/packages/core/src/metrics/public-api.ts b/packages/core/src/metrics/public-api.ts index 637ecb684427..95722e637e98 100644 --- a/packages/core/src/metrics/public-api.ts +++ b/packages/core/src/metrics/public-api.ts @@ -1,7 +1,7 @@ import type { Scope } from '../scope'; +import { MULTIPLEXED_METRIC_ROUTING_KEY } from '../transports/multiplexed'; import type { Metric, MetricRoutingInfo, MetricType } from '../types-hoist/metric'; import { _INTERNAL_captureMetric } from './internal'; -import { MULTIPLEXED_METRIC_ROUTING_KEY } from '../transports/multiplexed'; /** * Options for capturing a metric. diff --git a/packages/core/src/transports/multiplexed.ts b/packages/core/src/transports/multiplexed.ts index 325dc8bb2f95..10ce0c0458b6 100644 --- a/packages/core/src/transports/multiplexed.ts +++ b/packages/core/src/transports/multiplexed.ts @@ -1,10 +1,10 @@ import { getEnvelopeEndpointWithUrlEncodedAuth } from '../api'; import type { Envelope, EnvelopeItemType, EventItem } from '../types-hoist/envelope'; import type { Event } from '../types-hoist/event'; +import type { SerializedMetric, SerializedMetricContainer } from '../types-hoist/metric'; import type { BaseTransportOptions, Transport, TransportMakeRequestResponse } from '../types-hoist/transport'; import { dsnFromString } from '../utils/dsn'; import { createEnvelope, forEachEnvelopeItem } from '../utils/envelope'; -import type { SerializedMetric, SerializedMetricContainer } from '../types-hoist/metric'; interface MatchParam { /** The envelope to be sent */ @@ -61,8 +61,9 @@ export function metricFromEnvelope(env: Envelope): SerializedMetric | undefined forEachEnvelopeItem(env, (item, type) => { if (type === 'trace_metric') { const container = Array.isArray(item) ? (item[1] as SerializedMetricContainer) : undefined; - if (container && container.items && Array.isArray(container.items) && container.items.length > 0) { - metric = container.items[0]; + const containerItems = container?.items; + if (containerItems) { + metric = containerItems[0]; } } return !!metric; diff --git a/packages/core/test/lib/transports/multiplexed.test.ts b/packages/core/test/lib/transports/multiplexed.test.ts index 6d51bc6317a2..5447736c1b76 100644 --- a/packages/core/test/lib/transports/multiplexed.test.ts +++ b/packages/core/test/lib/transports/multiplexed.test.ts @@ -8,18 +8,18 @@ import { makeMultiplexedTransport, parseEnvelope, } from '../../../src'; +import { createMetricEnvelope } from '../../../src/metrics/envelope'; import { eventFromEnvelope, - MULTIPLEXED_TRANSPORT_EXTRA_KEY, metricFromEnvelope, MULTIPLEXED_METRIC_ROUTING_KEY, + MULTIPLEXED_TRANSPORT_EXTRA_KEY, } from '../../../src/transports/multiplexed'; import type { ClientReport } from '../../../src/types-hoist/clientreport'; import type { Envelope, EventEnvelope, EventItem } from '../../../src/types-hoist/envelope'; import type { TransactionEvent } from '../../../src/types-hoist/event'; -import type { BaseTransportOptions, Transport } from '../../../src/types-hoist/transport'; import type { SerializedMetric } from '../../../src/types-hoist/metric'; -import { createMetricEnvelope } from '../../../src/metrics/envelope'; +import type { BaseTransportOptions, Transport } from '../../../src/types-hoist/transport'; const DSN1 = 'https://1234@5678.ingest.sentry.io/4321'; const DSN1_URL = getEnvelopeEndpointWithUrlEncodedAuth(dsnFromString(DSN1)!); @@ -436,7 +436,7 @@ describe('makeMultiplexedTransport with metrics', () => { createTestTransport((url, _, envelope) => { expect(url).toBe(DSN2_URL); const metric = metricFromEnvelope(envelope); - expect(metric?.attributes?.['sentry.release']).toBe('cart@1.0.0'); + expect(metric?.attributes?.[MULTIPLEXED_METRIC_ROUTING_KEY]).toEqual({ type: 'string', value: 'cart@1.0.0' }); expect(envelope[0].dsn).toBe(DSN2); }), ); From f845c7b0c99a5eb0fee26b8517d7854dfc8c14e8 Mon Sep 17 00:00:00 2001 From: Harshit Singh Date: Mon, 2 Feb 2026 16:24:52 +0530 Subject: [PATCH 07/12] fix(metrics): fix failing tests --- .size-limit.js | 2 +- packages/core/test/lib/transports/multiplexed.test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.size-limit.js b/.size-limit.js index 7feee4373586..846c2fbc15b1 100644 --- a/.size-limit.js +++ b/.size-limit.js @@ -178,7 +178,7 @@ module.exports = [ name: 'CDN Bundle', path: createCDNPath('bundle.min.js'), gzip: true, - limit: '28 KB', + limit: '28.1 KB', }, { name: 'CDN Bundle (incl. Tracing)', diff --git a/packages/core/test/lib/transports/multiplexed.test.ts b/packages/core/test/lib/transports/multiplexed.test.ts index 5447736c1b76..3fce3c3ed8cc 100644 --- a/packages/core/test/lib/transports/multiplexed.test.ts +++ b/packages/core/test/lib/transports/multiplexed.test.ts @@ -436,7 +436,7 @@ describe('makeMultiplexedTransport with metrics', () => { createTestTransport((url, _, envelope) => { expect(url).toBe(DSN2_URL); const metric = metricFromEnvelope(envelope); - expect(metric?.attributes?.[MULTIPLEXED_METRIC_ROUTING_KEY]).toEqual({ type: 'string', value: 'cart@1.0.0' }); + expect(metric?.attributes?.['sentry.release']).toEqual({ type: 'string', value: 'cart@1.0.0' }); expect(envelope[0].dsn).toBe(DSN2); }), ); From 1c836c5200f8ca2dabd64aa74cb157de6cf61094 Mon Sep 17 00:00:00 2001 From: Harshit Singh Date: Mon, 2 Feb 2026 17:14:54 +0530 Subject: [PATCH 08/12] fix(metrics): updated limit for failing size test --- .size-limit.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.size-limit.js b/.size-limit.js index 846c2fbc15b1..90962d4b9df8 100644 --- a/.size-limit.js +++ b/.size-limit.js @@ -148,7 +148,7 @@ module.exports = [ import: createImport('init', 'ErrorBoundary', 'reactRouterV6BrowserTracingIntegration'), ignore: ['react/jsx-runtime'], gzip: true, - limit: '44.5 KB', + limit: '44.6 KB', }, // Vue SDK (ESM) { From 350f662e3b9b399169a9fb81f79566724ca3fc4d Mon Sep 17 00:00:00 2001 From: Harshit Singh Date: Tue, 3 Feb 2026 16:39:02 +0530 Subject: [PATCH 09/12] fix(metrics): address comments left by cursor and sentry --- packages/core/src/metrics/internal.ts | 17 +----- packages/core/src/transports/multiplexed.ts | 54 ++++++++++++++----- .../core/test/lib/metrics/internal.test.ts | 15 +++--- .../test/lib/transports/multiplexed.test.ts | 53 ++++++++++++++++++ 4 files changed, 104 insertions(+), 35 deletions(-) diff --git a/packages/core/src/metrics/internal.ts b/packages/core/src/metrics/internal.ts index ca11b401a864..75d3f5f7ef8a 100644 --- a/packages/core/src/metrics/internal.ts +++ b/packages/core/src/metrics/internal.ts @@ -4,7 +4,6 @@ import type { Client } from '../client'; import { getClient, getCurrentScope, getIsolationScope } from '../currentScopes'; import { DEBUG_BUILD } from '../debug-build'; import type { Scope } from '../scope'; -import { MULTIPLEXED_METRIC_ROUTING_KEY } from '../transports/multiplexed'; import type { Integration } from '../types-hoist/integration'; import type { Metric, MetricRoutingInfo, SerializedMetric } from '../types-hoist/metric'; import type { User } from '../types-hoist/user'; @@ -81,20 +80,6 @@ export interface InternalCaptureMetricOptions { routing?: Array; } -/** - * A helper function which strips the routing information from the attributes. - * It is used to prevent the routing information from being sent to Sentry. - * @param attributes - The attributes to strip the routing information from. - * @returns The attributes without the routing information. - */ -function _stripRoutingAttributes(attributes: Record | undefined): Record | undefined { - if (!attributes) return attributes; - - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { [MULTIPLEXED_METRIC_ROUTING_KEY]: _routing, ...rest } = attributes; - return rest; -} - /** * Enriches metric with all contextual attributes (user, SDK metadata, replay, etc.) */ @@ -165,7 +150,7 @@ function _buildSerializedMetric( value: metric.value, attributes: { ...serializeAttributes(scopeAttributes), - ...serializeAttributes(_stripRoutingAttributes(metric.attributes), 'skip-undefined'), + ...serializeAttributes(metric.attributes, 'skip-undefined'), }, }; } diff --git a/packages/core/src/transports/multiplexed.ts b/packages/core/src/transports/multiplexed.ts index 10ce0c0458b6..ab1c63fb32f7 100644 --- a/packages/core/src/transports/multiplexed.ts +++ b/packages/core/src/transports/multiplexed.ts @@ -72,6 +72,26 @@ export function metricFromEnvelope(env: Envelope): SerializedMetric | undefined return metric; } +/** + * Applies the release to all metrics in an envelope. + */ +function applyReleaseToMetrics(envelope: Envelope, release: string): void { + forEachEnvelopeItem(envelope, (item, type) => { + if (type === 'trace_metric') { + const container = Array.isArray(item) ? (item[1] as SerializedMetricContainer) : undefined; + const containerItems = container?.items; + if (containerItems) { + for (const metric of containerItems) { + if (!metric.attributes) { + metric.attributes = {}; + } + metric.attributes['sentry.release'] = { type: 'string', value: release }; + } + } + } + }); +} + /** * Creates a transport that overrides the release on all events and metrics. */ @@ -90,14 +110,8 @@ function makeOverrideReleaseTransport( if (event) { event.release = release; } - const metric = metricFromEnvelope(envelope); - if (metric) { - // This is mainly for tracking/debugging purposes - if (!metric.attributes) { - metric.attributes = {}; - } - metric.attributes['sentry.release'] = { type: 'string', value: release }; - } + + applyReleaseToMetrics(envelope, release); return transport.send(envelope); }, @@ -145,11 +159,25 @@ export function makeMultiplexedTransport( return event.extra[MULTIPLEXED_TRANSPORT_EXTRA_KEY]; } const metric = args.getMetric(); - if ( - metric?.attributes?.[MULTIPLEXED_METRIC_ROUTING_KEY] && - Array.isArray(metric.attributes[MULTIPLEXED_METRIC_ROUTING_KEY]) - ) { - return metric.attributes[MULTIPLEXED_METRIC_ROUTING_KEY] as RouteTo[]; + if (metric?.attributes?.[MULTIPLEXED_METRIC_ROUTING_KEY]) { + const routingAttr = metric.attributes[MULTIPLEXED_METRIC_ROUTING_KEY]; + let routingValue: unknown; + if (typeof routingAttr === 'object' && routingAttr !== null && 'value' in routingAttr) { + routingValue = routingAttr.value; + if (typeof routingValue === 'string') { + try { + routingValue = JSON.parse(routingValue); + } catch { + return []; + } + } + } else { + routingValue = routingAttr; + } + + if (Array.isArray(routingValue)) { + return routingValue as RouteTo[]; + } } return []; }); diff --git a/packages/core/test/lib/metrics/internal.test.ts b/packages/core/test/lib/metrics/internal.test.ts index 71a27ab7f7d7..90b5d7dc4019 100644 --- a/packages/core/test/lib/metrics/internal.test.ts +++ b/packages/core/test/lib/metrics/internal.test.ts @@ -1051,8 +1051,8 @@ describe('_INTERNAL_captureMetric', () => { }); }); -describe('routing attribute stripping', () => { - it('strips routing attributes before serialization', () => { +describe('routing attribute preservation', () => { + it('preserves routing attributes during serialization', () => { const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN }); const client = new TestClient(options); const scope = new Scope(); @@ -1078,11 +1078,14 @@ describe('routing attribute stripping', () => { type: 'string', value: 'value', }, + 'sentry.routing': { + type: 'string', + value: JSON.stringify([{ dsn: 'https://test.dsn', release: 'v1.0.0' }]), + }, }); - expect(buffer?.[0]?.attributes).not.toHaveProperty('sentry.routing'); }); - it('handles missing attributes when stripping routing', () => { + it('handles missing attributes during serialization', () => { const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN }); const client = new TestClient(options); const scope = new Scope(); @@ -1102,7 +1105,7 @@ describe('routing attribute stripping', () => { expect(buffer?.[0]?.attributes).toEqual({}); }); - it('preserves other attributes when routing is stripped', () => { + it('preserves all attributes including routing', () => { const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, release: 'v1.0.0', environment: 'production' }); const client = new TestClient(options); const scope = new Scope(); @@ -1129,6 +1132,6 @@ describe('routing attribute stripping', () => { expect(attrs).toHaveProperty('userId'); expect(attrs).toHaveProperty('sentry.release'); expect(attrs).toHaveProperty('sentry.environment'); - expect(attrs).not.toHaveProperty('sentry.routing'); + expect(attrs).toHaveProperty('sentry.routing'); }); }); diff --git a/packages/core/test/lib/transports/multiplexed.test.ts b/packages/core/test/lib/transports/multiplexed.test.ts index 3fce3c3ed8cc..0b3666bb23ef 100644 --- a/packages/core/test/lib/transports/multiplexed.test.ts +++ b/packages/core/test/lib/transports/multiplexed.test.ts @@ -4,6 +4,7 @@ import { createEnvelope, createTransport, dsnFromString, + forEachEnvelopeItem, getEnvelopeEndpointWithUrlEncodedAuth, makeMultiplexedTransport, parseEnvelope, @@ -452,6 +453,58 @@ describe('makeMultiplexedTransport with metrics', () => { const transport = makeTransport({ url: DSN1_URL, ...transportOptions }); await transport.send(envelope); }); + + it('applies release to all metrics in batch', async () => { + expect.assertions(4); + + const makeTransport = makeMultiplexedTransport( + createTestTransport((url, _, envelope) => { + expect(url).toBe(DSN2_URL); + + const metrics: SerializedMetric[] = []; + forEachEnvelopeItem(envelope, (item, type) => { + if (type === 'trace_metric') { + const container = Array.isArray(item) ? (item[1] as any) : undefined; + if (container?.items) { + metrics.push(...container.items); + } + } + }); + + expect(metrics).toHaveLength(3); + expect(metrics[0]?.attributes?.['sentry.release']).toEqual({ type: 'string', value: 'batch@1.0.0' }); + expect(metrics[2]?.attributes?.['sentry.release']).toEqual({ type: 'string', value: 'batch@1.0.0' }); + }), + ); + + const metric1: SerializedMetric = { + ...METRIC, + name: 'metric1', + attributes: { + [MULTIPLEXED_METRIC_ROUTING_KEY]: [{ dsn: DSN2, release: 'batch@1.0.0' }] as any, + }, + }; + + const metric2: SerializedMetric = { + ...METRIC, + name: 'metric2', + attributes: { + [MULTIPLEXED_METRIC_ROUTING_KEY]: [{ dsn: DSN2, release: 'batch@1.0.0' }] as any, + }, + }; + + const metric3: SerializedMetric = { + ...METRIC, + name: 'metric3', + attributes: { + [MULTIPLEXED_METRIC_ROUTING_KEY]: [{ dsn: DSN2, release: 'batch@1.0.0' }] as any, + }, + }; + + const envelope = createMetricEnvelope([metric1, metric2, metric3], undefined, undefined, undefined); + const transport = makeTransport({ url: DSN1_URL, ...transportOptions }); + await transport.send(envelope); + }); }); describe('metricFromEnvelope', () => { From cb71961374f0ad05d5e8f54a18dde1c4044c07f0 Mon Sep 17 00:00:00 2001 From: Harshit Singh Date: Tue, 3 Feb 2026 17:13:21 +0530 Subject: [PATCH 10/12] fix(metrics): address comments --- packages/core/src/metrics/internal.ts | 7 +-- packages/core/src/transports/multiplexed.ts | 21 ++++++++ .../test/lib/transports/multiplexed.test.ts | 53 ++++++++++++++++++- 3 files changed, 74 insertions(+), 7 deletions(-) diff --git a/packages/core/src/metrics/internal.ts b/packages/core/src/metrics/internal.ts index 75d3f5f7ef8a..bdd13d884967 100644 --- a/packages/core/src/metrics/internal.ts +++ b/packages/core/src/metrics/internal.ts @@ -5,7 +5,7 @@ import { getClient, getCurrentScope, getIsolationScope } from '../currentScopes' import { DEBUG_BUILD } from '../debug-build'; import type { Scope } from '../scope'; import type { Integration } from '../types-hoist/integration'; -import type { Metric, MetricRoutingInfo, SerializedMetric } from '../types-hoist/metric'; +import type { Metric, SerializedMetric } from '../types-hoist/metric'; import type { User } from '../types-hoist/user'; import { debug } from '../utils/debug-logger'; import { getCombinedScopeData } from '../utils/scopeData'; @@ -73,11 +73,6 @@ export interface InternalCaptureMetricOptions { * A function to capture the serialized metric. */ captureSerializedMetric?: (client: Client, metric: SerializedMetric) => void; - - /** - * The routing information for the metric. - */ - routing?: Array; } /** diff --git a/packages/core/src/transports/multiplexed.ts b/packages/core/src/transports/multiplexed.ts index ab1c63fb32f7..d58970a2b6a3 100644 --- a/packages/core/src/transports/multiplexed.ts +++ b/packages/core/src/transports/multiplexed.ts @@ -92,6 +92,26 @@ function applyReleaseToMetrics(envelope: Envelope, release: string): void { }); } +/** + * It strips routing attributes from all metrics in an envelope. + * This prevents the routing information from being sent to Sentry. + */ +function stripRoutingAttributesFromMetrics(envelope: Envelope): void { + forEachEnvelopeItem(envelope, (item, type) => { + if (type === 'trace_metric') { + const container = Array.isArray(item) ? (item[1] as SerializedMetricContainer) : undefined; + const containerItems = container?.items; + if (containerItems) { + for (const metric of containerItems) { + if (metric.attributes && MULTIPLEXED_METRIC_ROUTING_KEY in metric.attributes) { + delete metric.attributes[MULTIPLEXED_METRIC_ROUTING_KEY]; + } + } + } + } + }); +} + /** * Creates a transport that overrides the release on all events and metrics. */ @@ -226,6 +246,7 @@ export function makeMultiplexedTransport( }) .filter((t): t is [string, Transport] => !!t); + stripRoutingAttributesFromMetrics(envelope); // If we have no transports to send to, use the fallback transport // Don't override the DSN in the header for the fallback transport. '' is falsy const transportsWithFallback: [string, Transport][] = transports.length ? transports : [['', fallbackTransport]]; diff --git a/packages/core/test/lib/transports/multiplexed.test.ts b/packages/core/test/lib/transports/multiplexed.test.ts index 0b3666bb23ef..de78bb342747 100644 --- a/packages/core/test/lib/transports/multiplexed.test.ts +++ b/packages/core/test/lib/transports/multiplexed.test.ts @@ -431,13 +431,14 @@ describe('makeMultiplexedTransport with metrics', () => { }); it('uses MULTIPLEXED_METRIC_ROUTING_KEY for routing', async () => { - expect.assertions(3); + expect.assertions(4); const makeTransport = makeMultiplexedTransport( createTestTransport((url, _, envelope) => { expect(url).toBe(DSN2_URL); const metric = metricFromEnvelope(envelope); expect(metric?.attributes?.['sentry.release']).toEqual({ type: 'string', value: 'cart@1.0.0' }); + expect(metric?.attributes?.[MULTIPLEXED_METRIC_ROUTING_KEY]).toBeUndefined(); expect(envelope[0].dsn).toBe(DSN2); }), ); @@ -505,6 +506,56 @@ describe('makeMultiplexedTransport with metrics', () => { const transport = makeTransport({ url: DSN1_URL, ...transportOptions }); await transport.send(envelope); }); + + it('strips routing attributes from all metrics before sending', async () => { + expect.assertions(5); + + const makeTransport = makeMultiplexedTransport( + createTestTransport((url, _, envelope) => { + expect(url).toBe(DSN2_URL); + + // Extract all metrics from the envelope + const metrics: SerializedMetric[] = []; + forEachEnvelopeItem(envelope, (item, type) => { + if (type === 'trace_metric') { + const container = Array.isArray(item) ? (item[1] as any) : undefined; + if (container?.items) { + metrics.push(...container.items); + } + } + }); + + // Verify all metrics have routing attributes stripped + expect(metrics).toHaveLength(2); + expect(metrics[0]?.attributes?.[MULTIPLEXED_METRIC_ROUTING_KEY]).toBeUndefined(); + expect(metrics[1]?.attributes?.[MULTIPLEXED_METRIC_ROUTING_KEY]).toBeUndefined(); + // But other attributes should be preserved + expect(metrics[0]?.attributes?.['custom']).toBeDefined(); + }), + ); + + const metric1: SerializedMetric = { + ...METRIC, + name: 'metric1', + attributes: { + [MULTIPLEXED_METRIC_ROUTING_KEY]: [{ dsn: DSN2, release: 'test@1.0.0' }] as any, + custom: { type: 'string', value: 'value1' }, + }, + }; + + const metric2: SerializedMetric = { + ...METRIC, + name: 'metric2', + attributes: { + [MULTIPLEXED_METRIC_ROUTING_KEY]: [{ dsn: DSN2, release: 'test@1.0.0' }] as any, + custom: { type: 'string', value: 'value2' }, + }, + }; + + const envelope = createMetricEnvelope([metric1, metric2], undefined, undefined, undefined); + const transport = makeTransport({ url: DSN1_URL, ...transportOptions }); + await transport.send(envelope); + }); }); describe('metricFromEnvelope', () => { From 222973e2d5ae5b14eaac5fc0cae256e7c470b873 Mon Sep 17 00:00:00 2001 From: Harshit Singh Date: Tue, 3 Feb 2026 18:56:24 +0530 Subject: [PATCH 11/12] fix(metrics): address comments by cursor for race conditions --- packages/core/src/transports/multiplexed.ts | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/packages/core/src/transports/multiplexed.ts b/packages/core/src/transports/multiplexed.ts index d58970a2b6a3..47bcdb83f523 100644 --- a/packages/core/src/transports/multiplexed.ts +++ b/packages/core/src/transports/multiplexed.ts @@ -79,14 +79,14 @@ function applyReleaseToMetrics(envelope: Envelope, release: string): void { forEachEnvelopeItem(envelope, (item, type) => { if (type === 'trace_metric') { const container = Array.isArray(item) ? (item[1] as SerializedMetricContainer) : undefined; - const containerItems = container?.items; - if (containerItems) { - for (const metric of containerItems) { - if (!metric.attributes) { - metric.attributes = {}; - } - metric.attributes['sentry.release'] = { type: 'string', value: release }; - } + if (container?.items) { + container.items = container.items.map(metric => ({ + ...metric, + attributes: { + ...metric.attributes, + 'sentry.release': { type: 'string', value: release }, + }, + })); } } }); @@ -104,7 +104,9 @@ function stripRoutingAttributesFromMetrics(envelope: Envelope): void { if (containerItems) { for (const metric of containerItems) { if (metric.attributes && MULTIPLEXED_METRIC_ROUTING_KEY in metric.attributes) { - delete metric.attributes[MULTIPLEXED_METRIC_ROUTING_KEY]; + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { [MULTIPLEXED_METRIC_ROUTING_KEY]: _routing, ...restAttributes } = metric.attributes; + metric.attributes = restAttributes; } } } @@ -247,6 +249,7 @@ export function makeMultiplexedTransport( .filter((t): t is [string, Transport] => !!t); stripRoutingAttributesFromMetrics(envelope); + // If we have no transports to send to, use the fallback transport // Don't override the DSN in the header for the fallback transport. '' is falsy const transportsWithFallback: [string, Transport][] = transports.length ? transports : [['', fallbackTransport]]; From b861ed2c3298ff839d22da9480dcc6c146b851bd Mon Sep 17 00:00:00 2001 From: Harshit Singh Date: Thu, 5 Feb 2026 17:12:41 +0530 Subject: [PATCH 12/12] fix(metrics): address comments --- packages/browser/src/index.ts | 2 - packages/core/src/index.ts | 7 +- packages/core/src/transports/multiplexed.ts | 76 +++++++++++++-------- 3 files changed, 48 insertions(+), 37 deletions(-) diff --git a/packages/browser/src/index.ts b/packages/browser/src/index.ts index e1941278e0f1..149068892f54 100644 --- a/packages/browser/src/index.ts +++ b/packages/browser/src/index.ts @@ -57,8 +57,6 @@ export { getSpanStatusFromHttpCode, setHttpStatus, makeMultiplexedTransport, - MULTIPLEXED_TRANSPORT_EXTRA_KEY, - MULTIPLEXED_METRIC_ROUTING_KEY, moduleMetadataIntegration, supabaseIntegration, instrumentSupabaseClient, diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 3ef91fbe25b9..6eb9812e2a00 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -55,12 +55,7 @@ export { ServerRuntimeClient } from './server-runtime-client'; export { initAndBind, setCurrentClient } from './sdk'; export { createTransport } from './transports/base'; export { makeOfflineTransport } from './transports/offline'; -export { - makeMultiplexedTransport, - MULTIPLEXED_TRANSPORT_EXTRA_KEY, - MULTIPLEXED_METRIC_ROUTING_KEY, - metricFromEnvelope, -} from './transports/multiplexed'; +export { makeMultiplexedTransport } from './transports/multiplexed'; export { getIntegrationsToSetup, addIntegration, defineIntegration, installedIntegrations } from './integration'; export { _INTERNAL_skipAiProviderWrapping, diff --git a/packages/core/src/transports/multiplexed.ts b/packages/core/src/transports/multiplexed.ts index 47bcdb83f523..943370105dd4 100644 --- a/packages/core/src/transports/multiplexed.ts +++ b/packages/core/src/transports/multiplexed.ts @@ -1,8 +1,10 @@ import { getEnvelopeEndpointWithUrlEncodedAuth } from '../api'; +import { DEBUG_BUILD } from '../debug-build'; import type { Envelope, EnvelopeItemType, EventItem } from '../types-hoist/envelope'; import type { Event } from '../types-hoist/event'; import type { SerializedMetric, SerializedMetricContainer } from '../types-hoist/metric'; import type { BaseTransportOptions, Transport, TransportMakeRequestResponse } from '../types-hoist/transport'; +import { debug } from '../utils/debug-logger'; import { dsnFromString } from '../utils/dsn'; import { createEnvelope, forEachEnvelopeItem } from '../utils/envelope'; @@ -50,15 +52,32 @@ export function eventFromEnvelope(env: Envelope, types: EnvelopeItemType[]): Eve return event; } +/** + * It iterates over metric containers in an envelope. + */ +function forEachMetricContainer( + envelope: Envelope, + callback: (container: SerializedMetricContainer, metrics: SerializedMetric[]) => void | boolean, +): void { + forEachEnvelopeItem(envelope, (item, type) => { + if (type === 'trace_metric') { + const container = Array.isArray(item) ? (item[1] as SerializedMetricContainer) : undefined; + if (container?.items) { + return callback(container, container.items); + } + } + }); +} + /** * Gets a metric from an envelope. * * This is only exported for use in tests and advanced use cases. */ -export function metricFromEnvelope(env: Envelope): SerializedMetric | undefined { +export function metricFromEnvelope(envelope: Envelope): SerializedMetric | undefined { let metric: SerializedMetric | undefined; - forEachEnvelopeItem(env, (item, type) => { + forEachEnvelopeItem(envelope, (item, type) => { if (type === 'trace_metric') { const container = Array.isArray(item) ? (item[1] as SerializedMetricContainer) : undefined; const containerItems = container?.items; @@ -76,19 +95,14 @@ export function metricFromEnvelope(env: Envelope): SerializedMetric | undefined * Applies the release to all metrics in an envelope. */ function applyReleaseToMetrics(envelope: Envelope, release: string): void { - forEachEnvelopeItem(envelope, (item, type) => { - if (type === 'trace_metric') { - const container = Array.isArray(item) ? (item[1] as SerializedMetricContainer) : undefined; - if (container?.items) { - container.items = container.items.map(metric => ({ - ...metric, - attributes: { - ...metric.attributes, - 'sentry.release': { type: 'string', value: release }, - }, - })); - } - } + forEachMetricContainer(envelope, (container, metrics) => { + container.items = metrics.map(metric => ({ + ...metric, + attributes: { + ...metric.attributes, + 'sentry.release': { type: 'string', value: release }, + }, + })); }); } @@ -97,18 +111,14 @@ function applyReleaseToMetrics(envelope: Envelope, release: string): void { * This prevents the routing information from being sent to Sentry. */ function stripRoutingAttributesFromMetrics(envelope: Envelope): void { - forEachEnvelopeItem(envelope, (item, type) => { - if (type === 'trace_metric') { - const container = Array.isArray(item) ? (item[1] as SerializedMetricContainer) : undefined; - const containerItems = container?.items; - if (containerItems) { - for (const metric of containerItems) { - if (metric.attributes && MULTIPLEXED_METRIC_ROUTING_KEY in metric.attributes) { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { [MULTIPLEXED_METRIC_ROUTING_KEY]: _routing, ...restAttributes } = metric.attributes; - metric.attributes = restAttributes; - } - } + let strippedCount = 0; + forEachMetricContainer(envelope, (_container, metrics) => { + for (const metric of metrics) { + if (metric.attributes && MULTIPLEXED_METRIC_ROUTING_KEY in metric.attributes) { + DEBUG_BUILD && strippedCount++; + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { [MULTIPLEXED_METRIC_ROUTING_KEY]: _routing, ...restAttributes } = metric.attributes; + metric.attributes = restAttributes; } } }); @@ -183,13 +193,17 @@ export function makeMultiplexedTransport( const metric = args.getMetric(); if (metric?.attributes?.[MULTIPLEXED_METRIC_ROUTING_KEY]) { const routingAttr = metric.attributes[MULTIPLEXED_METRIC_ROUTING_KEY]; + DEBUG_BUILD && debug.log('[Multiplexed Transport] Found metric routing attribute:', routingAttr); + let routingValue: unknown; if (typeof routingAttr === 'object' && routingAttr !== null && 'value' in routingAttr) { routingValue = routingAttr.value; if (typeof routingValue === 'string') { try { routingValue = JSON.parse(routingValue); - } catch { + DEBUG_BUILD && debug.log('[Multiplexed Transport] Parsed routing value:', routingValue); + } catch (e) { + DEBUG_BUILD && debug.warn('[Multiplexed Transport] Failed to parse routing JSON:', e); return []; } } @@ -198,7 +212,11 @@ export function makeMultiplexedTransport( } if (Array.isArray(routingValue)) { - return routingValue as RouteTo[]; + const validRoutes = routingValue.filter( + (route): route is RouteTo => route !== null && route !== undefined && typeof route === 'object', + ); + DEBUG_BUILD && debug.log('[Multiplexed Transport] Valid routes:', validRoutes); + return validRoutes; } } return [];