From 3075cfd470397db65d6b5044de5a58bce45f33db Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Tue, 18 Nov 2025 13:07:24 +0100 Subject: [PATCH 1/4] add hook --- packages/core/src/client.ts | 14 ++++ packages/core/src/metrics/internal.ts | 2 + packages/core/src/server-runtime-client.ts | 16 +++++ .../test/lib/server-runtime-client.test.ts | 65 +++++++++++++++++++ 4 files changed, 97 insertions(+) diff --git a/packages/core/src/client.ts b/packages/core/src/client.ts index f363e61becd7..1c925d930036 100644 --- a/packages/core/src/client.ts +++ b/packages/core/src/client.ts @@ -783,6 +783,13 @@ export abstract class Client { */ public on(hook: 'flushMetrics', callback: () => void): () => void; + /** + * A hook that is called when a metric is processed before it is captured and before the `beforeSendMetric` callback is fired. + * + * @returns {() => void} A function that, when executed, removes the registered callback. + */ + public on(hook: 'processMetric', callback: (metric: Metric) => void): () => void; + /** * A hook that is called when a http server request is started. * This hook is called after request isolation, but before the request is processed. @@ -992,6 +999,13 @@ export abstract class Client { */ public emit(hook: 'flushMetrics'): void; + /** + * + * Emit a hook event for client to process a metric before it is captured. + * This hook is called before the `beforeSendMetric` callback is fired. + */ + public emit(hook: 'processMetric', metric: Metric): void; + /** * Emit a hook event for client when a http server request is started. * This hook is called after request isolation, but before the request is processed. diff --git a/packages/core/src/metrics/internal.ts b/packages/core/src/metrics/internal.ts index b38d61b5195c..7ac1372d1285 100644 --- a/packages/core/src/metrics/internal.ts +++ b/packages/core/src/metrics/internal.ts @@ -227,6 +227,8 @@ export function _INTERNAL_captureMetric(beforeMetric: Metric, options?: Internal // Enrich metric with contextual attributes const enrichedMetric = _enrichMetricAttributes(beforeMetric, client, currentScope); + client.emit('processMetric', enrichedMetric); + // todo(v11): Remove the experimental `beforeSendMetric` // eslint-disable-next-line deprecation/deprecation const beforeSendCallback = beforeSendMetric || _experiments?.beforeSendMetric; diff --git a/packages/core/src/server-runtime-client.ts b/packages/core/src/server-runtime-client.ts index 988e642d0a27..131b708048c5 100644 --- a/packages/core/src/server-runtime-client.ts +++ b/packages/core/src/server-runtime-client.ts @@ -40,6 +40,8 @@ export class ServerRuntimeClient< addUserAgentToTransportHeaders(options); super(options); + + this._setUpMetricsProcessing(); } /** @@ -176,6 +178,20 @@ export class ServerRuntimeClient< return super._prepareEvent(event, hint, currentScope, isolationScope); } + + /** + * Process a server-side metric before it is captured. + */ + private _setUpMetricsProcessing(): void { + this.on('processMetric', metric => { + if (this._options.serverName) { + metric.attributes = { + ...metric.attributes, + 'server.address': metric.attributes?.['server.address'] ?? this._options.serverName, + }; + } + }); + } } function setCurrentRequestSessionErroredOrCrashed(eventHint?: EventHint): void { diff --git a/packages/core/test/lib/server-runtime-client.test.ts b/packages/core/test/lib/server-runtime-client.test.ts index 3c5fe874af9f..24fb60d187ef 100644 --- a/packages/core/test/lib/server-runtime-client.test.ts +++ b/packages/core/test/lib/server-runtime-client.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it, test, vi } from 'vitest'; import { applySdkMetadata, createTransport, Scope } from '../../src'; +import { _INTERNAL_captureMetric, _INTERNAL_getMetricBuffer } from '../../src/metrics/internal'; import type { ServerRuntimeClientOptions } from '../../src/server-runtime-client'; import { ServerRuntimeClient } from '../../src/server-runtime-client'; import type { Event, EventHint } from '../../src/types-hoist/event'; @@ -236,4 +237,68 @@ describe('ServerRuntimeClient', () => { }); }); }); + + describe('metrics processing', () => { + it('adds server.address attribute to metrics when serverName is set', () => { + const options = getDefaultClientOptions({ dsn: PUBLIC_DSN, serverName: 'my-server.example.com' }); + client = new ServerRuntimeClient(options); + const scope = new Scope(); + scope.setClient(client); + + _INTERNAL_captureMetric({ type: 'counter', name: 'test.metric', value: 1 }, { scope }); + + const metricAttributes = _INTERNAL_getMetricBuffer(client)?.[0]?.attributes; + expect(metricAttributes).toEqual( + expect.objectContaining({ + 'server.address': { + value: 'my-server.example.com', + type: 'string', + }, + }), + ); + }); + + it('does not add server.address attribute when serverName is not set', () => { + const options = getDefaultClientOptions({ dsn: PUBLIC_DSN }); + client = new ServerRuntimeClient(options); + const scope = new Scope(); + scope.setClient(client); + + _INTERNAL_captureMetric({ type: 'counter', name: 'test.metric', value: 1 }, { scope }); + + const metricAttributes = _INTERNAL_getMetricBuffer(client)?.[0]?.attributes; + expect(metricAttributes).not.toEqual( + expect.objectContaining({ + 'server.address': expect.anything(), + }), + ); + }); + + it('does not overwrite existing server.address attribute', () => { + const options = getDefaultClientOptions({ dsn: PUBLIC_DSN, serverName: 'my-server.example.com' }); + client = new ServerRuntimeClient(options); + const scope = new Scope(); + scope.setClient(client); + + _INTERNAL_captureMetric( + { + type: 'counter', + name: 'test.metric', + value: 1, + attributes: { 'server.address': 'existing-server.example.com' }, + }, + { scope }, + ); + + const metricAttributes = _INTERNAL_getMetricBuffer(client)?.[0]?.attributes; + expect(metricAttributes).toEqual( + expect.objectContaining({ + 'server.address': { + value: 'existing-server.example.com', + type: 'string', + }, + }), + ); + }); + }); }); From 867d345e3c2d2ee688d94f01c8ae17ce44bc0ebd Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Tue, 18 Nov 2025 13:07:36 +0100 Subject: [PATCH 2/4] add integration tests --- .../metrics/server-address-option/scenario.ts | 19 ++++++++++ .../metrics/server-address-option/test.ts | 36 +++++++++++++++++++ .../metrics/server-address/scenario.ts | 18 ++++++++++ .../public-api/metrics/server-address/test.ts | 36 +++++++++++++++++++ 4 files changed, 109 insertions(+) create mode 100644 dev-packages/node-integration-tests/suites/public-api/metrics/server-address-option/scenario.ts create mode 100644 dev-packages/node-integration-tests/suites/public-api/metrics/server-address-option/test.ts create mode 100644 dev-packages/node-integration-tests/suites/public-api/metrics/server-address/scenario.ts create mode 100644 dev-packages/node-integration-tests/suites/public-api/metrics/server-address/test.ts diff --git a/dev-packages/node-integration-tests/suites/public-api/metrics/server-address-option/scenario.ts b/dev-packages/node-integration-tests/suites/public-api/metrics/server-address-option/scenario.ts new file mode 100644 index 000000000000..1f6d60f91cac --- /dev/null +++ b/dev-packages/node-integration-tests/suites/public-api/metrics/server-address-option/scenario.ts @@ -0,0 +1,19 @@ +import * as Sentry from '@sentry/node'; +import { loggingTransport } from '@sentry-internal/node-integration-tests'; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0.0', + environment: 'test', + serverName: 'mi-servidor.com', + transport: loggingTransport, +}); + +async function run(): Promise { + Sentry.metrics.count('test.counter', 1, { attributes: { endpoint: '/api/test' } }); + + await Sentry.flush(); +} + +// eslint-disable-next-line @typescript-eslint/no-floating-promises +run(); diff --git a/dev-packages/node-integration-tests/suites/public-api/metrics/server-address-option/test.ts b/dev-packages/node-integration-tests/suites/public-api/metrics/server-address-option/test.ts new file mode 100644 index 000000000000..825d94f41624 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/public-api/metrics/server-address-option/test.ts @@ -0,0 +1,36 @@ +import { afterAll, describe, expect, test } from 'vitest'; +import { cleanupChildProcesses, createRunner } from '../../../../utils/runner'; + +describe('metrics server.address', () => { + afterAll(() => { + cleanupChildProcesses(); + }); + + test('should add server.address attribute to metrics when serverName is set', async () => { + const runner = createRunner(__dirname, 'scenario.ts') + .expect({ + trace_metric: { + items: [ + { + timestamp: expect.any(Number), + trace_id: expect.any(String), + name: 'test.counter', + type: 'counter', + value: 1, + attributes: { + endpoint: { value: '/api/test', type: 'string' }, + 'server.address': { value: 'mi-servidor.com', type: 'string' }, + 'sentry.release': { value: '1.0.0', type: 'string' }, + 'sentry.environment': { value: 'test', type: 'string' }, + 'sentry.sdk.name': { value: 'sentry.javascript.node', type: 'string' }, + 'sentry.sdk.version': { value: expect.any(String), type: 'string' }, + }, + }, + ], + }, + }) + .start(); + + await runner.completed(); + }); +}); diff --git a/dev-packages/node-integration-tests/suites/public-api/metrics/server-address/scenario.ts b/dev-packages/node-integration-tests/suites/public-api/metrics/server-address/scenario.ts new file mode 100644 index 000000000000..a985f2a0fce3 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/public-api/metrics/server-address/scenario.ts @@ -0,0 +1,18 @@ +import * as Sentry from '@sentry/node'; +import { loggingTransport } from '@sentry-internal/node-integration-tests'; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0.0', + environment: 'test', + transport: loggingTransport, +}); + +async function run(): Promise { + Sentry.metrics.count('test.counter', 1, { attributes: { endpoint: '/api/test' } }); + + await Sentry.flush(); +} + +// eslint-disable-next-line @typescript-eslint/no-floating-promises +run(); diff --git a/dev-packages/node-integration-tests/suites/public-api/metrics/server-address/test.ts b/dev-packages/node-integration-tests/suites/public-api/metrics/server-address/test.ts new file mode 100644 index 000000000000..1ee4eda2de3e --- /dev/null +++ b/dev-packages/node-integration-tests/suites/public-api/metrics/server-address/test.ts @@ -0,0 +1,36 @@ +import { afterAll, describe, expect, test } from 'vitest'; +import { cleanupChildProcesses, createRunner } from '../../../../utils/runner'; + +describe('metrics server.address', () => { + afterAll(() => { + cleanupChildProcesses(); + }); + + test('should add server.address attribute to metrics when serverName is set', async () => { + const runner = createRunner(__dirname, 'scenario.ts') + .expect({ + trace_metric: { + items: [ + { + timestamp: expect.any(Number), + trace_id: expect.any(String), + name: 'test.counter', + type: 'counter', + value: 1, + attributes: { + endpoint: { value: '/api/test', type: 'string' }, + 'server.address': { value: expect.any(String), type: 'string' }, + 'sentry.release': { value: '1.0.0', type: 'string' }, + 'sentry.environment': { value: 'test', type: 'string' }, + 'sentry.sdk.name': { value: 'sentry.javascript.node', type: 'string' }, + 'sentry.sdk.version': { value: expect.any(String), type: 'string' }, + }, + }, + ], + }, + }) + .start(); + + await runner.completed(); + }); +}); From d28c030a7a441bb03a5ef170b73a22cfa185b4e7 Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Tue, 18 Nov 2025 13:22:25 +0100 Subject: [PATCH 3/4] add cloudflare test --- .../metrics/server-address/index.ts | 21 ++++++++ .../public-api/metrics/server-address/test.ts | 50 +++++++++++++++++++ .../metrics/server-address/wrangler.jsonc | 6 +++ 3 files changed, 77 insertions(+) create mode 100644 dev-packages/cloudflare-integration-tests/suites/public-api/metrics/server-address/index.ts create mode 100644 dev-packages/cloudflare-integration-tests/suites/public-api/metrics/server-address/test.ts create mode 100644 dev-packages/cloudflare-integration-tests/suites/public-api/metrics/server-address/wrangler.jsonc diff --git a/dev-packages/cloudflare-integration-tests/suites/public-api/metrics/server-address/index.ts b/dev-packages/cloudflare-integration-tests/suites/public-api/metrics/server-address/index.ts new file mode 100644 index 000000000000..635fcfc8721e --- /dev/null +++ b/dev-packages/cloudflare-integration-tests/suites/public-api/metrics/server-address/index.ts @@ -0,0 +1,21 @@ +import * as Sentry from '@sentry/cloudflare'; + +interface Env { + SENTRY_DSN: string; +} + +export default Sentry.withSentry( + (env: Env) => ({ + dsn: env.SENTRY_DSN, + release: '1.0.0', + environment: 'test', + serverName: 'mi-servidor.com', + }), + { + async fetch(_request, _env, _ctx) { + Sentry.metrics.count('test.counter', 1, { attributes: { endpoint: '/api/test' } }); + await Sentry.flush(); + return new Response('OK'); + }, + }, +); diff --git a/dev-packages/cloudflare-integration-tests/suites/public-api/metrics/server-address/test.ts b/dev-packages/cloudflare-integration-tests/suites/public-api/metrics/server-address/test.ts new file mode 100644 index 000000000000..5ee5b0954e59 --- /dev/null +++ b/dev-packages/cloudflare-integration-tests/suites/public-api/metrics/server-address/test.ts @@ -0,0 +1,50 @@ +import type { SerializedMetricContainer } from '@sentry/core'; +import { expect, it } from 'vitest'; +import { createRunner } from '../../../../runner'; + +it('should add server.address attribute to metrics when serverName is set', async ({ signal }) => { + const runner = createRunner(__dirname) + .expect(envelope => { + const metric = envelope[1]?.[0]?.[1] as SerializedMetricContainer; + + expect(metric.items[0]).toEqual( + expect.objectContaining({ + name: 'test.counter', + type: 'counter', + value: 1, + span_id: expect.any(String), + timestamp: expect.any(Number), + trace_id: expect.any(String), + attributes: { + endpoint: { + type: 'string', + value: '/api/test', + }, + 'sentry.environment': { + type: 'string', + value: 'test', + }, + 'sentry.release': { + type: 'string', + value: expect.any(String), + }, + 'sentry.sdk.name': { + type: 'string', + value: 'sentry.javascript.cloudflare', + }, + 'sentry.sdk.version': { + type: 'string', + value: expect.any(String), + }, + 'server.address': { + type: 'string', + value: 'mi-servidor.com', + }, + }, + }), + ); + }) + .start(signal); + await runner.makeRequest('get', '/'); + await runner.completed(); +}); diff --git a/dev-packages/cloudflare-integration-tests/suites/public-api/metrics/server-address/wrangler.jsonc b/dev-packages/cloudflare-integration-tests/suites/public-api/metrics/server-address/wrangler.jsonc new file mode 100644 index 000000000000..d6be01281f0c --- /dev/null +++ b/dev-packages/cloudflare-integration-tests/suites/public-api/metrics/server-address/wrangler.jsonc @@ -0,0 +1,6 @@ +{ + "name": "worker-name", + "compatibility_date": "2025-06-17", + "main": "index.ts", + "compatibility_flags": ["nodejs_compat"], +} From 2edaa8c3ba7ac4b2eec6b5e3b4c56e123469f3af Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Tue, 18 Nov 2025 14:27:04 +0100 Subject: [PATCH 4/4] Update packages/core/src/server-runtime-client.ts Co-authored-by: Lukas Stracke --- packages/core/src/server-runtime-client.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/server-runtime-client.ts b/packages/core/src/server-runtime-client.ts index 131b708048c5..d1ae8e9063e6 100644 --- a/packages/core/src/server-runtime-client.ts +++ b/packages/core/src/server-runtime-client.ts @@ -186,8 +186,8 @@ export class ServerRuntimeClient< this.on('processMetric', metric => { if (this._options.serverName) { metric.attributes = { + 'server.address': this._options.serverName, ...metric.attributes, - 'server.address': metric.attributes?.['server.address'] ?? this._options.serverName, }; } });