Skip to content
Closed
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
97bafa8
feat(metrics): initialise MULTIPLEXED_METRIC_ROUTING_KEY for routing
harshit078 Jan 29, 2026
ab12a06
Merge branch 'develop' into routing-to-different-dsn
harshit078 Jan 29, 2026
b77918f
Merge branch 'develop' into routing-to-different-dsn
harshit078 Jan 30, 2026
9b635b1
fix(metrics): resolve linting errors
harshit078 Jan 30, 2026
a7632a9
Merge branch 'develop' into routing-to-different-dsn
harshit078 Jan 30, 2026
37d54c2
Merge branch 'develop' into routing-to-different-dsn
harshit078 Jan 31, 2026
00f559f
fix(metrics): updated build files and added striping logic
harshit078 Jan 31, 2026
c2faff6
fix(metrics): linting errors
harshit078 Jan 31, 2026
8a82bfd
fix(metrics): add tests for multiplex
harshit078 Jan 31, 2026
d989887
fix(metrics): fix failing core eslint tests
harshit078 Feb 2, 2026
d9854cb
Merge branch 'develop' into routing-to-different-dsn
harshit078 Feb 2, 2026
f845c7b
fix(metrics): fix failing tests
harshit078 Feb 2, 2026
1c836c5
fix(metrics): updated limit for failing size test
harshit078 Feb 2, 2026
350f662
fix(metrics): address comments left by cursor and sentry
harshit078 Feb 3, 2026
d38b5f5
Merge branch 'develop' into routing-to-different-dsn
harshit078 Feb 3, 2026
cb71961
fix(metrics): address comments
harshit078 Feb 3, 2026
222973e
fix(metrics): address comments by cursor for race conditions
harshit078 Feb 3, 2026
6baf564
Merge branch 'develop' into routing-to-different-dsn
harshit078 Feb 3, 2026
33c0d25
Merge branch 'develop' into routing-to-different-dsn
harshit078 Feb 4, 2026
23800bf
Merge branch 'develop' into routing-to-different-dsn
harshit078 Feb 4, 2026
b861ed2
fix(metrics): address comments
harshit078 Feb 5, 2026
e95310b
Merge branch 'develop' into routing-to-different-dsn
harshit078 Feb 5, 2026
35c776a
Merge branch 'develop' into routing-to-different-dsn
harshit078 Feb 6, 2026
58fccc9
fix(metrics): address comments and fix tests
harshit078 Feb 6, 2026
cf7811c
fix(metrics): linting test
harshit078 Feb 6, 2026
a41729a
Merge branch 'develop' into routing-to-different-dsn
harshit078 Feb 10, 2026
5f1fab4
Merge branch 'develop' into routing-to-different-dsn
harshit078 Feb 12, 2026
fdccbd9
Merge branch 'develop' into routing-to-different-dsn
harshit078 Feb 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .size-limit.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand Down Expand Up @@ -178,7 +178,7 @@ module.exports = [
name: 'CDN Bundle',
path: createCDNPath('bundle.min.js'),
gzip: true,
limit: '28 KB',
limit: '28.1 KB',
Comment thread
cursor[bot] marked this conversation as resolved.
Outdated
},
{
name: 'CDN Bundle (incl. Tracing)',
Expand Down
1 change: 1 addition & 0 deletions packages/browser/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ export {
setHttpStatus,
makeMultiplexedTransport,
MULTIPLEXED_TRANSPORT_EXTRA_KEY,
Comment thread
harshit078 marked this conversation as resolved.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is still a breaking change

MULTIPLEXED_METRIC_ROUTING_KEY,
moduleMetadataIntegration,
supabaseIntegration,
instrumentSupabaseClient,
Expand Down
7 changes: 6 additions & 1 deletion packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,12 @@ 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,
metricFromEnvelope,
Comment thread
harshit078 marked this conversation as resolved.
Outdated
Comment thread
harshit078 marked this conversation as resolved.
Outdated
} from './transports/multiplexed';
export { getIntegrationsToSetup, addIntegration, defineIntegration, installedIntegrations } from './integration';
export {
_INTERNAL_skipAiProviderWrapping,
Expand Down
24 changes: 22 additions & 2 deletions packages/core/src/metrics/internal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ 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, 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';
Expand Down Expand Up @@ -73,6 +74,25 @@ export interface InternalCaptureMetricOptions {
* A function to capture the serialized metric.
*/
captureSerializedMetric?: (client: Client, metric: SerializedMetric) => void;

/**
* The routing information for the metric.
*/
routing?: Array<MetricRoutingInfo>;
Comment thread
harshit078 marked this conversation as resolved.
Outdated
}

/**
* 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<string, unknown> | undefined): Record<string, unknown> | undefined {
if (!attributes) return attributes;

// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { [MULTIPLEXED_METRIC_ROUTING_KEY]: _routing, ...rest } = attributes;
return rest;
}

/**
Expand Down Expand Up @@ -145,7 +165,7 @@ function _buildSerializedMetric(
value: metric.value,
attributes: {
...serializeAttributes(scopeAttributes),
...serializeAttributes(metric.attributes, 'skip-undefined'),
...serializeAttributes(_stripRoutingAttributes(metric.attributes), 'skip-undefined'),
Comment thread
harshit078 marked this conversation as resolved.
Outdated
Comment thread
cursor[bot] marked this conversation as resolved.
Outdated
},
};
}
Expand Down
17 changes: 12 additions & 5 deletions packages/core/src/metrics/public-api.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { Scope } from '../scope';
import type { Metric, MetricType } from '../types-hoist/metric';
import { MULTIPLEXED_METRIC_ROUTING_KEY } from '../transports/multiplexed';
import type { Metric, MetricRoutingInfo, MetricType } from '../types-hoist/metric';
import { _INTERNAL_captureMetric } from './internal';

/**
Expand All @@ -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<MetricRoutingInfo>;
}

/**
Expand All @@ -31,10 +38,10 @@ export interface MetricOptions {
* @param options - Options for capturing the metric.
*/
function captureMetric(type: MetricType, name: string, value: number, options?: MetricOptions): void {
_INTERNAL_captureMetric(
{ type, name, value, unit: options?.unit, attributes: options?.attributes },
{ scope: options?.scope },
);
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 });
Comment thread
sentry[bot] marked this conversation as resolved.
}

/**
Expand Down
50 changes: 48 additions & 2 deletions packages/core/src/transports/multiplexed.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
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';
Expand All @@ -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 };
Expand All @@ -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.
*
Expand All @@ -47,7 +51,29 @@ 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 {
Comment thread
harshit078 marked this conversation as resolved.
Outdated
let metric: SerializedMetric | undefined;

forEachEnvelopeItem(env, (item, type) => {
if (type === 'trace_metric') {
const container = Array.isArray(item) ? (item[1] as SerializedMetricContainer) : undefined;
const containerItems = container?.items;
if (containerItems) {
metric = containerItems[0];
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't this mean we always just pull the first metric?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes it would mean we pull the first metric. What I'm thinking now after your comment is that I can add a logic which will check if all metrics and route to same or multiple destinations. Whats your opinion ?

}
}
return !!metric;
});

return metric;
}

/**
* Creates a transport that overrides the release on all events and metrics.
*/
function makeOverrideReleaseTransport<TO extends BaseTransportOptions>(
createTransport: (options: TO) => Transport,
Expand All @@ -64,6 +90,15 @@ function makeOverrideReleaseTransport<TO extends BaseTransportOptions>(
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 };
}
Comment thread
cursor[bot] marked this conversation as resolved.
Outdated

return transport.send(envelope);
},
};
Expand Down Expand Up @@ -109,6 +144,13 @@ export function makeMultiplexedTransport<TO extends BaseTransportOptions>(
) {
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 [];
});

Expand Down Expand Up @@ -142,7 +184,11 @@ export function makeMultiplexedTransport<TO extends BaseTransportOptions>(
return eventFromEnvelope(envelope, eventTypes);
}

const transports = actualMatcher({ envelope, getEvent })
function getMetric(): SerializedMetric | undefined {
return metricFromEnvelope(envelope);
}

const transports = actualMatcher({ envelope, getEvent, getMetric })
Comment thread
cursor[bot] marked this conversation as resolved.
Comment thread
cursor[bot] marked this conversation as resolved.
.map(result => {
Comment thread
sentry[bot] marked this conversation as resolved.
if (typeof result === 'string') {
return getTransport(result, undefined);
Expand Down
13 changes: 13 additions & 0 deletions packages/core/src/types-hoist/metric.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,10 +72,23 @@ 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;
}

export type SerializedMetricContainer = {
items: Array<SerializedMetric>;
};

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;
}
Comment thread
cursor[bot] marked this conversation as resolved.
82 changes: 82 additions & 0 deletions packages/core/test/lib/metrics/internal.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
});
});
Loading
Loading