@devmoods/sentry 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,10 +1,10 @@
1
1
  <?xml version="1.0" encoding="UTF-8"?>
2
- <coverage generated="1776097513623" clover="3.2.0">
3
- <project timestamp="1776097513623" name="All files">
2
+ <coverage generated="1776355785825" clover="3.2.0">
3
+ <project timestamp="1776355785825" name="All files">
4
4
  <metrics statements="1" coveredstatements="1" conditionals="0" coveredconditionals="0" methods="0" coveredmethods="0" elements="1" coveredelements="1" complexity="0" loc="1" ncloc="1" packages="1" files="1" classes="1"/>
5
5
  <file name="index.ts" path="/Users/hanse/dev/github.com/devmoods/express-extras/packages/@devmoods/sentry/src/index.ts">
6
6
  <metrics statements="1" coveredstatements="1" conditionals="0" coveredconditionals="0" methods="0" coveredmethods="0"/>
7
- <line num="10" count="1" type="stmt"/>
7
+ <line num="10" count="10" type="stmt"/>
8
8
  </file>
9
9
  </project>
10
10
  </coverage>
@@ -1,2 +1,2 @@
1
- {"/Users/hanse/dev/github.com/devmoods/express-extras/packages/@devmoods/sentry/src/index.ts": {"path":"/Users/hanse/dev/github.com/devmoods/express-extras/packages/@devmoods/sentry/src/index.ts","statementMap":{"0":{"start":{"line":10,"column":13},"end":{"line":15,"column":2}}},"fnMap":{},"branchMap":{},"s":{"0":1},"f":{},"b":{},"meta":{"lastBranch":0,"lastFunction":0,"lastStatement":1,"seen":{"s:10:13:15:2":0}}}
1
+ {"/Users/hanse/dev/github.com/devmoods/express-extras/packages/@devmoods/sentry/src/index.ts": {"path":"/Users/hanse/dev/github.com/devmoods/express-extras/packages/@devmoods/sentry/src/index.ts","statementMap":{"0":{"start":{"line":10,"column":13},"end":{"line":15,"column":2}}},"fnMap":{},"branchMap":{},"s":{"0":10},"f":{},"b":{},"meta":{"lastBranch":0,"lastFunction":0,"lastStatement":1,"seen":{"s:10:13:15:2":0}}}
2
2
  }
@@ -101,7 +101,7 @@
101
101
  <div class='footer quiet pad2 space-top1 center small'>
102
102
  Code coverage generated by
103
103
  <a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
104
- at 2026-04-13T16:25:13.617Z
104
+ at 2026-04-16T16:09:45.820Z
105
105
  </div>
106
106
  <script src="prettify.js"></script>
107
107
  <script>
@@ -185,7 +185,98 @@
185
185
  <a name='L120'></a><a href='#L120'>120</a>
186
186
  <a name='L121'></a><a href='#L121'>121</a>
187
187
  <a name='L122'></a><a href='#L122'>122</a>
188
- <a name='L123'></a><a href='#L123'>123</a></td><td class="line-coverage quiet"><span class="cline-any cline-neutral">&nbsp;</span>
188
+ <a name='L123'></a><a href='#L123'>123</a>
189
+ <a name='L124'></a><a href='#L124'>124</a>
190
+ <a name='L125'></a><a href='#L125'>125</a>
191
+ <a name='L126'></a><a href='#L126'>126</a>
192
+ <a name='L127'></a><a href='#L127'>127</a>
193
+ <a name='L128'></a><a href='#L128'>128</a>
194
+ <a name='L129'></a><a href='#L129'>129</a>
195
+ <a name='L130'></a><a href='#L130'>130</a>
196
+ <a name='L131'></a><a href='#L131'>131</a>
197
+ <a name='L132'></a><a href='#L132'>132</a>
198
+ <a name='L133'></a><a href='#L133'>133</a>
199
+ <a name='L134'></a><a href='#L134'>134</a>
200
+ <a name='L135'></a><a href='#L135'>135</a>
201
+ <a name='L136'></a><a href='#L136'>136</a>
202
+ <a name='L137'></a><a href='#L137'>137</a>
203
+ <a name='L138'></a><a href='#L138'>138</a>
204
+ <a name='L139'></a><a href='#L139'>139</a>
205
+ <a name='L140'></a><a href='#L140'>140</a>
206
+ <a name='L141'></a><a href='#L141'>141</a>
207
+ <a name='L142'></a><a href='#L142'>142</a>
208
+ <a name='L143'></a><a href='#L143'>143</a>
209
+ <a name='L144'></a><a href='#L144'>144</a>
210
+ <a name='L145'></a><a href='#L145'>145</a>
211
+ <a name='L146'></a><a href='#L146'>146</a>
212
+ <a name='L147'></a><a href='#L147'>147</a>
213
+ <a name='L148'></a><a href='#L148'>148</a>
214
+ <a name='L149'></a><a href='#L149'>149</a>
215
+ <a name='L150'></a><a href='#L150'>150</a>
216
+ <a name='L151'></a><a href='#L151'>151</a>
217
+ <a name='L152'></a><a href='#L152'>152</a>
218
+ <a name='L153'></a><a href='#L153'>153</a>
219
+ <a name='L154'></a><a href='#L154'>154</a>
220
+ <a name='L155'></a><a href='#L155'>155</a>
221
+ <a name='L156'></a><a href='#L156'>156</a>
222
+ <a name='L157'></a><a href='#L157'>157</a>
223
+ <a name='L158'></a><a href='#L158'>158</a>
224
+ <a name='L159'></a><a href='#L159'>159</a>
225
+ <a name='L160'></a><a href='#L160'>160</a>
226
+ <a name='L161'></a><a href='#L161'>161</a>
227
+ <a name='L162'></a><a href='#L162'>162</a>
228
+ <a name='L163'></a><a href='#L163'>163</a>
229
+ <a name='L164'></a><a href='#L164'>164</a>
230
+ <a name='L165'></a><a href='#L165'>165</a>
231
+ <a name='L166'></a><a href='#L166'>166</a>
232
+ <a name='L167'></a><a href='#L167'>167</a>
233
+ <a name='L168'></a><a href='#L168'>168</a></td><td class="line-coverage quiet"><span class="cline-any cline-neutral">&nbsp;</span>
234
+ <span class="cline-any cline-neutral">&nbsp;</span>
235
+ <span class="cline-any cline-neutral">&nbsp;</span>
236
+ <span class="cline-any cline-neutral">&nbsp;</span>
237
+ <span class="cline-any cline-neutral">&nbsp;</span>
238
+ <span class="cline-any cline-neutral">&nbsp;</span>
239
+ <span class="cline-any cline-neutral">&nbsp;</span>
240
+ <span class="cline-any cline-neutral">&nbsp;</span>
241
+ <span class="cline-any cline-neutral">&nbsp;</span>
242
+ <span class="cline-any cline-yes">10x</span>
243
+ <span class="cline-any cline-neutral">&nbsp;</span>
244
+ <span class="cline-any cline-neutral">&nbsp;</span>
245
+ <span class="cline-any cline-neutral">&nbsp;</span>
246
+ <span class="cline-any cline-neutral">&nbsp;</span>
247
+ <span class="cline-any cline-neutral">&nbsp;</span>
248
+ <span class="cline-any cline-neutral">&nbsp;</span>
249
+ <span class="cline-any cline-neutral">&nbsp;</span>
250
+ <span class="cline-any cline-neutral">&nbsp;</span>
251
+ <span class="cline-any cline-neutral">&nbsp;</span>
252
+ <span class="cline-any cline-neutral">&nbsp;</span>
253
+ <span class="cline-any cline-neutral">&nbsp;</span>
254
+ <span class="cline-any cline-neutral">&nbsp;</span>
255
+ <span class="cline-any cline-neutral">&nbsp;</span>
256
+ <span class="cline-any cline-neutral">&nbsp;</span>
257
+ <span class="cline-any cline-neutral">&nbsp;</span>
258
+ <span class="cline-any cline-neutral">&nbsp;</span>
259
+ <span class="cline-any cline-neutral">&nbsp;</span>
260
+ <span class="cline-any cline-neutral">&nbsp;</span>
261
+ <span class="cline-any cline-neutral">&nbsp;</span>
262
+ <span class="cline-any cline-neutral">&nbsp;</span>
263
+ <span class="cline-any cline-neutral">&nbsp;</span>
264
+ <span class="cline-any cline-neutral">&nbsp;</span>
265
+ <span class="cline-any cline-neutral">&nbsp;</span>
266
+ <span class="cline-any cline-neutral">&nbsp;</span>
267
+ <span class="cline-any cline-neutral">&nbsp;</span>
268
+ <span class="cline-any cline-neutral">&nbsp;</span>
269
+ <span class="cline-any cline-neutral">&nbsp;</span>
270
+ <span class="cline-any cline-neutral">&nbsp;</span>
271
+ <span class="cline-any cline-neutral">&nbsp;</span>
272
+ <span class="cline-any cline-neutral">&nbsp;</span>
273
+ <span class="cline-any cline-neutral">&nbsp;</span>
274
+ <span class="cline-any cline-neutral">&nbsp;</span>
275
+ <span class="cline-any cline-neutral">&nbsp;</span>
276
+ <span class="cline-any cline-neutral">&nbsp;</span>
277
+ <span class="cline-any cline-neutral">&nbsp;</span>
278
+ <span class="cline-any cline-neutral">&nbsp;</span>
279
+ <span class="cline-any cline-neutral">&nbsp;</span>
189
280
  <span class="cline-any cline-neutral">&nbsp;</span>
190
281
  <span class="cline-any cline-neutral">&nbsp;</span>
191
282
  <span class="cline-any cline-neutral">&nbsp;</span>
@@ -194,7 +285,6 @@
194
285
  <span class="cline-any cline-neutral">&nbsp;</span>
195
286
  <span class="cline-any cline-neutral">&nbsp;</span>
196
287
  <span class="cline-any cline-neutral">&nbsp;</span>
197
- <span class="cline-any cline-yes">1x</span>
198
288
  <span class="cline-any cline-neutral">&nbsp;</span>
199
289
  <span class="cline-any cline-neutral">&nbsp;</span>
200
290
  <span class="cline-any cline-neutral">&nbsp;</span>
@@ -308,7 +398,7 @@
308
398
  <span class="cline-any cline-neutral">&nbsp;</span>
309
399
  <span class="cline-any cline-neutral">&nbsp;</span>
310
400
  <span class="cline-any cline-neutral">&nbsp;</span></td><td class="text"><pre class="prettyprint lang-js">import { config, define, t, type ConfigFromDefs } from '@devmoods/config';
311
- import { type LoggerAdapter } from '@devmoods/observability';
401
+ import { type LoggerAdapter, type MetricAttributes, type MetricsAdapter } from '@devmoods/observability';
312
402
  import * as Sentry from '@sentry/node';
313
403
  import { type ErrorRequestHandler } from 'express';
314
404
  &nbsp;
@@ -393,6 +483,51 @@ export function sentryErrorHandler(
393
483
  }) as unknown as ErrorRequestHandler;
394
484
  }
395
485
  &nbsp;
486
+ /**
487
+ * MetricsAdapter backed by `Sentry.metrics` (available in @sentry/node &gt;=10.25.0).
488
+ * Requires `Sentry.init()` to have been called; calls are silently dropped when
489
+ * Sentry is not initialised, matching the behaviour of SentryLoggerAdapter.
490
+ *
491
+ * Instrument mapping:
492
+ * increment → Sentry.metrics.count (monotonically increasing counter)
493
+ * gauge → Sentry.metrics.gauge (current absolute value)
494
+ * distribution → Sentry.metrics.distribution (arbitrary value distribution)
495
+ * timing → Sentry.metrics.distribution (distribution with unit "millisecond")
496
+ */
497
+ export class SentryMetricsAdapter implements MetricsAdapter {
498
+ increment(name: string, value = 1, attributes?: MetricAttributes): void {
499
+ /* v8 ignore next -- @preserve */
500
+ if (!Sentry.isInitialized()) {
501
+ return;
502
+ }
503
+ Sentry.metrics.count(name, value, { attributes });
504
+ }
505
+ &nbsp;
506
+ gauge(name: string, value: number, attributes?: MetricAttributes): void {
507
+ /* v8 ignore next -- @preserve */
508
+ if (!Sentry.isInitialized()) {
509
+ return;
510
+ }
511
+ Sentry.metrics.gauge(name, value, { attributes });
512
+ }
513
+ &nbsp;
514
+ distribution(name: string, value: number, attributes?: MetricAttributes): void {
515
+ /* v8 ignore next -- @preserve */
516
+ if (!Sentry.isInitialized()) {
517
+ return;
518
+ }
519
+ Sentry.metrics.distribution(name, value, { attributes });
520
+ }
521
+ &nbsp;
522
+ timing(name: string, value: number, attributes?: MetricAttributes): void {
523
+ /* v8 ignore next -- @preserve */
524
+ if (!Sentry.isInitialized()) {
525
+ return;
526
+ }
527
+ Sentry.metrics.distribution(name, value, { unit: 'millisecond', attributes });
528
+ }
529
+ }
530
+ &nbsp;
396
531
  export class SentryLoggerAdapter implements LoggerAdapter {
397
532
  constructor() {}
398
533
  &nbsp;
@@ -436,7 +571,7 @@ export class SentryLoggerAdapter implements LoggerAdapter {
436
571
  <div class='footer quiet pad2 space-top1 center small'>
437
572
  Code coverage generated by
438
573
  <a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
439
- at 2026-04-13T16:25:13.617Z
574
+ at 2026-04-16T16:09:45.820Z
440
575
  </div>
441
576
  <script src="prettify.js"></script>
442
577
  <script>
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { type LoggerAdapter } from '@devmoods/observability';
1
+ import { type LoggerAdapter, type MetricAttributes, type MetricsAdapter } from '@devmoods/observability';
2
2
  import * as Sentry from '@sentry/node';
3
3
  import { type ErrorRequestHandler } from 'express';
4
4
  declare module '@devmoods/config' {
@@ -18,6 +18,23 @@ export declare function initSentry(options?: SentryOptions): Promise<void>;
18
18
  export declare function closeSentry(): Promise<void>;
19
19
  export declare function sentryOptions(options?: SentryOptions): Promise<SentryOptions>;
20
20
  export declare function sentryErrorHandler(options?: Sentry.NodeOptions): ErrorRequestHandler;
21
+ /**
22
+ * MetricsAdapter backed by `Sentry.metrics` (available in @sentry/node >=10.25.0).
23
+ * Requires `Sentry.init()` to have been called; calls are silently dropped when
24
+ * Sentry is not initialised, matching the behaviour of SentryLoggerAdapter.
25
+ *
26
+ * Instrument mapping:
27
+ * increment → Sentry.metrics.count (monotonically increasing counter)
28
+ * gauge → Sentry.metrics.gauge (current absolute value)
29
+ * distribution → Sentry.metrics.distribution (arbitrary value distribution)
30
+ * timing → Sentry.metrics.distribution (distribution with unit "millisecond")
31
+ */
32
+ export declare class SentryMetricsAdapter implements MetricsAdapter {
33
+ increment(name: string, value?: number, attributes?: MetricAttributes): void;
34
+ gauge(name: string, value: number, attributes?: MetricAttributes): void;
35
+ distribution(name: string, value: number, attributes?: MetricAttributes): void;
36
+ timing(name: string, value: number, attributes?: MetricAttributes): void;
37
+ }
21
38
  export declare class SentryLoggerAdapter implements LoggerAdapter {
22
39
  constructor();
23
40
  captureError(error: Error | null, attributes?: Record<string, any>): void;
package/dist/index.js CHANGED
@@ -61,6 +61,50 @@ export function sentryErrorHandler(options = {}) {
61
61
  ...options,
62
62
  });
63
63
  }
64
+ /**
65
+ * MetricsAdapter backed by `Sentry.metrics` (available in @sentry/node >=10.25.0).
66
+ * Requires `Sentry.init()` to have been called; calls are silently dropped when
67
+ * Sentry is not initialised, matching the behaviour of SentryLoggerAdapter.
68
+ *
69
+ * Instrument mapping:
70
+ * increment → Sentry.metrics.count (monotonically increasing counter)
71
+ * gauge → Sentry.metrics.gauge (current absolute value)
72
+ * distribution → Sentry.metrics.distribution (arbitrary value distribution)
73
+ * timing → Sentry.metrics.distribution (distribution with unit "millisecond")
74
+ */
75
+ export class SentryMetricsAdapter {
76
+ increment(name, value = 1, attributes) {
77
+ /* v8 ignore next -- @preserve */
78
+ if (!Sentry.isInitialized()) {
79
+ return;
80
+ }
81
+ Sentry.metrics.count(name, value, { attributes });
82
+ }
83
+ gauge(name, value, attributes) {
84
+ /* v8 ignore next -- @preserve */
85
+ if (!Sentry.isInitialized()) {
86
+ return;
87
+ }
88
+ Sentry.metrics.gauge(name, value, { attributes });
89
+ }
90
+ distribution(name, value, attributes) {
91
+ /* v8 ignore next -- @preserve */
92
+ if (!Sentry.isInitialized()) {
93
+ return;
94
+ }
95
+ Sentry.metrics.distribution(name, value, { attributes });
96
+ }
97
+ timing(name, value, attributes) {
98
+ /* v8 ignore next -- @preserve */
99
+ if (!Sentry.isInitialized()) {
100
+ return;
101
+ }
102
+ Sentry.metrics.distribution(name, value, {
103
+ unit: 'millisecond',
104
+ attributes,
105
+ });
106
+ }
107
+ }
64
108
  export class SentryLoggerAdapter {
65
109
  constructor() { }
66
110
  captureError(error, attributes) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@devmoods/sentry",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -14,22 +14,23 @@
14
14
  },
15
15
  "scripts": {
16
16
  "clean": "rm -rf dist",
17
- "lint": "eslint",
17
+ "lint": "oxlint .",
18
+ "format": "oxfmt .",
18
19
  "test": "vitest",
19
20
  "test:coverage": "pnpm run test --coverage",
20
21
  "build": "pnpm run clean && tsgo",
21
22
  "prepublishOnly": "pnpm run clean && pnpm run build"
22
23
  },
23
24
  "dependencies": {
24
- "@devmoods/config": "0.2.3",
25
- "@devmoods/observability": "0.2.0",
26
- "@sentry/node": "^10.48.0",
27
- "@sentry/node-native": "^10.48.0",
28
- "@sentry/profiling-node": "^10.48.0",
25
+ "@devmoods/config": "0.2.4",
26
+ "@devmoods/observability": "0.3.0",
27
+ "@sentry/node": "^10.49.0",
28
+ "@sentry/node-native": "^10.49.0",
29
+ "@sentry/profiling-node": "^10.49.0",
29
30
  "express": "^4.21.2"
30
31
  },
31
32
  "peerDependencies": {
32
33
  "express": ">=4.21.2 <5.0.0"
33
34
  },
34
- "gitHead": "ea4f51756aca98bd050cf98128bf4d4b3da1bf38"
35
+ "gitHead": "2c4100d80823bcf16bd962b4b8a26bd078522dfc"
35
36
  }
@@ -2,6 +2,38 @@ import { overrideConfig } from '@devmoods/config';
2
2
 
3
3
  import { sentryOptions } from '../index.js';
4
4
 
5
+ async function loadModuleWithSentryMock(initialized = true) {
6
+ vi.resetModules();
7
+
8
+ const sentryMock = {
9
+ init: vi.fn(),
10
+ close: vi.fn(),
11
+ isInitialized: vi.fn(() => initialized),
12
+ expressErrorHandler: vi.fn(() => vi.fn()),
13
+ captureException: vi.fn(),
14
+ logger: {
15
+ trace: vi.fn(),
16
+ debug: vi.fn(),
17
+ info: vi.fn(),
18
+ warn: vi.fn(),
19
+ error: vi.fn(),
20
+ fatal: vi.fn(),
21
+ },
22
+ metrics: {
23
+ count: vi.fn(),
24
+ gauge: vi.fn(),
25
+ distribution: vi.fn(),
26
+ },
27
+ };
28
+
29
+ vi.doMock('@sentry/node', () => sentryMock);
30
+
31
+ await import('../setupTests.js');
32
+ const module = await import('../index.js');
33
+
34
+ return { ...module, Sentry: sentryMock };
35
+ }
36
+
5
37
  test('sentryOptions', async () => {
6
38
  vi.spyOn(console, 'warn').mockImplementation(() => {});
7
39
 
@@ -26,3 +58,109 @@ test('sentryOptions', async () => {
26
58
 
27
59
  reset();
28
60
  });
61
+
62
+ describe('SentryMetricsAdapter', () => {
63
+ beforeEach(() => {
64
+ vi.restoreAllMocks();
65
+ });
66
+
67
+ test('increment calls Sentry.metrics.count with default value of 1', async () => {
68
+ const { Sentry, SentryMetricsAdapter } = await loadModuleWithSentryMock();
69
+ const adapter = new SentryMetricsAdapter();
70
+
71
+ adapter.increment('req.count');
72
+
73
+ expect(Sentry.metrics.count).toHaveBeenCalledWith('req.count', 1, {
74
+ attributes: undefined,
75
+ });
76
+ });
77
+
78
+ test('increment passes explicit value and attributes', async () => {
79
+ const { Sentry, SentryMetricsAdapter } = await loadModuleWithSentryMock();
80
+ const adapter = new SentryMetricsAdapter();
81
+
82
+ adapter.increment('req.count', 5, { status: '200' });
83
+
84
+ expect(Sentry.metrics.count).toHaveBeenCalledWith('req.count', 5, {
85
+ attributes: { status: '200' },
86
+ });
87
+ });
88
+
89
+ test('increment is a no-op when Sentry is not initialised', async () => {
90
+ const { Sentry, SentryMetricsAdapter } =
91
+ await loadModuleWithSentryMock(false);
92
+ const adapter = new SentryMetricsAdapter();
93
+
94
+ adapter.increment('req.count');
95
+
96
+ expect(Sentry.metrics.count).not.toHaveBeenCalled();
97
+ });
98
+
99
+ test('gauge calls Sentry.metrics.gauge', async () => {
100
+ const { Sentry, SentryMetricsAdapter } = await loadModuleWithSentryMock();
101
+ const adapter = new SentryMetricsAdapter();
102
+
103
+ adapter.gauge('mem.usage', 512, { host: 'web-1' });
104
+
105
+ expect(Sentry.metrics.gauge).toHaveBeenCalledWith('mem.usage', 512, {
106
+ attributes: { host: 'web-1' },
107
+ });
108
+ });
109
+
110
+ test('gauge is a no-op when Sentry is not initialised', async () => {
111
+ const { Sentry, SentryMetricsAdapter } =
112
+ await loadModuleWithSentryMock(false);
113
+ const adapter = new SentryMetricsAdapter();
114
+
115
+ adapter.gauge('mem.usage', 512);
116
+
117
+ expect(Sentry.metrics.gauge).not.toHaveBeenCalled();
118
+ });
119
+
120
+ test('distribution calls Sentry.metrics.distribution', async () => {
121
+ const { Sentry, SentryMetricsAdapter } = await loadModuleWithSentryMock();
122
+ const adapter = new SentryMetricsAdapter();
123
+
124
+ adapter.distribution('payload.size', 2048, { route: '/upload' });
125
+
126
+ expect(Sentry.metrics.distribution).toHaveBeenCalledWith(
127
+ 'payload.size',
128
+ 2048,
129
+ {
130
+ attributes: { route: '/upload' },
131
+ },
132
+ );
133
+ });
134
+
135
+ test('distribution is a no-op when Sentry is not initialised', async () => {
136
+ const { Sentry, SentryMetricsAdapter } =
137
+ await loadModuleWithSentryMock(false);
138
+ const adapter = new SentryMetricsAdapter();
139
+
140
+ adapter.distribution('payload.size', 2048);
141
+
142
+ expect(Sentry.metrics.distribution).not.toHaveBeenCalled();
143
+ });
144
+
145
+ test('timing calls Sentry.metrics.distribution with unit millisecond', async () => {
146
+ const { Sentry, SentryMetricsAdapter } = await loadModuleWithSentryMock();
147
+ const adapter = new SentryMetricsAdapter();
148
+
149
+ adapter.timing('db.query', 37, { table: 'users' });
150
+
151
+ expect(Sentry.metrics.distribution).toHaveBeenCalledWith('db.query', 37, {
152
+ unit: 'millisecond',
153
+ attributes: { table: 'users' },
154
+ });
155
+ });
156
+
157
+ test('timing is a no-op when Sentry is not initialised', async () => {
158
+ const { Sentry, SentryMetricsAdapter } =
159
+ await loadModuleWithSentryMock(false);
160
+ const adapter = new SentryMetricsAdapter();
161
+
162
+ adapter.timing('db.query', 37);
163
+
164
+ expect(Sentry.metrics.distribution).not.toHaveBeenCalled();
165
+ });
166
+ });
package/src/index.ts CHANGED
@@ -1,5 +1,9 @@
1
1
  import { config, define, t, type ConfigFromDefs } from '@devmoods/config';
2
- import { type LoggerAdapter } from '@devmoods/observability';
2
+ import {
3
+ type LoggerAdapter,
4
+ type MetricAttributes,
5
+ type MetricsAdapter,
6
+ } from '@devmoods/observability';
3
7
  import * as Sentry from '@sentry/node';
4
8
  import { type ErrorRequestHandler } from 'express';
5
9
 
@@ -84,6 +88,58 @@ export function sentryErrorHandler(
84
88
  }) as unknown as ErrorRequestHandler;
85
89
  }
86
90
 
91
+ /**
92
+ * MetricsAdapter backed by `Sentry.metrics` (available in @sentry/node >=10.25.0).
93
+ * Requires `Sentry.init()` to have been called; calls are silently dropped when
94
+ * Sentry is not initialised, matching the behaviour of SentryLoggerAdapter.
95
+ *
96
+ * Instrument mapping:
97
+ * increment → Sentry.metrics.count (monotonically increasing counter)
98
+ * gauge → Sentry.metrics.gauge (current absolute value)
99
+ * distribution → Sentry.metrics.distribution (arbitrary value distribution)
100
+ * timing → Sentry.metrics.distribution (distribution with unit "millisecond")
101
+ */
102
+ export class SentryMetricsAdapter implements MetricsAdapter {
103
+ increment(name: string, value = 1, attributes?: MetricAttributes): void {
104
+ /* v8 ignore next -- @preserve */
105
+ if (!Sentry.isInitialized()) {
106
+ return;
107
+ }
108
+ Sentry.metrics.count(name, value, { attributes });
109
+ }
110
+
111
+ gauge(name: string, value: number, attributes?: MetricAttributes): void {
112
+ /* v8 ignore next -- @preserve */
113
+ if (!Sentry.isInitialized()) {
114
+ return;
115
+ }
116
+ Sentry.metrics.gauge(name, value, { attributes });
117
+ }
118
+
119
+ distribution(
120
+ name: string,
121
+ value: number,
122
+ attributes?: MetricAttributes,
123
+ ): void {
124
+ /* v8 ignore next -- @preserve */
125
+ if (!Sentry.isInitialized()) {
126
+ return;
127
+ }
128
+ Sentry.metrics.distribution(name, value, { attributes });
129
+ }
130
+
131
+ timing(name: string, value: number, attributes?: MetricAttributes): void {
132
+ /* v8 ignore next -- @preserve */
133
+ if (!Sentry.isInitialized()) {
134
+ return;
135
+ }
136
+ Sentry.metrics.distribution(name, value, {
137
+ unit: 'millisecond',
138
+ attributes,
139
+ });
140
+ }
141
+ }
142
+
87
143
  export class SentryLoggerAdapter implements LoggerAdapter {
88
144
  constructor() {}
89
145