apcore-js 0.3.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,6 +1,7 @@
1
1
  import { describe, it, expect } from 'vitest';
2
2
  import { MetricsCollector, MetricsMiddleware } from '../../src/observability/metrics.js';
3
3
  import { Context } from '../../src/context.js';
4
+ import { ModuleError } from '../../src/errors.js';
4
5
 
5
6
  describe('MetricsCollector', () => {
6
7
  it('increments counters', () => {
@@ -42,6 +43,19 @@ describe('MetricsCollector', () => {
42
43
  expect(Object.keys(snap['counters'] as Record<string, unknown>)).toHaveLength(0);
43
44
  });
44
45
 
46
+ it('snapshot returns counters and histogram sub-keys', () => {
47
+ const collector = new MetricsCollector();
48
+ collector.increment('req_total', { status: '200' });
49
+ collector.observe('req_duration', { route: '/health' }, 0.01);
50
+ const snap = collector.snapshot();
51
+ expect(snap).toHaveProperty('counters');
52
+ expect(snap).toHaveProperty('histograms');
53
+ const hists = snap['histograms'] as Record<string, unknown>;
54
+ expect(hists).toHaveProperty('sums');
55
+ expect(hists).toHaveProperty('counts');
56
+ expect(hists).toHaveProperty('buckets');
57
+ });
58
+
45
59
  it('exportPrometheus produces valid format', () => {
46
60
  const collector = new MetricsCollector();
47
61
  collector.incrementCalls('mod.a', 'success');
@@ -57,6 +71,26 @@ describe('MetricsCollector', () => {
57
71
  const collector = new MetricsCollector();
58
72
  expect(collector.exportPrometheus()).toBe('');
59
73
  });
74
+
75
+ it('accepts custom buckets and uses them for observations', () => {
76
+ const collector = new MetricsCollector([0.1, 0.5, 1.0]);
77
+ collector.observe('custom_hist', { op: 'read' }, 0.3);
78
+ const snap = collector.snapshot();
79
+ const hists = snap['histograms'] as Record<string, Record<string, number>>;
80
+ // value 0.3 falls in the 0.5 bucket but not 0.1
81
+ expect(hists['buckets']['custom_hist|op=read|0.1']).toBeUndefined();
82
+ expect(hists['buckets']['custom_hist|op=read|0.5']).toBe(1);
83
+ expect(hists['buckets']['custom_hist|op=read|Inf']).toBe(1);
84
+ });
85
+
86
+ it('exportPrometheus omits label braces when metric has no labels', () => {
87
+ const collector = new MetricsCollector();
88
+ // increment with empty labels so parseLabels receives '' and formatLabels receives {}
89
+ collector.increment('no_label_counter', {});
90
+ const output = collector.exportPrometheus();
91
+ expect(output).toContain('no_label_counter 1');
92
+ expect(output).not.toContain('no_label_counter{');
93
+ });
60
94
  });
61
95
 
62
96
  describe('MetricsMiddleware', () => {
@@ -73,7 +107,7 @@ describe('MetricsMiddleware', () => {
73
107
  expect(counters['apcore_module_calls_total|module_id=mod.a,status=success']).toBe(1);
74
108
  });
75
109
 
76
- it('records error metrics on failure', () => {
110
+ it('records error metrics on failure with plain Error', () => {
77
111
  const collector = new MetricsCollector();
78
112
  const mw = new MetricsMiddleware(collector);
79
113
  const ctx = Context.create();
@@ -86,4 +120,67 @@ describe('MetricsMiddleware', () => {
86
120
  expect(counters['apcore_module_calls_total|module_id=mod.a,status=error']).toBe(1);
87
121
  expect(counters['apcore_module_errors_total|error_code=Error,module_id=mod.a']).toBe(1);
88
122
  });
123
+
124
+ it('records error code from ModuleError.code instead of constructor name', () => {
125
+ const collector = new MetricsCollector();
126
+ const mw = new MetricsMiddleware(collector);
127
+ const ctx = Context.create();
128
+
129
+ mw.before('mod.b', {}, ctx);
130
+ mw.onError('mod.b', {}, new ModuleError('CUSTOM_CODE', 'something went wrong'), ctx);
131
+
132
+ const snap = collector.snapshot();
133
+ const counters = snap['counters'] as Record<string, number>;
134
+ expect(counters['apcore_module_errors_total|error_code=CUSTOM_CODE,module_id=mod.b']).toBe(1);
135
+ });
136
+
137
+ it('after() returns null without recording metrics when starts is undefined', () => {
138
+ const collector = new MetricsCollector();
139
+ const mw = new MetricsMiddleware(collector);
140
+ const ctx = new Context('trace-id', null, [], null, null);
141
+
142
+ const result = mw.after('mod.a', {}, { result: 'ok' }, ctx);
143
+
144
+ expect(result).toBeNull();
145
+ const snap = collector.snapshot();
146
+ expect(Object.keys(snap['counters'] as Record<string, unknown>)).toHaveLength(0);
147
+ });
148
+
149
+ it('after() returns null without recording metrics when starts array is empty', () => {
150
+ const collector = new MetricsCollector();
151
+ const mw = new MetricsMiddleware(collector);
152
+ const ctx = new Context('trace-id', null, [], null, null);
153
+ ctx.data['_metrics_starts'] = [];
154
+
155
+ const result = mw.after('mod.a', {}, { result: 'ok' }, ctx);
156
+
157
+ expect(result).toBeNull();
158
+ const snap = collector.snapshot();
159
+ expect(Object.keys(snap['counters'] as Record<string, unknown>)).toHaveLength(0);
160
+ });
161
+
162
+ it('onError() returns null without recording metrics when starts is undefined', () => {
163
+ const collector = new MetricsCollector();
164
+ const mw = new MetricsMiddleware(collector);
165
+ const ctx = new Context('trace-id', null, [], null, null);
166
+
167
+ const result = mw.onError('mod.a', {}, new Error('boom'), ctx);
168
+
169
+ expect(result).toBeNull();
170
+ const snap = collector.snapshot();
171
+ expect(Object.keys(snap['counters'] as Record<string, unknown>)).toHaveLength(0);
172
+ });
173
+
174
+ it('onError() returns null without recording metrics when starts array is empty', () => {
175
+ const collector = new MetricsCollector();
176
+ const mw = new MetricsMiddleware(collector);
177
+ const ctx = new Context('trace-id', null, [], null, null);
178
+ ctx.data['_metrics_starts'] = [];
179
+
180
+ const result = mw.onError('mod.a', {}, new Error('boom'), ctx);
181
+
182
+ expect(result).toBeNull();
183
+ const snap = collector.snapshot();
184
+ expect(Object.keys(snap['counters'] as Record<string, unknown>)).toHaveLength(0);
185
+ });
89
186
  });