wardx 0.5.0 → 0.8.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.
package/README.md CHANGED
@@ -8,6 +8,26 @@ The SDK records logs, events, and metrics. The SDK also gets Remote Config and a
8
8
 
9
9
  A measure call changes local memory only. The SDK sends frames on a timer. The SDK uses HTTP `POST /v1/sync` with JSON and gzip.
10
10
 
11
+ ## Disable the SDK
12
+
13
+ `enabled` defaults to `true`. Set it when creating the client:
14
+
15
+ ```js
16
+ const wardx = createWardx({ enabled: false });
17
+ const requests = wardx.counter('requests');
18
+ requests.inc();
19
+ await wardx.shutdown();
20
+ ```
21
+
22
+ When disabled, credentials and other connection options are unnecessary. Metric
23
+ handles, timers, events, logs, identity, retention, and experiment goals do nothing.
24
+ The client does not initialize the telemetry engine, transport, or background
25
+ timers, and does not hash identifiers or call the tracer. `flush()` and `shutdown()`
26
+ resolve immediately. `config.get(key, fallback)` returns the supplied fallback.
27
+
28
+ The mode is fixed at creation; changing the options or `settings` afterward does
29
+ not toggle it. Cached metric handles remain safe to call, including after shutdown.
30
+
11
31
  ## Install
12
32
 
13
33
  ```bash
@@ -43,7 +63,7 @@ To receive frames, run an ingest server. Install `@wardx/server` and start it wi
43
63
 
44
64
  ## Start the SDK
45
65
 
46
- `createWardx` requires these keys:
66
+ When enabled, `createWardx` requires these keys:
47
67
 
48
68
  | Key | Description |
49
69
  | --- | --- |
@@ -59,6 +79,48 @@ Optional keys include `tracer` and overrides for centralized values in `@wardx/c
59
79
 
60
80
  The SDK starts a bootstrap sync immediately. The SDK then syncs on `syncIntervalMs` with jitter.
61
81
 
82
+ ## Recommended: bind once, measure through handles
83
+
84
+ Create metric handles once per client and stable name/dimension combination,
85
+ then reuse them in handlers, callbacks, and loops. This avoids repeated dimension
86
+ validation, series-key construction, and registry lookup. Normal aggregation
87
+ windows and flushes reset values, not handles. Rebind when replacing the client;
88
+ do not mutate a dimension dictionary to retarget an existing handle.
89
+
90
+ For varying dimensions, bind one recorder per application-owned, bounded value
91
+ set (mode, region, source). Never build an unbounded handle cache keyed by user
92
+ IDs or arbitrary input. Keep histogram buckets fixed. Timer tokens measure one
93
+ operation: create a fresh token for each operation, not one token for the client.
94
+ For a hot duration path, reuse a histogram and observe an application-measured
95
+ elapsed duration instead. Events, logs, retention, and experiment goals remain
96
+ per-occurrence calls.
97
+
98
+ ```js
99
+ function createMatchTelemetry(wardx, mode) {
100
+ const completed = wardx.counter('match.completed', { mode });
101
+ const duration = wardx.histogram('match.duration_ms', {
102
+ mode, buckets: [30_000, 60_000, 180_000, 600_000, 1_800_000]
103
+ });
104
+ const players = wardx.distinct('match.players', { mode });
105
+ return {
106
+ onCompleted(durationMs, userId) {
107
+ completed.inc();
108
+ duration.observe(durationMs);
109
+ players.add(userId);
110
+ }
111
+ };
112
+ }
113
+
114
+ const rankedTelemetry = createMatchTelemetry(wardx, 'ranked');
115
+ // In each ranked-match callback:
116
+ rankedTelemetry.onCompleted(durationMs, userId);
117
+ ```
118
+
119
+ The same setup works with `enabled: false`: cached handles are inert. An inline
120
+ lookup is valid for occasional instrumentation; prefer stored handles for
121
+ recurring work. API reference tables show lookup and measurement together only
122
+ to identify the methods.
123
+
62
124
  ## Use case 1: Instrument a Node.js service
63
125
 
64
126
  **When:** You run a Node.js process and you need telemetry.
@@ -80,10 +142,14 @@ const wardx = createWardx({
80
142
 
81
143
  wardx.log.info('match_started', { mode: 'ranked', players: 4 });
82
144
  wardx.event('match.started', { mode: 'ranked', country: 'AR' });
83
- wardx.counter('match.completed', { mode: 'ranked' }).inc();
84
- wardx.gauge('players.online').set(12);
85
- wardx.histogram('request.duration', { buckets: [10, 25, 50, 100, 250] }).observe(42);
86
- wardx.distinct('shot.traffic.hids', { result: 'violating' }).add(hid);
145
+ const matchCompleted = wardx.counter('match.completed', { mode: 'ranked' });
146
+ matchCompleted.inc();
147
+ const playersOnline = wardx.gauge('players.online');
148
+ playersOnline.set(12);
149
+ const requestDuration = wardx.histogram('request.duration', { buckets: [10, 25, 50, 100, 250] });
150
+ requestDuration.observe(42);
151
+ const shotTrafficHids = wardx.distinct('shot.traffic.hids', { result: 'violating' });
152
+ shotTrafficHids.add(hid);
87
153
 
88
154
  const end = wardx.timer('matchmaking.duration');
89
155
  end({ result: 'success' });
@@ -108,21 +174,26 @@ Pass the ingest URL, project key, and project name in `createWardx`.
108
174
  **Objective:** Use `inc()` for one occurrence. Use `add(n)` for a finite sum.
109
175
 
110
176
  ```js
111
- function handleRequest(req, res, wardx) {
112
- const requests = wardx.counter('http.requests', { route: 'matchmaking' });
177
+ const requests = wardx.counter('http.requests', { route: 'matchmaking' });
178
+ const errors = wardx.counter('http.errors', { route: 'matchmaking', code: 500 });
179
+ const coinsAwarded = wardx.counter('coins.awarded', { source: 'match' });
180
+ const completedByMode = new Map(['ranked', 'casual'].map((mode) =>
181
+ [mode, wardx.counter('match.completed', { mode })]
182
+ ));
183
+
184
+ function handleRequest(req, res) {
113
185
  requests.inc();
114
-
115
- if (res.statusCode >= 500) {
116
- wardx.counter('http.errors', { route: 'matchmaking', code: 500 }).inc();
117
- }
186
+ if (res.statusCode >= 500) errors.inc();
118
187
  }
119
188
 
120
- function grantCoins(wardx, amount) {
121
- wardx.counter('coins.awarded', { source: 'match' }).add(amount);
189
+ function grantCoins(amount) {
190
+ coinsAwarded.add(amount);
122
191
  }
123
192
 
124
- function completeMatch(wardx, mode) {
125
- wardx.counter('match.completed', { mode }).inc();
193
+ function completeMatch(mode) {
194
+ const completed = completedByMode.get(mode);
195
+ if (!completed) throw new Error(`Unsupported match mode: ${mode}`);
196
+ completed.inc();
126
197
  }
127
198
  ```
128
199
 
@@ -131,7 +202,7 @@ To detect abnormal grants, pair this counter with a histogram and a rare anomaly
131
202
  ### Procedure
132
203
 
133
204
  1. Call `counter(name, dims)` to get a series.
134
- 2. Keep that object if you increment in a loop.
205
+ 2. Keep that handle in its owning module or component and reuse it across calls and windows.
135
206
  3. Call `inc()` to add `1`.
136
207
  4. Call `add(n)` to add a finite number.
137
208
 
@@ -146,11 +217,17 @@ A counter in a frame is a window delta. The counter is not a lifetime total.
146
217
  **Objective:** Call `set(value)` with a finite number. The frame stores the last value and a timestamp.
147
218
 
148
219
  ```js
149
- function reportLobby(wardx, lobby) {
150
- wardx.gauge('players.online', { region: lobby.region }).set(lobby.playerCount);
151
- wardx.gauge('matchmaking.queue_depth').set(lobby.queue.length);
220
+ function createLobbyReporter(wardx, region) {
221
+ const playersOnline = wardx.gauge('players.online', { region });
222
+ const queueDepth = wardx.gauge('matchmaking.queue_depth');
223
+ return (lobby) => {
224
+ playersOnline.set(lobby.playerCount);
225
+ queueDepth.set(lobby.queue.length);
226
+ };
152
227
  }
153
228
 
229
+ const reportLobby = createLobbyReporter(wardx, 'south-america');
230
+
154
231
  function startQueueProbe(wardx, getQueueDepth) {
155
232
  const queue = wardx.gauge('jobs.queue_depth');
156
233
  const timer = setInterval(() => {
@@ -176,15 +253,19 @@ If you do not call `set` in a window, that series is not in the frame.
176
253
  **Objective:** Call `observe(value)` so the SDK stores count, sum, min, max, and buckets.
177
254
 
178
255
  ```js
179
- function recordRequest(wardx, durationMs, bytes) {
180
- wardx.histogram('http.duration_ms', { route: 'checkout' }).observe(durationMs);
181
- wardx.histogram('http.payload_bytes', {
182
- buckets: [256, 1024, 4096, 16384, 65536]
183
- }).observe(bytes);
256
+ const requestDuration = wardx.histogram('http.duration_ms', { route: 'checkout' });
257
+ const payloadBytes = wardx.histogram('http.payload_bytes', {
258
+ buckets: [256, 1024, 4096, 16384, 65536]
259
+ });
260
+ const awardSize = wardx.histogram('coins.award_size');
261
+
262
+ function recordRequest(durationMs, bytes) {
263
+ requestDuration.observe(durationMs);
264
+ payloadBytes.observe(bytes);
184
265
  }
185
266
 
186
- function recordAward(wardx, amount, grantId) {
187
- wardx.histogram('coins.award_size').observe(amount, { grantId });
267
+ function recordAward(amount, grantId) {
268
+ awardSize.observe(amount, { grantId });
188
269
  }
189
270
  ```
190
271
 
@@ -205,15 +286,18 @@ If you start and stop a duration in the same process, use `timer` instead of a h
205
286
  **Objective:** Start a timer. Stop the timer when the work ends. The SDK records milliseconds in a histogram.
206
287
 
207
288
  ```js
208
- export async function handleMatchmaking(req, res, wardx) {
289
+ const matchmakingOk = wardx.counter('matchmaking.ok');
290
+ const matchmakingError = wardx.counter('matchmaking.error');
291
+
292
+ export async function handleMatchmaking(req, res) {
209
293
  const end = wardx.timer('matchmaking.duration', { route: 'matchmaking' });
210
294
  try {
211
295
  const result = await findMatch(req.body);
212
- wardx.counter('matchmaking.ok').inc();
296
+ matchmakingOk.inc();
213
297
  end({ result: 'success' });
214
298
  res.end(JSON.stringify(result));
215
299
  } catch (err) {
216
- wardx.counter('matchmaking.error').inc();
300
+ matchmakingError.inc();
217
301
  wardx.log.error('matchmaking_failed', { code: err.code || 'unknown' });
218
302
  end({ result: 'error' });
219
303
  res.statusCode = 500;
@@ -238,26 +322,28 @@ The stop function records milliseconds. Dimensions that you pass to the stop fun
238
322
  **Objective:** Call `event(name, attrs)`. Do not use an event when a counter is enough.
239
323
 
240
324
  ```js
241
- function onMatchStarted(wardx, match) {
242
- wardx.event('match.started', {
243
- mode: match.mode,
244
- country: match.country,
245
- players: match.players.length
246
- });
247
- wardx.counter('match.started', { mode: match.mode }).inc();
325
+ function createMatchStartedRecorder(wardx, mode) {
326
+ const started = wardx.counter('match.started', { mode });
327
+ return (match) => {
328
+ wardx.event('match.started', { mode, country: match.country, players: match.players.length });
329
+ started.inc();
330
+ };
248
331
  }
249
332
 
250
- function onPurchase(wardx, order) {
251
- wardx.event('purchase', {
252
- product: order.product,
253
- currency: order.currency,
254
- amount: order.amount
255
- });
256
- wardx.counter('purchase.count', { product: order.product }).inc();
257
- wardx.counter('purchase.amount', { currency: order.currency }).add(order.amount);
333
+ function createPurchaseRecorder(wardx, product, currency) {
334
+ const purchases = wardx.counter('purchase.count', { product });
335
+ const amount = wardx.counter('purchase.amount', { currency });
336
+ return (order) => {
337
+ wardx.event('purchase', { product, currency, amount: order.amount });
338
+ purchases.inc();
339
+ amount.add(order.amount);
340
+ };
258
341
  }
259
342
 
260
- function onSignup(wardx, user) {
343
+ const onMatchStarted = createMatchStartedRecorder(wardx, 'ranked');
344
+ const onPurchase = createPurchaseRecorder(wardx, 'coins-small', 'USD');
345
+
346
+ function onSignup(user) {
261
347
  wardx.event('signup.completed', { method: user.method });
262
348
  }
263
349
  ```
@@ -283,21 +369,28 @@ Wardx does not store a user journey. Delivery is at-most-once. Production discar
283
369
  Give each step its own name. Do not reuse `screen.view` with a `surface` attr as the funnel. Use `surface` only as a counter dimension when you also need a breakdown of one step.
284
370
 
285
371
  ```js
286
- function onOnboardingStart(wardx, channel) {
287
- wardx.event('onboarding.start', { channel });
288
- wardx.counter('onboarding.start', { channel }).inc();
372
+ function createOnboardingTelemetry(wardx, channel) {
373
+ const started = wardx.counter('onboarding.start', { channel });
374
+ const profile = wardx.counter('onboarding.profile');
375
+ const done = wardx.counter('onboarding.done');
376
+ return {
377
+ onStart() {
378
+ wardx.event('onboarding.start', { channel });
379
+ started.inc();
380
+ },
381
+ onProfile() {
382
+ wardx.event('onboarding.profile');
383
+ profile.inc();
384
+ },
385
+ onDone(userId) {
386
+ wardx.event('onboarding.done');
387
+ done.inc();
388
+ wardx.experiment.goal('onboarding.done', { subjectId: userId });
389
+ }
390
+ };
289
391
  }
290
392
 
291
- function onOnboardingProfile(wardx) {
292
- wardx.event('onboarding.profile');
293
- wardx.counter('onboarding.profile').inc();
294
- }
295
-
296
- function onOnboardingDone(wardx, userId) {
297
- wardx.event('onboarding.done');
298
- wardx.counter('onboarding.done').inc();
299
- wardx.experiment.goal('onboarding.done', { subjectId: userId });
300
- }
393
+ const onboarding = createOnboardingTelemetry(wardx, 'organic');
301
394
  ```
302
395
 
303
396
  ### Procedure
@@ -361,22 +454,28 @@ Retention requires a server with this feature; older servers cannot compute it.
361
454
  Wardx is not a ledger. Delivery is at-most-once. Production discards envelopes after ingest (`sink: "null"`). A player's wallet, and the row that explains one grant, live in the game database. Wardx answers whether the fleet is granting too much, or too large, in a 1-minute window.
362
455
 
363
456
  ```js
364
- function grantCoins(wardx, grant) {
365
- const { source, amount, reason, id } = grant;
366
-
367
- wardx.counter('coins.awarded', { source }).add(amount);
368
- wardx.counter('coins.grants', { source }).inc();
369
- wardx.histogram('coins.award_size', {
370
- source,
371
- buckets: [10, 50, 100, 250, 500, 1000, 5000]
372
- }).observe(amount, { grantId: id, reason });
373
-
374
- const maxAward = wardx.config.get('economy.maxAward', 500);
375
- if (amount > maxAward) {
376
- wardx.event('coins.anomaly', { source, amount, reason, grantId: id });
377
- wardx.log.warn('coins_anomaly', { source, amount, reason, grantId: id });
378
- }
457
+ function createGrantRecorder(wardx, source) {
458
+ const awarded = wardx.counter('coins.awarded', { source });
459
+ const grants = wardx.counter('coins.grants', { source });
460
+ const size = wardx.histogram('coins.award_size', {
461
+ source, buckets: [10, 50, 100, 250, 500, 1000, 5000]
462
+ });
463
+ return ({ amount, reason, id }) => {
464
+ awarded.add(amount);
465
+ grants.inc();
466
+ size.observe(amount, { grantId: id, reason });
467
+
468
+ const maxAward = wardx.config.get('economy.maxAward', 500);
469
+ if (amount > maxAward) {
470
+ wardx.event('coins.anomaly', { source, amount, reason, grantId: id });
471
+ wardx.log.warn('coins_anomaly', { source, amount, reason, grantId: id });
472
+ }
473
+ };
379
474
  }
475
+
476
+ const recordMatchGrant = createGrantRecorder(wardx, 'match');
477
+ // After committing each match reward:
478
+ recordMatchGrant({ amount: 50, reason: 'win', id: 'grant-42' });
380
479
  ```
381
480
 
382
481
  `source` is a small set, for example `match`, `daily`, `purchase`, or `admin`.
@@ -436,19 +535,20 @@ The ingest server config can define experiment `message-delay-v1` on key `messag
436
535
  On a client with one user, call `identify` once after login. Later `config.get` and `experiment.goal` use that subject. On a server that handles many users, pass `{ subjectId }` on every call. Do not use one SDK instance's default there; it would mix users.
437
536
 
438
537
  ```js
538
+ const messagesSent = wardx.counter('message.sent');
439
539
  wardx.identify(userId);
440
540
  const delayMs = wardx.config.get('message.delayMs', 1000);
441
541
  setTimeout(() => {
442
542
  deliver(text);
443
- wardx.counter('message.sent').inc();
543
+ messagesSent.inc();
444
544
  wardx.experiment.goal('message.sent', { value: 1 });
445
545
  }, delayMs);
446
546
 
447
- function sendMessage(wardx, userId, text) {
547
+ function sendMessage(userId, text) {
448
548
  const delayMs = wardx.config.get('message.delayMs', 1000, { subjectId: userId });
449
549
  setTimeout(() => {
450
550
  deliver(text);
451
- wardx.counter('message.sent').inc();
551
+ messagesSent.inc();
452
552
  wardx.experiment.goal('message.sent', { subjectId: userId, value: 1 });
453
553
  }, delayMs);
454
554
  }
@@ -483,7 +583,8 @@ const wardx = createWardx({
483
583
  privacySalt: 'demo-subject-hash-v1'
484
584
  });
485
585
 
486
- wardx.counter('jobs.completed').inc();
586
+ const jobsCompleted = wardx.counter('jobs.completed');
587
+ jobsCompleted.inc();
487
588
  await wardx.shutdown();
488
589
  ```
489
590
 
@@ -561,22 +662,28 @@ Two signals:
561
662
 
562
663
  ```js
563
664
  const SESSION_BUCKETS = [30_000, 60_000, 180_000, 300_000, 600_000, 1_200_000, 1_800_000, 3_600_000];
665
+ const sessionDuration = wardx.histogram('session.duration', { buckets: SESSION_BUCKETS });
666
+ const sessionTime = wardx.counter('session.time_ms');
667
+ const sessionsEnded = wardx.counter('session.ended');
564
668
 
565
- function onPlaySessionStart(wardx, userId) {
669
+ function onPlaySessionStart(userId) {
566
670
  wardx.identify(userId);
567
- return { startedAt: Date.now() };
671
+ return { startedAt: performance.now(), reportedMs: 0 };
568
672
  }
569
673
 
570
- function onPlaySessionEnd(wardx, session) {
571
- const durationMs = Date.now() - session.startedAt;
572
- wardx.histogram('session.duration', { buckets: SESSION_BUCKETS }).observe(durationMs);
573
- wardx.counter('session.time_ms').add(durationMs);
574
- wardx.counter('session.ended').inc();
575
- wardx.experiment.goal('session.duration', { value: durationMs });
674
+ function onPlayHeartbeat(session) {
675
+ const elapsedMs = performance.now() - session.startedAt;
676
+ sessionTime.add(elapsedMs - session.reportedMs);
677
+ session.reportedMs = elapsedMs;
576
678
  }
577
679
 
578
- function onPlayHeartbeat(wardx, elapsedMs) {
579
- wardx.counter('session.time_ms').add(elapsedMs);
680
+ function onPlaySessionEnd(session) {
681
+ const durationMs = performance.now() - session.startedAt;
682
+ sessionDuration.observe(durationMs);
683
+ sessionTime.add(durationMs - session.reportedMs);
684
+ session.reportedMs = durationMs;
685
+ sessionsEnded.inc();
686
+ wardx.experiment.goal('session.duration', { value: durationMs });
580
687
  }
581
688
  ```
582
689
 
@@ -603,22 +710,29 @@ If you only increment `session.time_ms` and never emit the goal, MCP can still s
603
710
  The keys must already exist in Remote Config. The game reads them with `config.get`. Variants may only change those keys.
604
711
 
605
712
  ```js
606
- function onLevelStart(wardx, userId, levelId) {
607
- const enemyHp = wardx.config.get(`level.${levelId}.enemyHp`, 100, { subjectId: userId });
608
- wardx.event('level.start', { level: levelId });
609
- wardx.counter('level.start', { level: levelId }).inc();
610
- return enemyHp;
713
+ function createLevelTelemetry(wardx, levelId) {
714
+ const started = wardx.counter('level.start', { level: levelId });
715
+ const failed = wardx.counter('level.fail', { level: levelId });
716
+ const completed = wardx.counter('level.complete', { level: levelId });
717
+ return {
718
+ onStart(userId) {
719
+ const enemyHp = wardx.config.get(`level.${levelId}.enemyHp`, 100, { subjectId: userId });
720
+ wardx.event('level.start', { level: levelId });
721
+ started.inc();
722
+ return enemyHp;
723
+ },
724
+ onFail() {
725
+ wardx.event('level.fail', { level: levelId });
726
+ failed.inc();
727
+ },
728
+ onComplete() {
729
+ wardx.event('level.complete', { level: levelId });
730
+ completed.inc();
731
+ }
732
+ };
611
733
  }
612
734
 
613
- function onLevelFail(wardx, levelId) {
614
- wardx.event('level.fail', { level: levelId });
615
- wardx.counter('level.fail', { level: levelId }).inc();
616
- }
617
-
618
- function onLevelComplete(wardx, levelId) {
619
- wardx.event('level.complete', { level: levelId });
620
- wardx.counter('level.complete', { level: levelId }).inc();
621
- }
735
+ const levelThree = createLevelTelemetry(wardx, 3);
622
736
  ```
623
737
 
624
738
  `level` is a small set of ids. Do not put a unique run id on the counter.
@@ -636,12 +750,18 @@ From MCP, after onboarding: `upsert_experiment` on the existing keys (`level.3.e
636
750
  **Objective:** Count the failure. Log the error with a stack or a provider code. Wardx does not edit source. MCP returns the row. The agent uses `path` or `git` on that role, plus its own file permissions, to change the code.
637
751
 
638
752
  ```js
639
- function handleCheckout(req, res, wardx) {
753
+ const paymentOk = wardx.counter('payment.ok');
754
+ const paymentErrors = new Map(['timeout', 'card_declined', 'unknown'].map((code) =>
755
+ [code, wardx.counter('payment.error', { code })]
756
+ ));
757
+
758
+ function handleCheckout(req, res) {
640
759
  try {
641
760
  charge(req.body);
642
- wardx.counter('payment.ok').inc();
761
+ paymentOk.inc();
643
762
  } catch (err) {
644
- wardx.counter('payment.error', { code: err.code || 'unknown' }).inc();
763
+ const errors = paymentErrors.get(err.code) ?? paymentErrors.get('unknown');
764
+ errors.inc();
645
765
  wardx.log.error('payment_failed', {
646
766
  name: err.name,
647
767
  code: err.code || 'unknown',
@@ -659,6 +779,10 @@ function clipStack(err, max = 4096) {
659
779
  }
660
780
  ```
661
781
 
782
+ Use the provider's finite error-code vocabulary in `paymentErrors`; map other
783
+ codes to `unknown` without growing the handle cache. The original code remains
784
+ in the log attrs.
785
+
662
786
  `stack` is an attr string. Do not send the Error object.
663
787
 
664
788
  From MCP: `get_aggregates` for the rate, then `get_recent_logs` with `level: 'error'` and the message. If the role has `path` or `git` in the catalog, the agent opens that checkout and edits there. If those fields are empty, Wardx has no source hint. Do not invent a path.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wardx",
3
- "version": "0.5.0",
3
+ "version": "0.8.0",
4
4
  "description": "Node.js SDK for Wardx telemetry, Remote Config, and experiments.",
5
5
  "keywords": [
6
6
  "wardx",
@@ -37,6 +37,6 @@
37
37
  "src"
38
38
  ],
39
39
  "dependencies": {
40
- "@wardx/core": "0.5.0"
40
+ "@wardx/core": "0.8.0"
41
41
  }
42
42
  }
package/src/WardxNode.js CHANGED
@@ -6,6 +6,7 @@ import {
6
6
  nextSyncDelayMs,
7
7
  ulid
8
8
  } from '@wardx/core';
9
+ import { disabledCore } from './disabled.js';
9
10
  import { readFileSync } from 'node:fs';
10
11
  import { dirname, join } from 'node:path';
11
12
  import { fileURLToPath } from 'node:url';
@@ -20,13 +21,8 @@ const pkg = JSON.parse(
20
21
  export class WardxNode {
21
22
  constructor(settings) {
22
23
  this.settings = settings;
23
- this._core = new WardxCore(settings);
24
- this._transport = createHttpTransport(settings);
25
- this._stopped = false;
26
- this._shutdownPromise = null;
27
- this._syncChain = Promise.resolve();
28
- this._instanceId = ulid();
29
- this._sessionId = ulid();
24
+ this._disabled = settings.enabled === false;
25
+ this._core = this._disabled ? disabledCore : new WardxCore(settings);
30
26
  this.log = this._core.log;
31
27
  this.config = {
32
28
  get: (key, fallback, context) => this._core.configGet(key, fallback, context)
@@ -34,6 +30,13 @@ export class WardxNode {
34
30
  this.experiment = {
35
31
  goal: (name, context) => this._core.experimentGoal(name, context)
36
32
  };
33
+ if (this._disabled) return;
34
+ this._transport = createHttpTransport(settings);
35
+ this._stopped = false;
36
+ this._shutdownPromise = null;
37
+ this._syncChain = Promise.resolve();
38
+ this._instanceId = ulid();
39
+ this._sessionId = ulid();
37
40
  this._aggregateTimer = setInterval(() => {
38
41
  this._core.internal.processRssBytes = readProcessRssBytes();
39
42
  this._core.snapshotIfDirty();
@@ -76,10 +79,12 @@ export class WardxNode {
76
79
  }
77
80
 
78
81
  flush() {
82
+ if (this._disabled) return Promise.resolve();
79
83
  return this._enqueueSync({ flush: true });
80
84
  }
81
85
 
82
86
  shutdown() {
87
+ if (this._disabled) return Promise.resolve();
83
88
  if (this._shutdownPromise === null) this._shutdownPromise = this._shutdown();
84
89
  return this._shutdownPromise;
85
90
  }
@@ -0,0 +1,19 @@
1
+ const noop = () => {};
2
+ const counter = Object.freeze({ inc: noop, add: noop });
3
+ const gauge = Object.freeze({ set: noop });
4
+ const histogram = Object.freeze({ observe: noop });
5
+ const distinct = Object.freeze({ add: noop });
6
+
7
+ export const disabledCore = Object.freeze({
8
+ counter: () => counter,
9
+ gauge: () => gauge,
10
+ histogram: () => histogram,
11
+ distinct: () => distinct,
12
+ timer: () => noop,
13
+ event: noop,
14
+ retentionActivity: noop,
15
+ identify: noop,
16
+ log: Object.freeze({ debug: noop, info: noop, warn: noop, error: noop }),
17
+ configGet: (key, fallback) => fallback,
18
+ experimentGoal: noop
19
+ });
package/src/index.d.ts CHANGED
@@ -70,12 +70,14 @@ export interface ExperimentApi {
70
70
  goal(name: string, context?: ExperimentGoalContext): void;
71
71
  }
72
72
 
73
+ export type DisabledWardxOptions = Partial<CreateWardxOptions> & { enabled: false };
74
+
73
75
  export class WardxNode {
74
- settings: ResolvedSettings;
76
+ settings: ResolvedSettings | DisabledWardxOptions;
75
77
  log: LogApi;
76
78
  config: ConfigApi;
77
79
  experiment: ExperimentApi;
78
- constructor(settings: ResolvedSettings);
80
+ constructor(settings: ResolvedSettings | DisabledWardxOptions);
79
81
  retentionActivity(userId: string): void;
80
82
  identify(subjectId: string | null | undefined): void;
81
83
  counter(name: string, dims?: Dimensions | null): CounterHandle;
@@ -88,5 +90,5 @@ export class WardxNode {
88
90
  shutdown(): Promise<void>;
89
91
  }
90
92
 
91
- export function createWardx(options: CreateWardxOptions): WardxNode;
93
+ export function createWardx(options: CreateWardxOptions | DisabledWardxOptions): WardxNode;
92
94
  export function createConsoleTracer(options?: ConsoleTracerOptions): Tracer;
package/src/index.js CHANGED
@@ -3,7 +3,10 @@ import { WardxNode } from './WardxNode.js';
3
3
  import { createConsoleTracer } from './trace/createConsoleTracer.js';
4
4
 
5
5
  export function createWardx(options) {
6
- return new WardxNode(resolveSettings(options));
6
+ if (options?.enabled !== undefined && typeof options.enabled !== 'boolean') {
7
+ throw new Error('enabled must be a boolean');
8
+ }
9
+ return new WardxNode(options?.enabled === false ? { enabled: false } : resolveSettings(options));
7
10
  }
8
11
 
9
12
  export { WardxNode, createConsoleTracer };