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 30ace1803b1a..3ef91fbe25b9 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -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, +} from './transports/multiplexed'; export { getIntegrationsToSetup, addIntegration, defineIntegration, installedIntegrations } from './integration'; export { _INTERNAL_skipAiProviderWrapping, diff --git a/packages/core/src/metrics/internal.ts b/packages/core/src/metrics/internal.ts index bdd13d884967..a3217c2966e4 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,24 @@ 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 +164,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/metrics/public-api.ts b/packages/core/src/metrics/public-api.ts index 7dcfe74dfdb0..637ecb684427 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,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 }); } /** diff --git a/packages/core/src/transports/multiplexed.ts b/packages/core/src/transports/multiplexed.ts index 41426b4a5d5a..325dc8bb2f95 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, SerializedMetricContainer } 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. * @@ -47,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, @@ -64,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); }, }; @@ -109,6 +143,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 +183,11 @@ export function makeMultiplexedTransport( return eventFromEnvelope(envelope, eventTypes); } - const transports = actualMatcher({ envelope, getEvent }) + function getMetric(): SerializedMetric | undefined { + return metricFromEnvelope(envelope); + } + + const transports = actualMatcher({ envelope, getEvent, getMetric }) .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; +}