Skip to content

Commit d12ba2e

Browse files
chargomeLms24
andauthored
feat(metrics): Add default server.address attribute on server runtimes (#18242)
Attaches a `server.address` attribute to all captured metrics on a `serverRuntimeClient` Did this by emitting a new `processMetric` hook in core, that we listen to in the `serverRuntimeClient`. This way we do not need to re-export all metrics functions from server runtime packages and still only get a minimal client bundle size bump. Added integration tests for node + cloudflare closes #18240 closes https://linear.app/getsentry/issue/JS-1178/attach-serveraddress-as-a-default-attribute-to-metrics --------- Co-authored-by: Lukas Stracke <lukas.stracke@sentry.io>
1 parent 935ef55 commit d12ba2e

File tree

11 files changed

+283
-0
lines changed

11 files changed

+283
-0
lines changed
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import * as Sentry from '@sentry/cloudflare';
2+
3+
interface Env {
4+
SENTRY_DSN: string;
5+
}
6+
7+
export default Sentry.withSentry(
8+
(env: Env) => ({
9+
dsn: env.SENTRY_DSN,
10+
release: '1.0.0',
11+
environment: 'test',
12+
serverName: 'mi-servidor.com',
13+
}),
14+
{
15+
async fetch(_request, _env, _ctx) {
16+
Sentry.metrics.count('test.counter', 1, { attributes: { endpoint: '/api/test' } });
17+
await Sentry.flush();
18+
return new Response('OK');
19+
},
20+
},
21+
);
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import type { SerializedMetricContainer } from '@sentry/core';
2+
import { expect, it } from 'vitest';
3+
import { createRunner } from '../../../../runner';
4+
5+
it('should add server.address attribute to metrics when serverName is set', async ({ signal }) => {
6+
const runner = createRunner(__dirname)
7+
.expect(envelope => {
8+
const metric = envelope[1]?.[0]?.[1] as SerializedMetricContainer;
9+
10+
expect(metric.items[0]).toEqual(
11+
expect.objectContaining({
12+
name: 'test.counter',
13+
type: 'counter',
14+
value: 1,
15+
span_id: expect.any(String),
16+
timestamp: expect.any(Number),
17+
trace_id: expect.any(String),
18+
attributes: {
19+
endpoint: {
20+
type: 'string',
21+
value: '/api/test',
22+
},
23+
'sentry.environment': {
24+
type: 'string',
25+
value: 'test',
26+
},
27+
'sentry.release': {
28+
type: 'string',
29+
value: expect.any(String),
30+
},
31+
'sentry.sdk.name': {
32+
type: 'string',
33+
value: 'sentry.javascript.cloudflare',
34+
},
35+
'sentry.sdk.version': {
36+
type: 'string',
37+
value: expect.any(String),
38+
},
39+
'server.address': {
40+
type: 'string',
41+
value: 'mi-servidor.com',
42+
},
43+
},
44+
}),
45+
);
46+
})
47+
.start(signal);
48+
await runner.makeRequest('get', '/');
49+
await runner.completed();
50+
});
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"name": "worker-name",
3+
"compatibility_date": "2025-06-17",
4+
"main": "index.ts",
5+
"compatibility_flags": ["nodejs_compat"],
6+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import * as Sentry from '@sentry/node';
2+
import { loggingTransport } from '@sentry-internal/node-integration-tests';
3+
4+
Sentry.init({
5+
dsn: 'https://public@dsn.ingest.sentry.io/1337',
6+
release: '1.0.0',
7+
environment: 'test',
8+
serverName: 'mi-servidor.com',
9+
transport: loggingTransport,
10+
});
11+
12+
async function run(): Promise<void> {
13+
Sentry.metrics.count('test.counter', 1, { attributes: { endpoint: '/api/test' } });
14+
15+
await Sentry.flush();
16+
}
17+
18+
// eslint-disable-next-line @typescript-eslint/no-floating-promises
19+
run();
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { afterAll, describe, expect, test } from 'vitest';
2+
import { cleanupChildProcesses, createRunner } from '../../../../utils/runner';
3+
4+
describe('metrics server.address', () => {
5+
afterAll(() => {
6+
cleanupChildProcesses();
7+
});
8+
9+
test('should add server.address attribute to metrics when serverName is set', async () => {
10+
const runner = createRunner(__dirname, 'scenario.ts')
11+
.expect({
12+
trace_metric: {
13+
items: [
14+
{
15+
timestamp: expect.any(Number),
16+
trace_id: expect.any(String),
17+
name: 'test.counter',
18+
type: 'counter',
19+
value: 1,
20+
attributes: {
21+
endpoint: { value: '/api/test', type: 'string' },
22+
'server.address': { value: 'mi-servidor.com', type: 'string' },
23+
'sentry.release': { value: '1.0.0', type: 'string' },
24+
'sentry.environment': { value: 'test', type: 'string' },
25+
'sentry.sdk.name': { value: 'sentry.javascript.node', type: 'string' },
26+
'sentry.sdk.version': { value: expect.any(String), type: 'string' },
27+
},
28+
},
29+
],
30+
},
31+
})
32+
.start();
33+
34+
await runner.completed();
35+
});
36+
});
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import * as Sentry from '@sentry/node';
2+
import { loggingTransport } from '@sentry-internal/node-integration-tests';
3+
4+
Sentry.init({
5+
dsn: 'https://public@dsn.ingest.sentry.io/1337',
6+
release: '1.0.0',
7+
environment: 'test',
8+
transport: loggingTransport,
9+
});
10+
11+
async function run(): Promise<void> {
12+
Sentry.metrics.count('test.counter', 1, { attributes: { endpoint: '/api/test' } });
13+
14+
await Sentry.flush();
15+
}
16+
17+
// eslint-disable-next-line @typescript-eslint/no-floating-promises
18+
run();
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { afterAll, describe, expect, test } from 'vitest';
2+
import { cleanupChildProcesses, createRunner } from '../../../../utils/runner';
3+
4+
describe('metrics server.address', () => {
5+
afterAll(() => {
6+
cleanupChildProcesses();
7+
});
8+
9+
test('should add server.address attribute to metrics when serverName is set', async () => {
10+
const runner = createRunner(__dirname, 'scenario.ts')
11+
.expect({
12+
trace_metric: {
13+
items: [
14+
{
15+
timestamp: expect.any(Number),
16+
trace_id: expect.any(String),
17+
name: 'test.counter',
18+
type: 'counter',
19+
value: 1,
20+
attributes: {
21+
endpoint: { value: '/api/test', type: 'string' },
22+
'server.address': { value: expect.any(String), type: 'string' },
23+
'sentry.release': { value: '1.0.0', type: 'string' },
24+
'sentry.environment': { value: 'test', type: 'string' },
25+
'sentry.sdk.name': { value: 'sentry.javascript.node', type: 'string' },
26+
'sentry.sdk.version': { value: expect.any(String), type: 'string' },
27+
},
28+
},
29+
],
30+
},
31+
})
32+
.start();
33+
34+
await runner.completed();
35+
});
36+
});

packages/core/src/client.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -783,6 +783,13 @@ export abstract class Client<O extends ClientOptions = ClientOptions> {
783783
*/
784784
public on(hook: 'flushMetrics', callback: () => void): () => void;
785785

786+
/**
787+
* A hook that is called when a metric is processed before it is captured and before the `beforeSendMetric` callback is fired.
788+
*
789+
* @returns {() => void} A function that, when executed, removes the registered callback.
790+
*/
791+
public on(hook: 'processMetric', callback: (metric: Metric) => void): () => void;
792+
786793
/**
787794
* A hook that is called when a http server request is started.
788795
* This hook is called after request isolation, but before the request is processed.
@@ -992,6 +999,13 @@ export abstract class Client<O extends ClientOptions = ClientOptions> {
992999
*/
9931000
public emit(hook: 'flushMetrics'): void;
9941001

1002+
/**
1003+
*
1004+
* Emit a hook event for client to process a metric before it is captured.
1005+
* This hook is called before the `beforeSendMetric` callback is fired.
1006+
*/
1007+
public emit(hook: 'processMetric', metric: Metric): void;
1008+
9951009
/**
9961010
* Emit a hook event for client when a http server request is started.
9971011
* This hook is called after request isolation, but before the request is processed.

packages/core/src/metrics/internal.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,8 @@ export function _INTERNAL_captureMetric(beforeMetric: Metric, options?: Internal
227227
// Enrich metric with contextual attributes
228228
const enrichedMetric = _enrichMetricAttributes(beforeMetric, client, currentScope);
229229

230+
client.emit('processMetric', enrichedMetric);
231+
230232
// todo(v11): Remove the experimental `beforeSendMetric`
231233
// eslint-disable-next-line deprecation/deprecation
232234
const beforeSendCallback = beforeSendMetric || _experiments?.beforeSendMetric;

packages/core/src/server-runtime-client.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ export class ServerRuntimeClient<
4040
addUserAgentToTransportHeaders(options);
4141

4242
super(options);
43+
44+
this._setUpMetricsProcessing();
4345
}
4446

4547
/**
@@ -176,6 +178,20 @@ export class ServerRuntimeClient<
176178

177179
return super._prepareEvent(event, hint, currentScope, isolationScope);
178180
}
181+
182+
/**
183+
* Process a server-side metric before it is captured.
184+
*/
185+
private _setUpMetricsProcessing(): void {
186+
this.on('processMetric', metric => {
187+
if (this._options.serverName) {
188+
metric.attributes = {
189+
'server.address': this._options.serverName,
190+
...metric.attributes,
191+
};
192+
}
193+
});
194+
}
179195
}
180196

181197
function setCurrentRequestSessionErroredOrCrashed(eventHint?: EventHint): void {

0 commit comments

Comments
 (0)