@lib-acr/core 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.
Files changed (104) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +97 -2
  3. package/dist/cjs/backoff.js +9 -0
  4. package/dist/cjs/backoff.js.map +1 -0
  5. package/dist/cjs/buffer.js +50 -0
  6. package/dist/cjs/buffer.js.map +1 -0
  7. package/dist/cjs/client.js +594 -0
  8. package/dist/cjs/client.js.map +1 -0
  9. package/dist/cjs/config.js +61 -0
  10. package/dist/cjs/config.js.map +1 -0
  11. package/dist/cjs/control/bucketing.js +66 -0
  12. package/dist/cjs/control/bucketing.js.map +1 -0
  13. package/dist/cjs/control/commands.js +45 -0
  14. package/dist/cjs/control/commands.js.map +1 -0
  15. package/dist/cjs/control/control-client.js +170 -0
  16. package/dist/cjs/control/control-client.js.map +1 -0
  17. package/dist/cjs/control/flags.js +44 -0
  18. package/dist/cjs/control/flags.js.map +1 -0
  19. package/dist/cjs/control/semver.js +63 -0
  20. package/dist/cjs/control/semver.js.map +1 -0
  21. package/dist/cjs/control/snapshot.js +177 -0
  22. package/dist/cjs/control/snapshot.js.map +1 -0
  23. package/dist/cjs/control/state.js +121 -0
  24. package/dist/cjs/control/state.js.map +1 -0
  25. package/dist/cjs/control/types.js +3 -0
  26. package/dist/cjs/control/types.js.map +1 -0
  27. package/dist/cjs/errors.js +28 -0
  28. package/dist/cjs/errors.js.map +1 -0
  29. package/dist/cjs/ids.js +65 -0
  30. package/dist/cjs/ids.js.map +1 -0
  31. package/dist/cjs/index.js +70 -0
  32. package/dist/cjs/index.js.map +1 -0
  33. package/dist/cjs/redact.js +74 -0
  34. package/dist/cjs/redact.js.map +1 -0
  35. package/dist/cjs/sdk-info.js +6 -0
  36. package/dist/cjs/sdk-info.js.map +1 -0
  37. package/dist/cjs/transport.js +111 -0
  38. package/dist/cjs/transport.js.map +1 -0
  39. package/dist/cjs/types.js +14 -0
  40. package/dist/cjs/types.js.map +1 -0
  41. package/dist/cjs/version.js +8 -0
  42. package/dist/cjs/version.js.map +1 -0
  43. package/dist/esm/backoff.js +6 -0
  44. package/dist/esm/backoff.js.map +1 -0
  45. package/dist/esm/buffer.js +46 -0
  46. package/dist/esm/buffer.js.map +1 -0
  47. package/dist/esm/client.js +588 -0
  48. package/dist/esm/client.js.map +1 -0
  49. package/dist/esm/config.js +58 -0
  50. package/dist/esm/config.js.map +1 -0
  51. package/dist/esm/control/bucketing.js +60 -0
  52. package/dist/esm/control/bucketing.js.map +1 -0
  53. package/dist/esm/control/commands.js +41 -0
  54. package/dist/esm/control/commands.js.map +1 -0
  55. package/dist/esm/control/control-client.js +166 -0
  56. package/dist/esm/control/control-client.js.map +1 -0
  57. package/dist/esm/control/flags.js +40 -0
  58. package/dist/esm/control/flags.js.map +1 -0
  59. package/dist/esm/control/semver.js +58 -0
  60. package/dist/esm/control/semver.js.map +1 -0
  61. package/dist/esm/control/snapshot.js +172 -0
  62. package/dist/esm/control/snapshot.js.map +1 -0
  63. package/dist/esm/control/state.js +112 -0
  64. package/dist/esm/control/state.js.map +1 -0
  65. package/dist/esm/control/types.js +2 -0
  66. package/dist/esm/control/types.js.map +1 -0
  67. package/dist/esm/errors.js +25 -0
  68. package/dist/esm/errors.js.map +1 -0
  69. package/dist/esm/ids.js +58 -0
  70. package/dist/esm/ids.js.map +1 -0
  71. package/dist/esm/index.js +18 -0
  72. package/dist/esm/index.js.map +1 -0
  73. package/dist/esm/package.json +3 -0
  74. package/dist/esm/redact.js +68 -0
  75. package/dist/esm/redact.js.map +1 -0
  76. package/dist/esm/sdk-info.js +3 -0
  77. package/dist/esm/sdk-info.js.map +1 -0
  78. package/dist/esm/transport.js +105 -0
  79. package/dist/esm/transport.js.map +1 -0
  80. package/dist/esm/types.js +10 -0
  81. package/dist/esm/types.js.map +1 -0
  82. package/dist/esm/version.js +4 -0
  83. package/dist/esm/version.js.map +1 -0
  84. package/dist/types/backoff.d.ts +2 -0
  85. package/dist/types/buffer.d.ts +21 -0
  86. package/dist/types/client.d.ts +187 -0
  87. package/dist/types/config.d.ts +27 -0
  88. package/dist/types/control/bucketing.d.ts +11 -0
  89. package/dist/types/control/commands.d.ts +37 -0
  90. package/dist/types/control/control-client.d.ts +69 -0
  91. package/dist/types/control/flags.d.ts +19 -0
  92. package/dist/types/control/semver.d.ts +15 -0
  93. package/dist/types/control/snapshot.d.ts +12 -0
  94. package/dist/types/control/state.d.ts +39 -0
  95. package/dist/types/control/types.d.ts +126 -0
  96. package/dist/types/errors.d.ts +7 -0
  97. package/dist/types/ids.d.ts +14 -0
  98. package/dist/types/index.d.ts +28 -0
  99. package/dist/types/redact.d.ts +11 -0
  100. package/dist/types/sdk-info.d.ts +4 -0
  101. package/dist/types/transport.d.ts +35 -0
  102. package/dist/types/types.d.ts +87 -0
  103. package/dist/types/version.d.ts +3 -0
  104. package/package.json +37 -4
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,98 @@
1
- # Temporary Holding Version
1
+ # @lib-acr/core
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
+ Shared telemetry client used by every ACR SDK: batching, retry with exponential backoff and jitter, a bounded buffer, sampling, offline buffering, redaction and the ingestion protocol.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install @lib-acr/core
9
+ ```
10
+
11
+ ## Quick start
12
+
13
+ ```ts
14
+ import { AcrClient } from '@lib-acr/core';
15
+
16
+ const acr = new AcrClient({
17
+ apiKey: process.env.ACR_API_KEY!, // acr_live_… or acr_test_…
18
+ apiSecret: process.env.ACR_API_SECRET, // server only
19
+ endpoint: 'https://acr.example.com/v1',
20
+ });
21
+
22
+ acr.trackEvent('checkout_started', { cartSize: 3 }, { userId: 'user_42' });
23
+ acr.log('info', 'order created', { orderId: 'o-1' });
24
+ acr.captureError(new Error('payment declined'), { handled: true });
25
+ await acr.flush(); // or await acr.shutdown(2000) before the process exits
26
+ ```
27
+
28
+ ## Notes
29
+
30
+ Options: `flushIntervalMs`, `maxBatchSize` (1–500), `maxBufferSize`, `maxRetries`, `retryBaseMs`, `requestTimeoutMs`, `sampleRate` (events and logs only), `minLogLevel` (`debug` by default; lower logs are dropped before buffering), `enabled`, `isOnline`, `onError`, `remoteControl` (default true), `configPollIntervalMs` (default 60 000). Invalid options throw at construction.
31
+
32
+ - **Sampling** is per session: items with a session id are kept or skipped as a whole session
33
+ (`fnv1a32('sample:' + sessionId) % 10000 < sampleRate * 10000`); items without one are sampled individually.
34
+ Errors and heartbeats are never sampled.
35
+ - **Enable state:** `setEnabled(false)` stops collection and the background flush timer; `setEnabled(true)` starts
36
+ the timer again.
37
+ - **Offline:** when the device goes offline during a flush, every undelivered item of the batch goes back into the
38
+ buffer in order. `stats()` reports `queued`, `sent`, `rejected`, `dropped`, `bufferOverflow`, `requeued`,
39
+ `sampledOut` and `belowMinLevel`.
40
+ - **Rate limits:** a 429 waits for the `Retry-After` header, or `retryAfterSeconds` from the JSON error body.
41
+ - **Free-text redaction:** log messages, error messages and stack traces have e-mail addresses, `Bearer`/`Basic`
42
+ credentials, JWTs, `acr_secret_…` values and `token=`/`password=` pairs masked before buffering (`redactString`).
43
+ - `sendAllNow()` starts one request per kind at once, without retries (used on page hide); `clearQueue()` discards
44
+ the buffer.
45
+
46
+ ## Remote control
47
+
48
+ Unless `remoteControl: false` is set, the client polls `GET {endpoint}/sdk/config` with the same key headers as
49
+ ingestion and `If-None-Match: "<revision>"` (a `304` keeps the current snapshot). It polls at start-up and every
50
+ `configPollIntervalMs` (default 60 000, at least 15 000). The server can ask for a longer interval with
51
+ `pollAfterSeconds`, and while the kill switch is on the client polls every 10 minutes. On failure it keeps the last
52
+ good snapshot and retries with backoff; the first failure in a row is reported to `onError` as `CONTROL_FETCH_FAILED`.
53
+ Timers are unref'd, and `shutdown()` stops polling.
54
+
55
+ - **Kill switch:** `sdk.enabled: false` stops collection and flushing (buffered items wait) until it is true again.
56
+ It is separate from `setEnabled()`: enabling locally does not override it.
57
+ - `sdk.sampleRate` and `sdk.logLevel` override `sampleRate` and `minLogLevel` while the snapshot sets them.
58
+
59
+ ```ts
60
+ const state = acr.getControlState(); // { ready, revision, sdkEnabled, maintenance, announcement, release, flags, … }
61
+ const off = acr.onControlChange((s) => render(s));
62
+ await acr.refreshControl(); // poll now
63
+ ```
64
+
65
+ `getControlState()` returns the same frozen object until something changes (new revision, `identify()`, an
66
+ announcement window opening or closing), so it works directly with `useSyncExternalStore`. `maintenance.active` is
67
+ false for subjects in `allowUserIds`. `release.updateAvailable` and `release.belowMinimum` compare `appVersion`
68
+ (semver) with `latestVersion` and `minimumVersion`; versions that are not valid semver never prompt.
69
+
70
+ Building blocks for other SDKs and frameworks: `parseSnapshot`, `planCommands`, `evaluateFlag`, `compareSemver`,
71
+ `deriveControlState`, `maintenanceResponse` and `isMaintenanceExemptPath`.
72
+
73
+ ## Feature flags
74
+
75
+ ```ts
76
+ acr.flag('new-checkout', false); // current subject: userId if identified, else anonymousId
77
+ acr.flag('new-checkout', false, { userId: 'user_42' }); // servers: evaluate for the request's user
78
+ acr.flags(); // every flag, evaluated for the current subject
79
+ ```
80
+
81
+ Flags are evaluated locally from the snapshot; there is no request per flag. A missing or disabled flag returns the
82
+ fallback. A subject in `allowUserIds` gets `value`. Otherwise `FNV-1a-32(flagKey + ":" + subject) % 100 < rollout`
83
+ gets `value` and everyone else gets the flag's `default`. Without a subject (a server call with no user) only a
84
+ 100 % rollout serves `value`. When the served value has a different type from the fallback, the fallback is returned.
85
+ The vectors in `packages/conformance/fixtures` pin the algorithm for every language.
86
+
87
+ ## Wire protocol
88
+
89
+ Every SDK sends the same protocol: `POST {endpoint}/ingest/{events|logs|errors|heartbeat}` with the `X-ACR-Api-Key`
90
+ header (and `X-ACR-Secret-Key` for server keys). The credential securely determines the app and its single
91
+ environment; the body is `{ schemaVersion: 1, sdk, items[1..500] }`.
92
+ Each item has a client `id`, which lets the server drop duplicates when a batch is retried.
93
+
94
+ Retries use exponential backoff with jitter for network errors, 429 and 5xx responses. Other 4xx responses are not retried.
95
+ Telemetry is buffered in a bounded in-memory queue. When it is full, the oldest items are dropped and counted in `stats()`.
96
+ SDK code never throws into the host application.
97
+
98
+ Metrics (`increment`, `gauge`, `histogram`) and traces are not yet available.
@@ -0,0 +1,9 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.backoffMs = backoffMs;
4
+ /** Exponential backoff with equal jitter: half fixed, half random. Capped at 30 s. */
5
+ function backoffMs(attempt, baseMs) {
6
+ const exp = Math.min(30000, baseMs * 2 ** attempt);
7
+ return exp / 2 + Math.random() * (exp / 2);
8
+ }
9
+ //# sourceMappingURL=backoff.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"backoff.js","sourceRoot":"","sources":["../../src/backoff.ts"],"names":[],"mappings":";;AACA,8BAGC;AAJD,sFAAsF;AACtF,SAAgB,SAAS,CAAC,OAAe,EAAE,MAAc;IACvD,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,KAAM,EAAE,MAAM,GAAG,CAAC,IAAI,OAAO,CAAC,CAAC;IACpD,OAAO,GAAG,GAAG,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC;AAC7C,CAAC"}
@@ -0,0 +1,50 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.BoundedBuffer = void 0;
4
+ /**
5
+ * Bounded FIFO. When full, the oldest entries are dropped so the newest telemetry survives and memory stays flat.
6
+ * Dropped entries are counted, never thrown.
7
+ */
8
+ class BoundedBuffer {
9
+ constructor(maxSize) {
10
+ this.maxSize = maxSize;
11
+ this.items = [];
12
+ this.droppedCount = 0;
13
+ }
14
+ get size() {
15
+ return this.items.length;
16
+ }
17
+ get dropped() {
18
+ return this.droppedCount;
19
+ }
20
+ /** Adds one entry, dropping the oldest when full. */
21
+ push(item) {
22
+ if (this.items.length >= this.maxSize) {
23
+ this.items.shift();
24
+ this.droppedCount += 1;
25
+ }
26
+ this.items.push(item);
27
+ }
28
+ /** Removes up to `max` entries from the front, in order. */
29
+ take(max) {
30
+ return this.items.splice(0, Math.max(0, max));
31
+ }
32
+ /** Puts entries back at the front (after a failed send). Keeps the bound by dropping the newest overflow. */
33
+ requeue(items) {
34
+ this.items = [...items, ...this.items];
35
+ if (this.items.length > this.maxSize) {
36
+ const overflow = this.items.length - this.maxSize;
37
+ this.items.splice(this.maxSize, overflow);
38
+ this.droppedCount += overflow;
39
+ }
40
+ }
41
+ /** Copy of the current entries, oldest first. The buffer is not changed. */
42
+ toArray() {
43
+ return [...this.items];
44
+ }
45
+ clear() {
46
+ this.items = [];
47
+ }
48
+ }
49
+ exports.BoundedBuffer = BoundedBuffer;
50
+ //# sourceMappingURL=buffer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"buffer.js","sourceRoot":"","sources":["../../src/buffer.ts"],"names":[],"mappings":";;;AAAA;;;GAGG;AACH,MAAa,aAAa;IAIxB,YAA6B,OAAe;QAAf,YAAO,GAAP,OAAO,CAAQ;QAHpC,UAAK,GAAQ,EAAE,CAAC;QAChB,iBAAY,GAAG,CAAC,CAAC;IAEsB,CAAC;IAEhD,IAAI,IAAI;QACN,OAAO,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC;IAC3B,CAAC;IAED,IAAI,OAAO;QACT,OAAO,IAAI,CAAC,YAAY,CAAC;IAC3B,CAAC;IAED,qDAAqD;IACrD,IAAI,CAAC,IAAO;QACV,IAAI,IAAI,CAAC,KAAK,CAAC,MAAM,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;YACtC,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC;YACnB,IAAI,CAAC,YAAY,IAAI,CAAC,CAAC;QACzB,CAAC;QACD,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACxB,CAAC;IAED,4DAA4D;IAC5D,IAAI,CAAC,GAAW;QACd,OAAO,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC;IAChD,CAAC;IAED,6GAA6G;IAC7G,OAAO,CAAC,KAAU;QAChB,IAAI,CAAC,KAAK,GAAG,CAAC,GAAG,KAAK,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC;QACvC,IAAI,IAAI,CAAC,KAAK,CAAC,MAAM,GAAG,IAAI,CAAC,OAAO,EAAE,CAAC;YACrC,MAAM,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC;YAClD,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC;YAC1C,IAAI,CAAC,YAAY,IAAI,QAAQ,CAAC;QAChC,CAAC;IACH,CAAC;IAED,4EAA4E;IAC5E,OAAO;QACL,OAAO,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC;IACzB,CAAC;IAED,KAAK;QACH,IAAI,CAAC,KAAK,GAAG,EAAE,CAAC;IAClB,CAAC;CACF;AA9CD,sCA8CC"}