From 97bafa8d2ce5da794fa1d6a4e8a2c33c956bb24b Mon Sep 17 00:00:00 2001 From: Harshit Singh Date: Fri, 30 Jan 2026 02:05:07 +0530 Subject: [PATCH 1/5] 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 2/5] 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 3/5] 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 4/5] 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 5/5] 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(); + }); +});