@lib-acr/node 0.0.0-stage → 0.0.1

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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Application Control Room
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,63 @@
1
- # Temporary Holding Version
1
+ # @lib-acr/node
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ ACR SDK for Node.js: everything in @lib-acr/core, plus runtime heartbeats (CPU, memory, event loop, GC), capture of uncaught exceptions and unhandled rejections, and a flush on exit.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install @lib-acr/node
9
+ ```
10
+
11
+ ## Quick start
12
+
13
+ ```ts
14
+ import { ACR } from '@lib-acr/node';
15
+
16
+ const acr = new ACR({
17
+ apiKey: process.env.ACR_API_KEY!,
18
+ apiSecret: process.env.ACR_API_SECRET!,
19
+ endpoint: 'https://acr.example.com/v1',
20
+ environment: 'production',
21
+ appVersion: '1.0.0',
22
+ });
23
+
24
+ acr.startHeartbeat(); // runtime snapshot every 30 s (configurable)
25
+ acr.captureProcessErrors(); // uncaught exceptions flush, then exit(1) when no other handler exists
26
+ acr.flushOnExit(); // flush once the event loop empties
27
+ acr.trackEvent('order_created', { orderId: '123' });
28
+ ```
29
+
30
+ ## Notes
31
+
32
+ Heartbeats are sent through `POST /ingest/heartbeat`. Each carries `instanceId` (default `<hostname>:<pid>`) and the runtime snapshot as attributes. Heartbeats follow the enable state: requested with `startHeartbeat()` while the client is disabled, they start as soon as `setEnabled(true)` is called, and pause again on `setEnabled(false)`.
33
+
34
+ All core options apply, including `minLogLevel` (default `debug`) to drop low-level logs before they are buffered.
35
+
36
+ ## Remote control
37
+
38
+ The client polls the control snapshot with the server key (see `@lib-acr/core`); the timers are unref'd and
39
+ `shutdown()` stops them. The kill switch (`sdk.enabled: false`) pauses collection, flushing and heartbeats until it
40
+ is lifted. Read maintenance and other state with `acr.getControlState()`, or answer `503` automatically with
41
+ `maintenanceMiddleware` from `@lib-acr/express` or `ACRMaintenanceGuard` from `@lib-acr/nestjs`.
42
+
43
+ ## Feature flags
44
+
45
+ ```ts
46
+ const enabled = acr.flag('new-checkout', false, { userId: req.user?.id });
47
+ ```
48
+
49
+ Pass the request's user id: a server process has no current user. Without one, only flags rolled out to 100 % serve
50
+ their value; partial rollouts serve the default.
51
+
52
+ ## Wire protocol
53
+
54
+ Every SDK sends the same protocol: `POST {endpoint}/ingest/{events|logs|errors|heartbeat}` with the `X-ACR-Api-Key`
55
+ header (and `X-ACR-Secret-Key` for server keys). The credential determines the app and environment. The body is
56
+ `{ schemaVersion: 1, sdk, items[1..500] }`.
57
+ Each item has a client `id`, which lets the server drop duplicates when a batch is retried.
58
+
59
+ Retries use exponential backoff with jitter for network errors, 429 and 5xx responses. Other 4xx responses are not retried.
60
+ Telemetry is buffered in a bounded in-memory queue. When it is full, the oldest items are dropped and counted in `stats()`.
61
+ SDK code never throws into the host application.
62
+
63
+ Metrics (`increment`, `gauge`, `histogram`) and traces are not yet available.
@@ -0,0 +1,126 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ACR = void 0;
4
+ const node_os_1 = require("node:os");
5
+ const core_1 = require("@lib-acr/core");
6
+ const version_js_1 = require("./version.js");
7
+ const runtime_js_1 = require("./runtime.js");
8
+ /**
9
+ * Node.js client. Adds runtime heartbeats (CPU, memory, event loop, GC) and capture of unhandled errors on top of the
10
+ * shared core client.
11
+ */
12
+ class ACR extends core_1.AcrClient {
13
+ /** `sdk` lets framework wrappers (Express, NestJS) report their own package name and version. */
14
+ constructor(options, sdk = version_js_1.NODE_SDK) {
15
+ super(options, sdk);
16
+ this.runtime = new runtime_js_1.RuntimeMonitor();
17
+ this.heartbeatTimer = null;
18
+ /** startHeartbeat() was called and stopHeartbeat() was not: heartbeats run whenever the client is enabled. */
19
+ this.heartbeatRequested = false;
20
+ this.processHooksInstalled = false;
21
+ this.instanceId = options.instanceId ?? `${(0, node_os_1.hostname)()}:${process.pid}`;
22
+ this.heartbeatIntervalMs = options.heartbeatIntervalMs ?? 30000;
23
+ if (!Number.isFinite(this.heartbeatIntervalMs) || this.heartbeatIntervalMs < 1000) {
24
+ throw new Error('[acr] heartbeatIntervalMs must be at least 1000');
25
+ }
26
+ }
27
+ /**
28
+ * Sends one heartbeat with a runtime snapshot now, then one per interval. Safe to call twice. While the client is
29
+ * disabled no heartbeats are sent; they start as soon as it is enabled again.
30
+ */
31
+ startHeartbeat() {
32
+ this.heartbeatRequested = true;
33
+ this.resumeHeartbeat();
34
+ }
35
+ stopHeartbeat() {
36
+ this.heartbeatRequested = false;
37
+ this.pauseHeartbeat();
38
+ }
39
+ /**
40
+ * Starts or pauses heartbeats requested with startHeartbeat() whenever collection is switched on or off, locally
41
+ * (setEnabled) or by the remote kill switch.
42
+ */
43
+ onEnabledChange() {
44
+ if (!this.heartbeatRequested)
45
+ return;
46
+ if (this.isEnabled)
47
+ this.resumeHeartbeat();
48
+ else
49
+ this.pauseHeartbeat();
50
+ }
51
+ /** Current runtime snapshot, without sending anything. */
52
+ runtimeSnapshot() {
53
+ return this.runtime.snapshot();
54
+ }
55
+ /**
56
+ * Reports uncaught exceptions and unhandled promise rejections. Telemetry is flushed before an uncaught exception
57
+ * ends the process. When this SDK is the only handler, the process exits with code 1 as Node would have done.
58
+ */
59
+ captureProcessErrors(options = {}) {
60
+ if (this.processHooksInstalled)
61
+ return;
62
+ this.processHooksInstalled = true;
63
+ const exitOnUncaught = options.exitOnUncaughtException ?? true;
64
+ const timeoutMs = options.exitTimeoutMs ?? 2000;
65
+ const hadHandlers = process.listenerCount('uncaughtException') > 0;
66
+ process.on('uncaughtException', (error) => {
67
+ this.captureError(error, { handled: false, source: 'backend', attributes: { kind: 'uncaught_exception' } });
68
+ void this.flushWithin(timeoutMs).then(() => {
69
+ if (exitOnUncaught && !hadHandlers)
70
+ process.exit(1);
71
+ });
72
+ });
73
+ process.on('unhandledRejection', (reason) => {
74
+ this.captureError(reason, { handled: false, source: 'backend', attributes: { kind: 'unhandled_rejection' } });
75
+ });
76
+ }
77
+ /** Flushes once when the event loop empties, so short-lived scripts do not lose their last batch. */
78
+ flushOnExit(timeoutMs = 2000) {
79
+ process.once('beforeExit', () => {
80
+ void this.flushWithin(timeoutMs);
81
+ });
82
+ }
83
+ async shutdown(timeoutMs) {
84
+ this.stopHeartbeat();
85
+ await super.shutdown(timeoutMs);
86
+ }
87
+ /** Flush that gives up after `timeoutMs` without closing the client. */
88
+ flushWithin(timeoutMs) {
89
+ let timer;
90
+ return Promise.race([
91
+ this.flush().catch(() => undefined),
92
+ new Promise((resolve) => {
93
+ timer = setTimeout(resolve, timeoutMs);
94
+ timer.unref?.();
95
+ }),
96
+ ]).finally(() => {
97
+ if (timer)
98
+ clearTimeout(timer);
99
+ });
100
+ }
101
+ /** Convenience for errors caught in application code. */
102
+ captureException(error, options = {}) {
103
+ this.captureError(error, { source: 'backend', ...options });
104
+ }
105
+ resumeHeartbeat() {
106
+ if (this.heartbeatTimer || !this.isEnabled)
107
+ return;
108
+ this.runtime.start();
109
+ this.beat();
110
+ this.heartbeatTimer = setInterval(() => this.beat(), this.heartbeatIntervalMs);
111
+ this.heartbeatTimer.unref?.();
112
+ }
113
+ pauseHeartbeat() {
114
+ if (this.heartbeatTimer)
115
+ clearInterval(this.heartbeatTimer);
116
+ this.heartbeatTimer = null;
117
+ this.runtime.stop();
118
+ }
119
+ beat() {
120
+ const s = this.runtime.snapshot();
121
+ const { uptimeSeconds, ...attributes } = s;
122
+ this.heartbeat({ instanceId: this.instanceId, uptimeSeconds, attributes });
123
+ }
124
+ }
125
+ exports.ACR = ACR;
126
+ //# sourceMappingURL=acr.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"acr.js","sourceRoot":"","sources":["../../src/acr.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,25 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ exports.NODE_SDK = exports.RuntimeMonitor = exports.ACR = void 0;
18
+ var acr_js_1 = require("./acr.js");
19
+ Object.defineProperty(exports, "ACR", { enumerable: true, get: function () { return acr_js_1.ACR; } });
20
+ var runtime_js_1 = require("./runtime.js");
21
+ Object.defineProperty(exports, "RuntimeMonitor", { enumerable: true, get: function () { return runtime_js_1.RuntimeMonitor; } });
22
+ var version_js_1 = require("./version.js");
23
+ Object.defineProperty(exports, "NODE_SDK", { enumerable: true, get: function () { return version_js_1.NODE_SDK; } });
24
+ __exportStar(require("@lib-acr/core"), exports);
25
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;AAAA,mCAA+B;AAAtB,6FAAA,GAAG,OAAA;AAEZ,2CAA8C;AAArC,4GAAA,cAAc,OAAA;AAEvB,2CAAwC;AAA/B,sGAAA,QAAQ,OAAA;AACjB,gDAA8B"}
@@ -0,0 +1,72 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.RuntimeMonitor = void 0;
4
+ const node_perf_hooks_1 = require("node:perf_hooks");
5
+ /**
6
+ * Samples process health between calls to snapshot(). CPU, event loop utilization and GC counts cover the time since
7
+ * the previous snapshot, so each heartbeat reports its own interval.
8
+ */
9
+ class RuntimeMonitor {
10
+ constructor() {
11
+ this.delay = (0, node_perf_hooks_1.monitorEventLoopDelay)({ resolution: 20 });
12
+ this.observer = null;
13
+ this.gcCount = 0;
14
+ this.lastCpu = process.cpuUsage();
15
+ this.lastHr = process.hrtime.bigint();
16
+ this.lastElu = node_perf_hooks_1.performance.eventLoopUtilization();
17
+ this.startedAt = Date.now();
18
+ }
19
+ start() {
20
+ this.delay.enable();
21
+ if (this.observer)
22
+ return;
23
+ try {
24
+ this.observer = new node_perf_hooks_1.PerformanceObserver((list) => {
25
+ this.gcCount += list.getEntries().length;
26
+ });
27
+ this.observer.observe({ entryTypes: ['gc'] });
28
+ }
29
+ catch {
30
+ this.observer = null;
31
+ }
32
+ }
33
+ stop() {
34
+ this.delay.disable();
35
+ this.observer?.disconnect();
36
+ this.observer = null;
37
+ }
38
+ snapshot(now = Date.now()) {
39
+ const mem = process.memoryUsage();
40
+ const cpu = process.cpuUsage(this.lastCpu);
41
+ const hr = process.hrtime.bigint();
42
+ const elapsedMicros = Number(hr - this.lastHr) / 1000;
43
+ const elu = node_perf_hooks_1.performance.eventLoopUtilization(this.lastElu);
44
+ const cpuPercent = elapsedMicros > 0 ? ((cpu.user + cpu.system) / elapsedMicros) * 100 : 0;
45
+ const p99 = this.delay.count > 0 ? this.delay.percentile(99) / 1e6 : 0;
46
+ this.delay.reset();
47
+ const gc = this.gcCount;
48
+ this.gcCount = 0;
49
+ this.lastCpu = process.cpuUsage();
50
+ this.lastHr = hr;
51
+ this.lastElu = node_perf_hooks_1.performance.eventLoopUtilization();
52
+ const handles = process.getActiveResourcesInfo?.().length ?? 0;
53
+ return {
54
+ uptimeSeconds: Math.max(0, Math.floor((now - this.startedAt) / 1000)),
55
+ rssBytes: mem.rss,
56
+ heapUsedBytes: mem.heapUsed,
57
+ heapTotalBytes: mem.heapTotal,
58
+ externalBytes: mem.external,
59
+ cpuPercent: round(Math.min(cpuPercent, 100 * 64)),
60
+ eventLoopUtilization: round(elu.utilization, 4),
61
+ eventLoopDelayP99Ms: round(Number.isFinite(p99) ? p99 : 0),
62
+ gcCount: gc,
63
+ activeHandles: handles,
64
+ };
65
+ }
66
+ }
67
+ exports.RuntimeMonitor = RuntimeMonitor;
68
+ function round(value, digits = 2) {
69
+ const f = 10 ** digits;
70
+ return Math.round(value * f) / f;
71
+ }
72
+ //# sourceMappingURL=runtime.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runtime.js","sourceRoot":"","sources":["../../src/runtime.ts"],"names":[],"mappings":";;;AAAA,qDAAkH;AAelH;;;GAGG;AACH,MAAa,cAAc;IAA3B;QACmB,UAAK,GAAsB,IAAA,uCAAqB,EAAC,EAAE,UAAU,EAAE,EAAE,EAAE,CAAC,CAAC;QAC9E,aAAQ,GAA+B,IAAI,CAAC;QAC5C,YAAO,GAAG,CAAC,CAAC;QACZ,YAAO,GAAG,OAAO,CAAC,QAAQ,EAAE,CAAC;QAC7B,WAAM,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC;QACjC,YAAO,GAAG,6BAAW,CAAC,oBAAoB,EAAE,CAAC;QACpC,cAAS,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;IAmD1C,CAAC;IAjDC,KAAK;QACH,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC;QACpB,IAAI,IAAI,CAAC,QAAQ;YAAE,OAAO;QAC1B,IAAI,CAAC;YACH,IAAI,CAAC,QAAQ,GAAG,IAAI,qCAAmB,CAAC,CAAC,IAAI,EAAE,EAAE;gBAC/C,IAAI,CAAC,OAAO,IAAI,IAAI,CAAC,UAAU,EAAE,CAAC,MAAM,CAAC;YAC3C,CAAC,CAAC,CAAC;YACH,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,EAAE,UAAU,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QAChD,CAAC;QAAC,MAAM,CAAC;YACP,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC;QACvB,CAAC;IACH,CAAC;IAED,IAAI;QACF,IAAI,CAAC,KAAK,CAAC,OAAO,EAAE,CAAC;QACrB,IAAI,CAAC,QAAQ,EAAE,UAAU,EAAE,CAAC;QAC5B,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC;IACvB,CAAC;IAED,QAAQ,CAAC,MAAc,IAAI,CAAC,GAAG,EAAE;QAC/B,MAAM,GAAG,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC;QAClC,MAAM,GAAG,GAAG,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QAC3C,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC;QACnC,MAAM,aAAa,GAAG,MAAM,CAAC,EAAE,GAAG,IAAI,CAAC,MAAM,CAAC,GAAG,IAAI,CAAC;QACtD,MAAM,GAAG,GAAG,6BAAW,CAAC,oBAAoB,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QAC3D,MAAM,UAAU,GAAG,aAAa,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,GAAG,GAAG,CAAC,MAAM,CAAC,GAAG,aAAa,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;QAE3F,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,EAAE,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;QACvE,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC;QACnB,MAAM,EAAE,GAAG,IAAI,CAAC,OAAO,CAAC;QACxB,IAAI,CAAC,OAAO,GAAG,CAAC,CAAC;QACjB,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC,QAAQ,EAAE,CAAC;QAClC,IAAI,CAAC,MAAM,GAAG,EAAE,CAAC;QACjB,IAAI,CAAC,OAAO,GAAG,6BAAW,CAAC,oBAAoB,EAAE,CAAC;QAElD,MAAM,OAAO,GAAI,OAAuD,CAAC,sBAAsB,EAAE,EAAE,CAAC,MAAM,IAAI,CAAC,CAAC;QAChH,OAAO;YACL,aAAa,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,GAAG,GAAG,IAAI,CAAC,SAAS,CAAC,GAAG,IAAI,CAAC,CAAC;YACrE,QAAQ,EAAE,GAAG,CAAC,GAAG;YACjB,aAAa,EAAE,GAAG,CAAC,QAAQ;YAC3B,cAAc,EAAE,GAAG,CAAC,SAAS;YAC7B,aAAa,EAAE,GAAG,CAAC,QAAQ;YAC3B,UAAU,EAAE,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,UAAU,EAAE,GAAG,GAAG,EAAE,CAAC,CAAC;YACjD,oBAAoB,EAAE,KAAK,CAAC,GAAG,CAAC,WAAW,EAAE,CAAC,CAAC;YAC/C,mBAAmB,EAAE,KAAK,CAAC,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;YAC1D,OAAO,EAAE,EAAE;YACX,aAAa,EAAE,OAAO;SACvB,CAAC;IACJ,CAAC;CACF;AA1DD,wCA0DC;AAED,SAAS,KAAK,CAAC,KAAa,EAAE,MAAM,GAAG,CAAC;IACtC,MAAM,CAAC,GAAG,EAAE,IAAI,MAAM,CAAC;IACvB,OAAO,IAAI,CAAC,KAAK,CAAC,KAAK,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC;AACnC,CAAC"}
@@ -0,0 +1,6 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SDK_INFO = void 0;
4
+ // Generated by scripts/sync-sdk-info.mjs from package.json. Do not edit by hand.
5
+ exports.SDK_INFO = { name: '@lib-acr/node', version: '0.0.1' };
6
+ //# sourceMappingURL=sdk-info.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sdk-info.js","sourceRoot":"","sources":["../../src/sdk-info.ts"],"names":[],"mappings":";;;AAAA,iFAAiF;AACpE,QAAA,QAAQ,GAAG,EAAE,IAAI,EAAE,eAAe,EAAE,OAAO,EAAE,OAAO,EAAW,CAAC"}
@@ -0,0 +1,6 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.NODE_SDK = void 0;
4
+ var sdk_info_js_1 = require("./sdk-info.js");
5
+ Object.defineProperty(exports, "NODE_SDK", { enumerable: true, get: function () { return sdk_info_js_1.SDK_INFO; } });
6
+ //# sourceMappingURL=version.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"version.js","sourceRoot":"","sources":["../../src/version.ts"],"names":[],"mappings":";;;AAAA,6CAAqD;AAA5C,uGAAA,QAAQ,OAAY"}
@@ -0,0 +1,122 @@
1
+ import { hostname } from 'node:os';
2
+ import { AcrClient } from '@lib-acr/core';
3
+ import { NODE_SDK } from './version.js';
4
+ import { RuntimeMonitor } from './runtime.js';
5
+ /**
6
+ * Node.js client. Adds runtime heartbeats (CPU, memory, event loop, GC) and capture of unhandled errors on top of the
7
+ * shared core client.
8
+ */
9
+ export class ACR extends AcrClient {
10
+ /** `sdk` lets framework wrappers (Express, NestJS) report their own package name and version. */
11
+ constructor(options, sdk = NODE_SDK) {
12
+ super(options, sdk);
13
+ this.runtime = new RuntimeMonitor();
14
+ this.heartbeatTimer = null;
15
+ /** startHeartbeat() was called and stopHeartbeat() was not: heartbeats run whenever the client is enabled. */
16
+ this.heartbeatRequested = false;
17
+ this.processHooksInstalled = false;
18
+ this.instanceId = options.instanceId ?? `${hostname()}:${process.pid}`;
19
+ this.heartbeatIntervalMs = options.heartbeatIntervalMs ?? 30000;
20
+ if (!Number.isFinite(this.heartbeatIntervalMs) || this.heartbeatIntervalMs < 1000) {
21
+ throw new Error('[acr] heartbeatIntervalMs must be at least 1000');
22
+ }
23
+ }
24
+ /**
25
+ * Sends one heartbeat with a runtime snapshot now, then one per interval. Safe to call twice. While the client is
26
+ * disabled no heartbeats are sent; they start as soon as it is enabled again.
27
+ */
28
+ startHeartbeat() {
29
+ this.heartbeatRequested = true;
30
+ this.resumeHeartbeat();
31
+ }
32
+ stopHeartbeat() {
33
+ this.heartbeatRequested = false;
34
+ this.pauseHeartbeat();
35
+ }
36
+ /**
37
+ * Starts or pauses heartbeats requested with startHeartbeat() whenever collection is switched on or off, locally
38
+ * (setEnabled) or by the remote kill switch.
39
+ */
40
+ onEnabledChange() {
41
+ if (!this.heartbeatRequested)
42
+ return;
43
+ if (this.isEnabled)
44
+ this.resumeHeartbeat();
45
+ else
46
+ this.pauseHeartbeat();
47
+ }
48
+ /** Current runtime snapshot, without sending anything. */
49
+ runtimeSnapshot() {
50
+ return this.runtime.snapshot();
51
+ }
52
+ /**
53
+ * Reports uncaught exceptions and unhandled promise rejections. Telemetry is flushed before an uncaught exception
54
+ * ends the process. When this SDK is the only handler, the process exits with code 1 as Node would have done.
55
+ */
56
+ captureProcessErrors(options = {}) {
57
+ if (this.processHooksInstalled)
58
+ return;
59
+ this.processHooksInstalled = true;
60
+ const exitOnUncaught = options.exitOnUncaughtException ?? true;
61
+ const timeoutMs = options.exitTimeoutMs ?? 2000;
62
+ const hadHandlers = process.listenerCount('uncaughtException') > 0;
63
+ process.on('uncaughtException', (error) => {
64
+ this.captureError(error, { handled: false, source: 'backend', attributes: { kind: 'uncaught_exception' } });
65
+ void this.flushWithin(timeoutMs).then(() => {
66
+ if (exitOnUncaught && !hadHandlers)
67
+ process.exit(1);
68
+ });
69
+ });
70
+ process.on('unhandledRejection', (reason) => {
71
+ this.captureError(reason, { handled: false, source: 'backend', attributes: { kind: 'unhandled_rejection' } });
72
+ });
73
+ }
74
+ /** Flushes once when the event loop empties, so short-lived scripts do not lose their last batch. */
75
+ flushOnExit(timeoutMs = 2000) {
76
+ process.once('beforeExit', () => {
77
+ void this.flushWithin(timeoutMs);
78
+ });
79
+ }
80
+ async shutdown(timeoutMs) {
81
+ this.stopHeartbeat();
82
+ await super.shutdown(timeoutMs);
83
+ }
84
+ /** Flush that gives up after `timeoutMs` without closing the client. */
85
+ flushWithin(timeoutMs) {
86
+ let timer;
87
+ return Promise.race([
88
+ this.flush().catch(() => undefined),
89
+ new Promise((resolve) => {
90
+ timer = setTimeout(resolve, timeoutMs);
91
+ timer.unref?.();
92
+ }),
93
+ ]).finally(() => {
94
+ if (timer)
95
+ clearTimeout(timer);
96
+ });
97
+ }
98
+ /** Convenience for errors caught in application code. */
99
+ captureException(error, options = {}) {
100
+ this.captureError(error, { source: 'backend', ...options });
101
+ }
102
+ resumeHeartbeat() {
103
+ if (this.heartbeatTimer || !this.isEnabled)
104
+ return;
105
+ this.runtime.start();
106
+ this.beat();
107
+ this.heartbeatTimer = setInterval(() => this.beat(), this.heartbeatIntervalMs);
108
+ this.heartbeatTimer.unref?.();
109
+ }
110
+ pauseHeartbeat() {
111
+ if (this.heartbeatTimer)
112
+ clearInterval(this.heartbeatTimer);
113
+ this.heartbeatTimer = null;
114
+ this.runtime.stop();
115
+ }
116
+ beat() {
117
+ const s = this.runtime.snapshot();
118
+ const { uptimeSeconds, ...attributes } = s;
119
+ this.heartbeat({ instanceId: this.instanceId, uptimeSeconds, attributes });
120
+ }
121
+ }
122
+ //# sourceMappingURL=acr.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"acr.js","sourceRoot":"","sources":["../../src/acr.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,5 @@
1
+ export { ACR } from './acr.js';
2
+ export { RuntimeMonitor } from './runtime.js';
3
+ export { NODE_SDK } from './version.js';
4
+ export * from '@lib-acr/core';
5
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,MAAM,UAAU,CAAC;AAE/B,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAE9C,OAAO,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AACxC,cAAc,eAAe,CAAC"}
@@ -0,0 +1,3 @@
1
+ {
2
+ "type": "module"
3
+ }
@@ -0,0 +1,68 @@
1
+ import { monitorEventLoopDelay, performance, PerformanceObserver } from 'node:perf_hooks';
2
+ /**
3
+ * Samples process health between calls to snapshot(). CPU, event loop utilization and GC counts cover the time since
4
+ * the previous snapshot, so each heartbeat reports its own interval.
5
+ */
6
+ export class RuntimeMonitor {
7
+ constructor() {
8
+ this.delay = monitorEventLoopDelay({ resolution: 20 });
9
+ this.observer = null;
10
+ this.gcCount = 0;
11
+ this.lastCpu = process.cpuUsage();
12
+ this.lastHr = process.hrtime.bigint();
13
+ this.lastElu = performance.eventLoopUtilization();
14
+ this.startedAt = Date.now();
15
+ }
16
+ start() {
17
+ this.delay.enable();
18
+ if (this.observer)
19
+ return;
20
+ try {
21
+ this.observer = new PerformanceObserver((list) => {
22
+ this.gcCount += list.getEntries().length;
23
+ });
24
+ this.observer.observe({ entryTypes: ['gc'] });
25
+ }
26
+ catch {
27
+ this.observer = null;
28
+ }
29
+ }
30
+ stop() {
31
+ this.delay.disable();
32
+ this.observer?.disconnect();
33
+ this.observer = null;
34
+ }
35
+ snapshot(now = Date.now()) {
36
+ const mem = process.memoryUsage();
37
+ const cpu = process.cpuUsage(this.lastCpu);
38
+ const hr = process.hrtime.bigint();
39
+ const elapsedMicros = Number(hr - this.lastHr) / 1000;
40
+ const elu = performance.eventLoopUtilization(this.lastElu);
41
+ const cpuPercent = elapsedMicros > 0 ? ((cpu.user + cpu.system) / elapsedMicros) * 100 : 0;
42
+ const p99 = this.delay.count > 0 ? this.delay.percentile(99) / 1e6 : 0;
43
+ this.delay.reset();
44
+ const gc = this.gcCount;
45
+ this.gcCount = 0;
46
+ this.lastCpu = process.cpuUsage();
47
+ this.lastHr = hr;
48
+ this.lastElu = performance.eventLoopUtilization();
49
+ const handles = process.getActiveResourcesInfo?.().length ?? 0;
50
+ return {
51
+ uptimeSeconds: Math.max(0, Math.floor((now - this.startedAt) / 1000)),
52
+ rssBytes: mem.rss,
53
+ heapUsedBytes: mem.heapUsed,
54
+ heapTotalBytes: mem.heapTotal,
55
+ externalBytes: mem.external,
56
+ cpuPercent: round(Math.min(cpuPercent, 100 * 64)),
57
+ eventLoopUtilization: round(elu.utilization, 4),
58
+ eventLoopDelayP99Ms: round(Number.isFinite(p99) ? p99 : 0),
59
+ gcCount: gc,
60
+ activeHandles: handles,
61
+ };
62
+ }
63
+ }
64
+ function round(value, digits = 2) {
65
+ const f = 10 ** digits;
66
+ return Math.round(value * f) / f;
67
+ }
68
+ //# sourceMappingURL=runtime.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runtime.js","sourceRoot":"","sources":["../../src/runtime.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,qBAAqB,EAAE,WAAW,EAAE,mBAAmB,EAA0B,MAAM,iBAAiB,CAAC;AAelH;;;GAGG;AACH,MAAM,OAAO,cAAc;IAA3B;QACmB,UAAK,GAAsB,qBAAqB,CAAC,EAAE,UAAU,EAAE,EAAE,EAAE,CAAC,CAAC;QAC9E,aAAQ,GAA+B,IAAI,CAAC;QAC5C,YAAO,GAAG,CAAC,CAAC;QACZ,YAAO,GAAG,OAAO,CAAC,QAAQ,EAAE,CAAC;QAC7B,WAAM,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC;QACjC,YAAO,GAAG,WAAW,CAAC,oBAAoB,EAAE,CAAC;QACpC,cAAS,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;IAmD1C,CAAC;IAjDC,KAAK;QACH,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC;QACpB,IAAI,IAAI,CAAC,QAAQ;YAAE,OAAO;QAC1B,IAAI,CAAC;YACH,IAAI,CAAC,QAAQ,GAAG,IAAI,mBAAmB,CAAC,CAAC,IAAI,EAAE,EAAE;gBAC/C,IAAI,CAAC,OAAO,IAAI,IAAI,CAAC,UAAU,EAAE,CAAC,MAAM,CAAC;YAC3C,CAAC,CAAC,CAAC;YACH,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,EAAE,UAAU,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QAChD,CAAC;QAAC,MAAM,CAAC;YACP,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC;QACvB,CAAC;IACH,CAAC;IAED,IAAI;QACF,IAAI,CAAC,KAAK,CAAC,OAAO,EAAE,CAAC;QACrB,IAAI,CAAC,QAAQ,EAAE,UAAU,EAAE,CAAC;QAC5B,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC;IACvB,CAAC;IAED,QAAQ,CAAC,MAAc,IAAI,CAAC,GAAG,EAAE;QAC/B,MAAM,GAAG,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC;QAClC,MAAM,GAAG,GAAG,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QAC3C,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC;QACnC,MAAM,aAAa,GAAG,MAAM,CAAC,EAAE,GAAG,IAAI,CAAC,MAAM,CAAC,GAAG,IAAI,CAAC;QACtD,MAAM,GAAG,GAAG,WAAW,CAAC,oBAAoB,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QAC3D,MAAM,UAAU,GAAG,aAAa,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,GAAG,GAAG,CAAC,MAAM,CAAC,GAAG,aAAa,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;QAE3F,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,EAAE,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;QACvE,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC;QACnB,MAAM,EAAE,GAAG,IAAI,CAAC,OAAO,CAAC;QACxB,IAAI,CAAC,OAAO,GAAG,CAAC,CAAC;QACjB,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC,QAAQ,EAAE,CAAC;QAClC,IAAI,CAAC,MAAM,GAAG,EAAE,CAAC;QACjB,IAAI,CAAC,OAAO,GAAG,WAAW,CAAC,oBAAoB,EAAE,CAAC;QAElD,MAAM,OAAO,GAAI,OAAuD,CAAC,sBAAsB,EAAE,EAAE,CAAC,MAAM,IAAI,CAAC,CAAC;QAChH,OAAO;YACL,aAAa,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,GAAG,GAAG,IAAI,CAAC,SAAS,CAAC,GAAG,IAAI,CAAC,CAAC;YACrE,QAAQ,EAAE,GAAG,CAAC,GAAG;YACjB,aAAa,EAAE,GAAG,CAAC,QAAQ;YAC3B,cAAc,EAAE,GAAG,CAAC,SAAS;YAC7B,aAAa,EAAE,GAAG,CAAC,QAAQ;YAC3B,UAAU,EAAE,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,UAAU,EAAE,GAAG,GAAG,EAAE,CAAC,CAAC;YACjD,oBAAoB,EAAE,KAAK,CAAC,GAAG,CAAC,WAAW,EAAE,CAAC,CAAC;YAC/C,mBAAmB,EAAE,KAAK,CAAC,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;YAC1D,OAAO,EAAE,EAAE;YACX,aAAa,EAAE,OAAO;SACvB,CAAC;IACJ,CAAC;CACF;AAED,SAAS,KAAK,CAAC,KAAa,EAAE,MAAM,GAAG,CAAC;IACtC,MAAM,CAAC,GAAG,EAAE,IAAI,MAAM,CAAC;IACvB,OAAO,IAAI,CAAC,KAAK,CAAC,KAAK,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC;AACnC,CAAC"}
@@ -0,0 +1,3 @@
1
+ // Generated by scripts/sync-sdk-info.mjs from package.json. Do not edit by hand.
2
+ export const SDK_INFO = { name: '@lib-acr/node', version: '0.0.1' };
3
+ //# sourceMappingURL=sdk-info.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sdk-info.js","sourceRoot":"","sources":["../../src/sdk-info.ts"],"names":[],"mappings":"AAAA,iFAAiF;AACjF,MAAM,CAAC,MAAM,QAAQ,GAAG,EAAE,IAAI,EAAE,eAAe,EAAE,OAAO,EAAE,OAAO,EAAW,CAAC"}
@@ -0,0 +1,2 @@
1
+ export { SDK_INFO as NODE_SDK } from './sdk-info.js';
2
+ //# sourceMappingURL=version.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"version.js","sourceRoot":"","sources":["../../src/version.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,IAAI,QAAQ,EAAE,MAAM,eAAe,CAAC"}
@@ -0,0 +1,57 @@
1
+ import { AcrClient, type AcrOptions, type ErrorOptions, type SdkInfo } from '@lib-acr/core';
2
+ import { type RuntimeSnapshot } from './runtime.js';
3
+ export interface NodeAcrOptions extends AcrOptions {
4
+ /** Identifies this process in heartbeats. Default `<hostname>:<pid>`. */
5
+ instanceId?: string;
6
+ /** Milliseconds between runtime heartbeats. Default 30 000. */
7
+ heartbeatIntervalMs?: number;
8
+ }
9
+ export interface CaptureProcessErrorsOptions {
10
+ /** Exit with code 1 after an uncaught exception, once the telemetry is flushed. Default true. */
11
+ exitOnUncaughtException?: boolean;
12
+ /** Longest wait for the flush before exiting. Default 2 000 ms. */
13
+ exitTimeoutMs?: number;
14
+ }
15
+ /**
16
+ * Node.js client. Adds runtime heartbeats (CPU, memory, event loop, GC) and capture of unhandled errors on top of the
17
+ * shared core client.
18
+ */
19
+ export declare class ACR extends AcrClient {
20
+ readonly instanceId: string;
21
+ private readonly runtime;
22
+ private heartbeatTimer;
23
+ /** startHeartbeat() was called and stopHeartbeat() was not: heartbeats run whenever the client is enabled. */
24
+ private heartbeatRequested;
25
+ private processHooksInstalled;
26
+ private readonly heartbeatIntervalMs;
27
+ /** `sdk` lets framework wrappers (Express, NestJS) report their own package name and version. */
28
+ constructor(options: NodeAcrOptions, sdk?: SdkInfo);
29
+ /**
30
+ * Sends one heartbeat with a runtime snapshot now, then one per interval. Safe to call twice. While the client is
31
+ * disabled no heartbeats are sent; they start as soon as it is enabled again.
32
+ */
33
+ startHeartbeat(): void;
34
+ stopHeartbeat(): void;
35
+ /**
36
+ * Starts or pauses heartbeats requested with startHeartbeat() whenever collection is switched on or off, locally
37
+ * (setEnabled) or by the remote kill switch.
38
+ */
39
+ protected onEnabledChange(): void;
40
+ /** Current runtime snapshot, without sending anything. */
41
+ runtimeSnapshot(): RuntimeSnapshot;
42
+ /**
43
+ * Reports uncaught exceptions and unhandled promise rejections. Telemetry is flushed before an uncaught exception
44
+ * ends the process. When this SDK is the only handler, the process exits with code 1 as Node would have done.
45
+ */
46
+ captureProcessErrors(options?: CaptureProcessErrorsOptions): void;
47
+ /** Flushes once when the event loop empties, so short-lived scripts do not lose their last batch. */
48
+ flushOnExit(timeoutMs?: number): void;
49
+ shutdown(timeoutMs?: number): Promise<void>;
50
+ /** Flush that gives up after `timeoutMs` without closing the client. */
51
+ flushWithin(timeoutMs: number): Promise<void>;
52
+ /** Convenience for errors caught in application code. */
53
+ captureException(error: unknown, options?: ErrorOptions): void;
54
+ private resumeHeartbeat;
55
+ private pauseHeartbeat;
56
+ private beat;
57
+ }
@@ -0,0 +1,6 @@
1
+ export { ACR } from './acr.js';
2
+ export type { CaptureProcessErrorsOptions, NodeAcrOptions } from './acr.js';
3
+ export { RuntimeMonitor } from './runtime.js';
4
+ export type { RuntimeSnapshot } from './runtime.js';
5
+ export { NODE_SDK } from './version.js';
6
+ export * from '@lib-acr/core';
@@ -0,0 +1,28 @@
1
+ export interface RuntimeSnapshot {
2
+ uptimeSeconds: number;
3
+ rssBytes: number;
4
+ heapUsedBytes: number;
5
+ heapTotalBytes: number;
6
+ externalBytes: number;
7
+ cpuPercent: number;
8
+ eventLoopUtilization: number;
9
+ eventLoopDelayP99Ms: number;
10
+ gcCount: number;
11
+ activeHandles: number;
12
+ }
13
+ /**
14
+ * Samples process health between calls to snapshot(). CPU, event loop utilization and GC counts cover the time since
15
+ * the previous snapshot, so each heartbeat reports its own interval.
16
+ */
17
+ export declare class RuntimeMonitor {
18
+ private readonly delay;
19
+ private observer;
20
+ private gcCount;
21
+ private lastCpu;
22
+ private lastHr;
23
+ private lastElu;
24
+ private readonly startedAt;
25
+ start(): void;
26
+ stop(): void;
27
+ snapshot(now?: number): RuntimeSnapshot;
28
+ }
@@ -0,0 +1,4 @@
1
+ export declare const SDK_INFO: {
2
+ readonly name: "@lib-acr/node";
3
+ readonly version: "0.0.1";
4
+ };
@@ -0,0 +1 @@
1
+ export { SDK_INFO as NODE_SDK } from './sdk-info.js';
package/package.json CHANGED
@@ -1,6 +1,42 @@
1
1
  {
2
2
  "name": "@lib-acr/node",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.0.1",
4
+ "description": "ACR SDK for Node.js: runtime heartbeats, runtime metrics, unhandled error capture and custom events and logs.",
5
+ "license": "MIT",
6
+ "main": "./dist/cjs/index.js",
7
+ "module": "./dist/esm/index.js",
8
+ "types": "./dist/types/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/types/index.d.ts",
12
+ "import": "./dist/esm/index.js",
13
+ "require": "./dist/cjs/index.js"
14
+ }
15
+ },
16
+ "files": [
17
+ "dist",
18
+ "README.md",
19
+ "LICENSE"
20
+ ],
21
+ "sideEffects": false,
22
+ "engines": {
23
+ "node": ">=18.17"
24
+ },
25
+ "dependencies": {
26
+ "@lib-acr/core": "0.0.1"
27
+ },
28
+ "scripts": {
29
+ "build": "node ../../scripts/build-package.mjs",
30
+ "typecheck": "tsc -p tsconfig.json --noEmit",
31
+ "test": "vitest run"
32
+ },
33
+ "repository": {
34
+ "type": "git",
35
+ "url": "git+https://github.com/application-control-room/application.git",
36
+ "directory": "packages/node"
37
+ },
38
+ "homepage": "https://github.com/application-control-room/application/tree/main/packages/node#readme",
39
+ "publishConfig": {
40
+ "access": "public"
41
+ }
42
+ }