@devmoods/sentry 0.2.0 → 0.4.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="1777360796652" clover="3.2.0">
3
+ <project timestamp="1777360796652" 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="14" 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":14,"column":13},"end":{"line":19,"column":2}}},"fnMap":{},"branchMap":{},"s":{"0":10},"f":{},"b":{},"meta":{"lastBranch":0,"lastFunction":0,"lastStatement":1,"seen":{"s:14:13:19: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-28T07:19:56.646Z
105
105
  </div>
106
106
  <script src="prettify.js"></script>
107
107
  <script>
@@ -185,7 +185,120 @@
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>
234
+ <a name='L169'></a><a href='#L169'>169</a>
235
+ <a name='L170'></a><a href='#L170'>170</a>
236
+ <a name='L171'></a><a href='#L171'>171</a>
237
+ <a name='L172'></a><a href='#L172'>172</a>
238
+ <a name='L173'></a><a href='#L173'>173</a>
239
+ <a name='L174'></a><a href='#L174'>174</a>
240
+ <a name='L175'></a><a href='#L175'>175</a>
241
+ <a name='L176'></a><a href='#L176'>176</a>
242
+ <a name='L177'></a><a href='#L177'>177</a>
243
+ <a name='L178'></a><a href='#L178'>178</a>
244
+ <a name='L179'></a><a href='#L179'>179</a></td><td class="line-coverage quiet"><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-yes">10x</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>
280
+ <span class="cline-any cline-neutral">&nbsp;</span>
281
+ <span class="cline-any cline-neutral">&nbsp;</span>
282
+ <span class="cline-any cline-neutral">&nbsp;</span>
283
+ <span class="cline-any cline-neutral">&nbsp;</span>
284
+ <span class="cline-any cline-neutral">&nbsp;</span>
285
+ <span class="cline-any cline-neutral">&nbsp;</span>
286
+ <span class="cline-any cline-neutral">&nbsp;</span>
287
+ <span class="cline-any cline-neutral">&nbsp;</span>
288
+ <span class="cline-any cline-neutral">&nbsp;</span>
289
+ <span class="cline-any cline-neutral">&nbsp;</span>
290
+ <span class="cline-any cline-neutral">&nbsp;</span>
291
+ <span class="cline-any cline-neutral">&nbsp;</span>
292
+ <span class="cline-any cline-neutral">&nbsp;</span>
293
+ <span class="cline-any cline-neutral">&nbsp;</span>
294
+ <span class="cline-any cline-neutral">&nbsp;</span>
295
+ <span class="cline-any cline-neutral">&nbsp;</span>
296
+ <span class="cline-any cline-neutral">&nbsp;</span>
297
+ <span class="cline-any cline-neutral">&nbsp;</span>
298
+ <span class="cline-any cline-neutral">&nbsp;</span>
299
+ <span class="cline-any cline-neutral">&nbsp;</span>
300
+ <span class="cline-any cline-neutral">&nbsp;</span>
301
+ <span class="cline-any cline-neutral">&nbsp;</span>
189
302
  <span class="cline-any cline-neutral">&nbsp;</span>
190
303
  <span class="cline-any cline-neutral">&nbsp;</span>
191
304
  <span class="cline-any cline-neutral">&nbsp;</span>
@@ -194,7 +307,6 @@
194
307
  <span class="cline-any cline-neutral">&nbsp;</span>
195
308
  <span class="cline-any cline-neutral">&nbsp;</span>
196
309
  <span class="cline-any cline-neutral">&nbsp;</span>
197
- <span class="cline-any cline-yes">1x</span>
198
310
  <span class="cline-any cline-neutral">&nbsp;</span>
199
311
  <span class="cline-any cline-neutral">&nbsp;</span>
200
312
  <span class="cline-any cline-neutral">&nbsp;</span>
@@ -308,7 +420,11 @@
308
420
  <span class="cline-any cline-neutral">&nbsp;</span>
309
421
  <span class="cline-any cline-neutral">&nbsp;</span>
310
422
  <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';
423
+ import {
424
+ type LoggerAdapter,
425
+ type MetricAttributes,
426
+ type MetricsAdapter,
427
+ } from '@devmoods/observability';
312
428
  import * as Sentry from '@sentry/node';
313
429
  import { type ErrorRequestHandler } from 'express';
314
430
  &nbsp;
@@ -393,6 +509,58 @@ export function sentryErrorHandler(
393
509
  }) as unknown as ErrorRequestHandler;
394
510
  }
395
511
  &nbsp;
512
+ /**
513
+ * MetricsAdapter backed by `Sentry.metrics` (available in @sentry/node &gt;=10.25.0).
514
+ * Requires `Sentry.init()` to have been called; calls are silently dropped when
515
+ * Sentry is not initialised, matching the behaviour of SentryLoggerAdapter.
516
+ *
517
+ * Instrument mapping:
518
+ * increment → Sentry.metrics.count (monotonically increasing counter)
519
+ * gauge → Sentry.metrics.gauge (current absolute value)
520
+ * distribution → Sentry.metrics.distribution (arbitrary value distribution)
521
+ * timing → Sentry.metrics.distribution (distribution with unit "millisecond")
522
+ */
523
+ export class SentryMetricsAdapter implements MetricsAdapter {
524
+ increment(name: string, value = 1, attributes?: MetricAttributes): void {
525
+ /* v8 ignore next -- @preserve */
526
+ if (!Sentry.isInitialized()) {
527
+ return;
528
+ }
529
+ Sentry.metrics.count(name, value, { attributes });
530
+ }
531
+ &nbsp;
532
+ gauge(name: string, value: number, attributes?: MetricAttributes): void {
533
+ /* v8 ignore next -- @preserve */
534
+ if (!Sentry.isInitialized()) {
535
+ return;
536
+ }
537
+ Sentry.metrics.gauge(name, value, { attributes });
538
+ }
539
+ &nbsp;
540
+ distribution(
541
+ name: string,
542
+ value: number,
543
+ attributes?: MetricAttributes,
544
+ ): void {
545
+ /* v8 ignore next -- @preserve */
546
+ if (!Sentry.isInitialized()) {
547
+ return;
548
+ }
549
+ Sentry.metrics.distribution(name, value, { attributes });
550
+ }
551
+ &nbsp;
552
+ timing(name: string, value: number, attributes?: MetricAttributes): void {
553
+ /* v8 ignore next -- @preserve */
554
+ if (!Sentry.isInitialized()) {
555
+ return;
556
+ }
557
+ Sentry.metrics.distribution(name, value, {
558
+ unit: 'millisecond',
559
+ attributes,
560
+ });
561
+ }
562
+ }
563
+ &nbsp;
396
564
  export class SentryLoggerAdapter implements LoggerAdapter {
397
565
  constructor() {}
398
566
  &nbsp;
@@ -436,7 +604,7 @@ export class SentryLoggerAdapter implements LoggerAdapter {
436
604
  <div class='footer quiet pad2 space-top1 center small'>
437
605
  Code coverage generated by
438
606
  <a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
439
- at 2026-04-13T16:25:13.617Z
607
+ at 2026-04-28T07:19:56.646Z
440
608
  </div>
441
609
  <script src="prettify.js"></script>
442
610
  <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.4.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
- "build": "pnpm run clean && tsgo",
21
+ "build": "pnpm run clean && tsc",
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.3.0",
26
+ "@devmoods/observability": "0.4.0",
27
+ "@sentry/node": "^10.50.0",
28
+ "@sentry/node-native": "^10.50.0",
29
+ "@sentry/profiling-node": "^10.50.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": "97c5d5f84397815a726bf44ae3f401f24c852fc6"
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