diff --git a/dev-packages/node-integration-tests/suites/tracing/metric-summaries/scenario.js b/dev-packages/node-integration-tests/suites/tracing/metric-summaries/scenario.js index ef68afb06576..e2921db180af 100644 --- a/dev-packages/node-integration-tests/suites/tracing/metric-summaries/scenario.js +++ b/dev-packages/node-integration-tests/suites/tracing/metric-summaries/scenario.js @@ -6,9 +6,6 @@ Sentry.init({ release: '1.0', tracesSampleRate: 1.0, transport: loggingTransport, - _experiments: { - metricsAggregator: true, - }, }); // Stop the process from exiting before the transaction is sent diff --git a/packages/astro/src/index.types.ts b/packages/astro/src/index.types.ts index 026321e8ab3d..93ec819d4e8f 100644 --- a/packages/astro/src/index.types.ts +++ b/packages/astro/src/index.types.ts @@ -26,6 +26,8 @@ export declare const defaultStackParser: StackParser; export declare function close(timeout?: number | undefined): PromiseLike; export declare function flush(timeout?: number | undefined): PromiseLike; +export declare const metrics: typeof clientSdk.metrics & typeof serverSdk.metrics; + /** * @deprecated This function will be removed in the next major version of the Sentry SDK. */ diff --git a/packages/browser/src/exports.ts b/packages/browser/src/exports.ts index f0f717f084cc..df0d9fc5d6c7 100644 --- a/packages/browser/src/exports.ts +++ b/packages/browser/src/exports.ts @@ -68,12 +68,12 @@ export { FunctionToString, // eslint-disable-next-line deprecation/deprecation InboundFilters, - metrics, functionToStringIntegration, inboundFiltersIntegration, parameterize, } from '@sentry/core'; +export * from './metrics'; export { WINDOW } from './helpers'; export { BrowserClient } from './client'; export { makeFetchTransport, makeXHRTransport } from './transports'; diff --git a/packages/browser/src/metrics.ts b/packages/browser/src/metrics.ts new file mode 100644 index 000000000000..a6c9e2f4e1cb --- /dev/null +++ b/packages/browser/src/metrics.ts @@ -0,0 +1,51 @@ +import type { MetricData } from '@sentry/core'; +import { BrowserMetricsAggregator, metrics as metricsCore } from '@sentry/core'; + +/** + * Adds a value to a counter metric + * + * @experimental This API is experimental and might have breaking changes in the future. + */ +function increment(name: string, value: number = 1, data?: MetricData): void { + metricsCore.increment(BrowserMetricsAggregator, name, value, data); +} + +/** + * Adds a value to a distribution metric + * + * @experimental This API is experimental and might have breaking changes in the future. + */ +function distribution(name: string, value: number, data?: MetricData): void { + metricsCore.distribution(BrowserMetricsAggregator, name, value, data); +} + +/** + * Adds a value to a set metric. Value must be a string or integer. + * + * @experimental This API is experimental and might have breaking changes in the future. + */ +function set(name: string, value: number | string, data?: MetricData): void { + metricsCore.set(BrowserMetricsAggregator, name, value, data); +} + +/** + * Adds a value to a gauge metric + * + * @experimental This API is experimental and might have breaking changes in the future. + */ +function gauge(name: string, value: number, data?: MetricData): void { + metricsCore.gauge(BrowserMetricsAggregator, name, value, data); +} + +export const metrics = { + increment, + distribution, + set, + gauge, + /** @deprecated An integration is no longer required to use the metrics feature */ + // eslint-disable-next-line deprecation/deprecation + MetricsAggregator: metricsCore.MetricsAggregator, + /** @deprecated An integration is no longer required to use the metrics feature */ + // eslint-disable-next-line deprecation/deprecation + metricsAggregatorIntegration: metricsCore.metricsAggregatorIntegration, +}; diff --git a/packages/bun/src/index.ts b/packages/bun/src/index.ts index e8a4738b9bba..8b08f5f8e86b 100644 --- a/packages/bun/src/index.ts +++ b/packages/bun/src/index.ts @@ -78,7 +78,7 @@ export { startInactiveSpan, startSpanManual, continueTrace, - metrics, + metricsNode as metrics, functionToStringIntegration, inboundFiltersIntegration, linkedErrorsIntegration, diff --git a/packages/core/src/baseclient.ts b/packages/core/src/baseclient.ts index b54e1887fd2b..ff56cf7ca9e6 100644 --- a/packages/core/src/baseclient.ts +++ b/packages/core/src/baseclient.ts @@ -95,9 +95,9 @@ const ALREADY_SEEN_ERROR = "Not capturing exception because it's already been ca */ export abstract class BaseClient implements Client { /** - * A reference to a metrics aggregator + * TODO (v8): Remove * - * @experimental Note this is alpha API. It may experience breaking changes in the future. + * @deprecated The metricsAggregator is no longer referenced on the client. */ public metricsAggregator?: MetricsAggregator; @@ -281,9 +281,7 @@ export abstract class BaseClient implements Client { public flush(timeout?: number): PromiseLike { const transport = this._transport; if (transport) { - if (this.metricsAggregator) { - this.metricsAggregator.flush(); - } + this.emit('flush'); return this._isClientDoneProcessing(timeout).then(clientFinished => { return transport.flush(timeout).then(transportFlushed => clientFinished && transportFlushed); }); @@ -298,9 +296,7 @@ export abstract class BaseClient implements Client { public close(timeout?: number): PromiseLike { return this.flush(timeout).then(result => { this.getOptions().enabled = false; - if (this.metricsAggregator) { - this.metricsAggregator.close(); - } + this.emit('close'); return result; }); } @@ -496,6 +492,10 @@ export abstract class BaseClient implements Client { /** @inheritdoc */ public on(hook: 'startNavigationSpan', callback: (options: StartSpanOptions) => void): void; + public on(hook: 'flush', callback: () => void): void; + + public on(hook: 'close', callback: () => void): void; + /** @inheritdoc */ public on(hook: string, callback: unknown): void { if (!this._hooks[hook]) { @@ -542,6 +542,12 @@ export abstract class BaseClient implements Client { /** @inheritdoc */ public emit(hook: 'startNavigationSpan', options: StartSpanOptions): void; + /** @inheritdoc */ + public emit(hook: 'flush'): void; + + /** @inheritdoc */ + public emit(hook: 'close'): void; + /** @inheritdoc */ public emit(hook: string, ...rest: unknown[]): void { if (this._hooks[hook]) { diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 849e34f6c92b..60aa5bab5124 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -106,6 +106,9 @@ export { linkedErrorsIntegration } from './integrations/linkederrors'; export { moduleMetadataIntegration } from './integrations/metadata'; export { requestDataIntegration } from './integrations/requestdata'; export { metrics } from './metrics/exports'; +export type { MetricData } from './metrics/exports'; +export { metricsNode } from './metrics/exports-node'; +export { BrowserMetricsAggregator } from './metrics/browser-aggregator'; /** @deprecated Import the integration function directly, e.g. `inboundFiltersIntegration()` instead of `new Integrations.InboundFilter(). */ const Integrations = INTEGRATIONS; diff --git a/packages/core/src/metrics/exports-node.ts b/packages/core/src/metrics/exports-node.ts new file mode 100644 index 000000000000..53cc6ee6dccf --- /dev/null +++ b/packages/core/src/metrics/exports-node.ts @@ -0,0 +1,52 @@ +import { MetricsAggregator } from './aggregator'; +import type { MetricData } from './exports'; +import { metrics as metricsCore } from './exports'; + +/** + * Adds a value to a counter metric + * + * @experimental This API is experimental and might have breaking changes in the future. + */ +function increment(name: string, value: number = 1, data?: MetricData): void { + metricsCore.increment(MetricsAggregator, name, value, data); +} + +/** + * Adds a value to a distribution metric + * + * @experimental This API is experimental and might have breaking changes in the future. + */ +function distribution(name: string, value: number, data?: MetricData): void { + metricsCore.distribution(MetricsAggregator, name, value, data); +} + +/** + * Adds a value to a set metric. Value must be a string or integer. + * + * @experimental This API is experimental and might have breaking changes in the future. + */ +function set(name: string, value: number | string, data?: MetricData): void { + metricsCore.set(MetricsAggregator, name, value, data); +} + +/** + * Adds a value to a gauge metric + * + * @experimental This API is experimental and might have breaking changes in the future. + */ +function gauge(name: string, value: number, data?: MetricData): void { + metricsCore.gauge(MetricsAggregator, name, value, data); +} + +export const metricsNode = { + increment, + distribution, + set, + gauge, + /** @deprecated An integration is no longer required to use the metrics feature */ + // eslint-disable-next-line deprecation/deprecation + MetricsAggregator: metricsCore.MetricsAggregator, + /** @deprecated An integration is no longer required to use the metrics feature */ + // eslint-disable-next-line deprecation/deprecation + metricsAggregatorIntegration: metricsCore.metricsAggregatorIntegration, +}; diff --git a/packages/core/src/metrics/exports.ts b/packages/core/src/metrics/exports.ts index 20d63a2ca119..02af9dbf6d0d 100644 --- a/packages/core/src/metrics/exports.ts +++ b/packages/core/src/metrics/exports.ts @@ -1,4 +1,9 @@ -import type { ClientOptions, MeasurementUnit, Primitive } from '@sentry/types'; +import type { + ClientOptions, + MeasurementUnit, + MetricsAggregator as MetricsAggregatorInterface, + Primitive, +} from '@sentry/types'; import { logger } from '@sentry/utils'; import type { BaseClient } from '../baseclient'; import { DEBUG_BUILD } from '../debug-build'; @@ -8,26 +13,44 @@ import { COUNTER_METRIC_TYPE, DISTRIBUTION_METRIC_TYPE, GAUGE_METRIC_TYPE, SET_M import { MetricsAggregator, metricsAggregatorIntegration } from './integration'; import type { MetricType } from './types'; -interface MetricData { +export interface MetricData { unit?: MeasurementUnit; tags?: Record; timestamp?: number; } +type MetricsAggregatorConstructor = { + new (client: BaseClient): MetricsAggregatorInterface; +}; + +/** + * Global metrics aggregator instance. + * + * This is initialized on the first call to any `Sentry.metric.*` method. + */ +let globalMetricsAggregator: MetricsAggregatorInterface | undefined; + function addToMetricsAggregator( + Aggregator: MetricsAggregatorConstructor, metricType: MetricType, name: string, value: number | string, data: MetricData | undefined = {}, ): void { const client = getClient>(); - const scope = getCurrentScope(); + if (!client) { + return; + } + + if (!globalMetricsAggregator) { + const aggregator = (globalMetricsAggregator = new Aggregator(client)); + + client.on('flush', () => aggregator.flush()); + client.on('close', () => aggregator.close()); + } + if (client) { - if (!client.metricsAggregator) { - DEBUG_BUILD && - logger.warn('No metrics aggregator enabled. Please add the MetricsAggregator integration to use metrics APIs'); - return; - } + const scope = getCurrentScope(); const { unit, tags, timestamp } = data; const { release, environment } = client.getOptions(); // eslint-disable-next-line deprecation/deprecation @@ -44,7 +67,7 @@ function addToMetricsAggregator( } DEBUG_BUILD && logger.log(`Adding value of ${value} to ${metricType} metric ${name}`); - client.metricsAggregator.add(metricType, name, value, unit, { ...metricTags, ...tags }, timestamp); + globalMetricsAggregator.add(metricType, name, value, unit, { ...metricTags, ...tags }, timestamp); } } @@ -53,8 +76,8 @@ function addToMetricsAggregator( * * @experimental This API is experimental and might have breaking changes in the future. */ -export function increment(name: string, value: number = 1, data?: MetricData): void { - addToMetricsAggregator(COUNTER_METRIC_TYPE, name, value, data); +function increment(aggregator: MetricsAggregatorConstructor, name: string, value: number = 1, data?: MetricData): void { + addToMetricsAggregator(aggregator, COUNTER_METRIC_TYPE, name, value, data); } /** @@ -62,8 +85,8 @@ export function increment(name: string, value: number = 1, data?: MetricData): v * * @experimental This API is experimental and might have breaking changes in the future. */ -export function distribution(name: string, value: number, data?: MetricData): void { - addToMetricsAggregator(DISTRIBUTION_METRIC_TYPE, name, value, data); +function distribution(aggregator: MetricsAggregatorConstructor, name: string, value: number, data?: MetricData): void { + addToMetricsAggregator(aggregator, DISTRIBUTION_METRIC_TYPE, name, value, data); } /** @@ -71,8 +94,8 @@ export function distribution(name: string, value: number, data?: MetricData): vo * * @experimental This API is experimental and might have breaking changes in the future. */ -export function set(name: string, value: number | string, data?: MetricData): void { - addToMetricsAggregator(SET_METRIC_TYPE, name, value, data); +function set(aggregator: MetricsAggregatorConstructor, name: string, value: number | string, data?: MetricData): void { + addToMetricsAggregator(aggregator, SET_METRIC_TYPE, name, value, data); } /** @@ -80,8 +103,8 @@ export function set(name: string, value: number | string, data?: MetricData): vo * * @experimental This API is experimental and might have breaking changes in the future. */ -export function gauge(name: string, value: number, data?: MetricData): void { - addToMetricsAggregator(GAUGE_METRIC_TYPE, name, value, data); +function gauge(aggregator: MetricsAggregatorConstructor, name: string, value: number, data?: MetricData): void { + addToMetricsAggregator(aggregator, GAUGE_METRIC_TYPE, name, value, data); } export const metrics = { diff --git a/packages/core/src/metrics/integration.ts b/packages/core/src/metrics/integration.ts index af797bd8adf4..4a91c1fa2a58 100644 --- a/packages/core/src/metrics/integration.ts +++ b/packages/core/src/metrics/integration.ts @@ -1,7 +1,8 @@ import type { Client, ClientOptions, Integration, IntegrationClass, IntegrationFn } from '@sentry/types'; import type { BaseClient } from '../baseclient'; import { convertIntegrationFnToClass, defineIntegration } from '../integration'; -import { BrowserMetricsAggregator } from './browser-aggregator'; + +// TODO (v8): Remove this entire file const INTEGRATION_NAME = 'MetricsAggregator'; @@ -10,22 +11,26 @@ const _metricsAggregatorIntegration = (() => { name: INTEGRATION_NAME, // TODO v8: Remove this setupOnce() {}, // eslint-disable-line @typescript-eslint/no-empty-function - setup(client: BaseClient) { - client.metricsAggregator = new BrowserMetricsAggregator(client); + setup(_client: BaseClient) { + // }, }; }) satisfies IntegrationFn; +/** + * @deprecated An integration is no longer required to use the metrics feature + */ export const metricsAggregatorIntegration = defineIntegration(_metricsAggregatorIntegration); /** * Enables Sentry metrics monitoring. * * @experimental This API is experimental and might having breaking changes in the future. - * @deprecated Use `metricsAggegratorIntegration()` instead. + * @deprecated An integration is no longer required to use the metrics feature */ // eslint-disable-next-line deprecation/deprecation export const MetricsAggregator = convertIntegrationFnToClass( INTEGRATION_NAME, + // eslint-disable-next-line deprecation/deprecation metricsAggregatorIntegration, ) as IntegrationClass void }>; diff --git a/packages/core/src/metrics/metric-summary.ts b/packages/core/src/metrics/metric-summary.ts index ede2330bffcf..bf2e828dae1b 100644 --- a/packages/core/src/metrics/metric-summary.ts +++ b/packages/core/src/metrics/metric-summary.ts @@ -32,7 +32,7 @@ export function getMetricSummaryJsonForSpan(span: Span): Record; export declare function flush(timeout?: number | undefined): PromiseLike; +export declare const metrics: typeof clientSdk.metrics & typeof serverSdk.metrics; + /** * @deprecated This function will be removed in the next major version of the Sentry SDK. */ diff --git a/packages/types/src/client.ts b/packages/types/src/client.ts index 6c4409185e2d..3538fb7c41f3 100644 --- a/packages/types/src/client.ts +++ b/packages/types/src/client.ts @@ -272,6 +272,16 @@ export interface Client { */ on?(hook: 'startNavigationSpan', callback: (options: StartSpanOptions) => void): void; + /** + * A hook that is called when the client is flushing + */ + on?(hook: 'flush', callback: () => void): void; + + /** + * A hook that is called when the client is closing + */ + on?(hook: 'close', callback: () => void): void; + /** * Fire a hook event for transaction start. * Expects to be given a transaction as the second argument. @@ -343,5 +353,15 @@ export interface Client { */ emit?(hook: 'startNavigationSpan', options: StartSpanOptions): void; + /** + * Emit a hook event for client flush + */ + emit?(hook: 'flush'): void; + + /** + * Emit a hook event for client close + */ + emit?(hook: 'close'): void; + /* eslint-enable @typescript-eslint/unified-signatures */ }