wardx 0.1.7 → 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.
- package/README.md +24 -13
- package/package.json +3 -3
- package/src/WardxNode.js +11 -2
- package/src/index.d.ts +3 -0
package/README.md
CHANGED
|
@@ -32,8 +32,10 @@ To receive frames, run an ingest server. Install `@wardx/server` and start it wi
|
|
|
32
32
|
- A measure call does not send network data.
|
|
33
33
|
- A measure call does not wait for a Promise.
|
|
34
34
|
- Delivery is at-most-once. If a sync fails, the SDK discards the batch.
|
|
35
|
+
- Physical frames are serialized, split to `maxFrameBytes`, and assigned consecutive sequence numbers. An individually oversized row is counted in `wardx.internal.frame_rows_dropped`.
|
|
35
36
|
- The application has priority over telemetry.
|
|
36
37
|
- Remote Config is always read from local memory.
|
|
38
|
+
- Remote Config must not contain secrets. The project key authenticates the project; client-selected `role` is routing metadata, not authorization.
|
|
37
39
|
- The SDK sends names only. Descriptions live in the server catalog: ship them in the config file, or fill them during MCP onboarding.
|
|
38
40
|
- `identify(subjectId)` sets the default subject for this instance. A per-call `{ subjectId }` overrides it. A process that serves many users must pass `subjectId` on each call and must not `identify()`.
|
|
39
41
|
|
|
@@ -48,11 +50,12 @@ To receive frames, run an ingest server. Install `@wardx/server` and start it wi
|
|
|
48
50
|
| `endpoint` | Base URL of the ingest server, for example `http://127.0.0.1:8787`. |
|
|
49
51
|
| `projectKey` | Value of header `X-Wardx-Key`. |
|
|
50
52
|
| `project` | Project name. The name must match the server mapping. |
|
|
51
|
-
| `role` |
|
|
53
|
+
| `role` | Routing name of this instance inside the project, for example `client`, `unity`, `game-server`, `desktop`. Not `*`; not an authorization boundary. |
|
|
52
54
|
| `appVersion` | Application version. |
|
|
53
55
|
| `environment` | Environment name. |
|
|
56
|
+
| `privacySalt` | Required stable, project-specific salt for one-way subject hashes. |
|
|
54
57
|
|
|
55
|
-
Optional keys include `
|
|
58
|
+
Optional keys include `tracer` and overrides for centralized values in `@wardx/core` `defaults.json`. `maxFrameBytes` is at least `1024`; `experimentStateMaxSubjects` defaults to `100000` and bounds assignment/exposure state in this SDK instance. Missing or empty `privacySalt` is rejected; it is never derived from the project credential. `tracer` is a local diagnostic hook. It does not go over the wire.
|
|
56
59
|
|
|
57
60
|
The SDK starts a bootstrap sync immediately. The SDK then syncs on `syncIntervalMs` with jitter.
|
|
58
61
|
|
|
@@ -71,7 +74,8 @@ const wardx = createWardx({
|
|
|
71
74
|
project: 'demo',
|
|
72
75
|
role: 'client',
|
|
73
76
|
appVersion: '2.4.1',
|
|
74
|
-
environment: 'production'
|
|
77
|
+
environment: 'production',
|
|
78
|
+
privacySalt: 'demo-subject-hash-v1'
|
|
75
79
|
});
|
|
76
80
|
|
|
77
81
|
wardx.log.info('match_started', { mode: 'ranked', players: 4 });
|
|
@@ -79,6 +83,7 @@ wardx.event('match.started', { mode: 'ranked', country: 'AR' });
|
|
|
79
83
|
wardx.counter('match.completed', { mode: 'ranked' }).inc();
|
|
80
84
|
wardx.gauge('players.online').set(12);
|
|
81
85
|
wardx.histogram('request.duration', { buckets: [10, 25, 50, 100, 250] }).observe(42);
|
|
86
|
+
wardx.distinct('shot.traffic.hids', { result: 'violating' }).add(hid);
|
|
82
87
|
|
|
83
88
|
const end = wardx.timer('matchmaking.duration');
|
|
84
89
|
end({ result: 'success' });
|
|
@@ -273,7 +278,7 @@ If the event buffer is full, the SDK discards the new event and increments `ward
|
|
|
273
278
|
|
|
274
279
|
**Objective:** Emit one named event and one counter per step. Compare those counts. Do not reconstruct a per-user path.
|
|
275
280
|
|
|
276
|
-
Wardx does not store a user journey. Delivery is at-most-once. Production discards envelopes after ingest (`sink: "null"`). The aggregator counts events by name and role. Event attrs do not split that count. `sessionId` identifies the envelope. It is not a join key.
|
|
281
|
+
Wardx does not store a user journey. Delivery is at-most-once. Production discards envelopes after ingest (`sink: "null"`). The aggregator counts events by name and role. Event attrs do not split that count. `sessionId` identifies the envelope. It is not a join key. `distinct` can estimate unique identifiers per named aggregate, but it does not provide ordered sequences or time between steps.
|
|
277
282
|
|
|
278
283
|
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.
|
|
279
284
|
|
|
@@ -305,7 +310,10 @@ function onOnboardingDone(wardx, userId) {
|
|
|
305
310
|
|
|
306
311
|
On a backend that serves many users, increment the counters in process. Do not `event()` once per user action.
|
|
307
312
|
|
|
308
|
-
Do not put `userId` on a counter dimension.
|
|
313
|
+
Do not put `userId` on a counter dimension. For an approximate unique count at
|
|
314
|
+
one step, use `distinct(stepName, dims).add(userId)`. It hashes with the required
|
|
315
|
+
local `privacySalt` and sends only a 512-register HLL sketch (`p=9`, about 4.6%
|
|
316
|
+
standard error). This still does not create a Mixpanel-style per-user funnel.
|
|
309
317
|
|
|
310
318
|
## Use case 8: Detect abnormal point accumulation
|
|
311
319
|
|
|
@@ -388,7 +396,7 @@ The SDK updates the snapshot when a sync response contains a newer `configVersio
|
|
|
388
396
|
|
|
389
397
|
The ingest server config can define experiment `message-delay-v1` on key `message.delayMs`. See `@wardx/server`. Variants live on the server. The app still reads the same key.
|
|
390
398
|
|
|
391
|
-
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
|
|
399
|
+
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.
|
|
392
400
|
|
|
393
401
|
```js
|
|
394
402
|
wardx.identify(userId);
|
|
@@ -419,7 +427,7 @@ The first `config.get` that has a subject in a session can emit event `experimen
|
|
|
419
427
|
|
|
420
428
|
The payload does not contain the raw `subjectId`.
|
|
421
429
|
|
|
422
|
-
`experiment.goal` needs a subject: from `identify()` or from `{ subjectId }` on that call.
|
|
430
|
+
`experiment.goal` needs a subject: from `identify()` or from `{ subjectId }` on that call. It emits nothing until that subject has an exposure whose `goalMetric` matches the goal name; a valid event contains exactly that one assignment. Without a subject, the call throws.
|
|
423
431
|
|
|
424
432
|
## Use case 11: Continue when the ingest server is down
|
|
425
433
|
|
|
@@ -434,7 +442,8 @@ const wardx = createWardx({
|
|
|
434
442
|
project: 'demo',
|
|
435
443
|
role: 'client',
|
|
436
444
|
appVersion: '0.1.0',
|
|
437
|
-
environment: 'development'
|
|
445
|
+
environment: 'development',
|
|
446
|
+
privacySalt: 'demo-subject-hash-v1'
|
|
438
447
|
});
|
|
439
448
|
|
|
440
449
|
wardx.counter('jobs.completed').inc();
|
|
@@ -458,7 +467,8 @@ const wardx = createWardx({
|
|
|
458
467
|
project: 'demo',
|
|
459
468
|
role: 'client',
|
|
460
469
|
appVersion: '0.1.0',
|
|
461
|
-
environment: 'production'
|
|
470
|
+
environment: 'production',
|
|
471
|
+
privacySalt: 'demo-subject-hash-v1'
|
|
462
472
|
});
|
|
463
473
|
|
|
464
474
|
async function onStop() {
|
|
@@ -470,7 +480,7 @@ process.on('SIGTERM', onStop);
|
|
|
470
480
|
process.on('SIGINT', onStop);
|
|
471
481
|
```
|
|
472
482
|
|
|
473
|
-
`shutdown` is safe to call more than
|
|
483
|
+
`shutdown` is safe to call more than once. Concurrent callers receive the same promise and all await the final flush and single transport close.
|
|
474
484
|
|
|
475
485
|
`flush` sends the current pending frames and does not stop the timers. Use `shutdown` when the process stops.
|
|
476
486
|
|
|
@@ -490,6 +500,7 @@ const wardx = createWardx({
|
|
|
490
500
|
role: 'client',
|
|
491
501
|
appVersion: '0.1.0',
|
|
492
502
|
environment: 'development',
|
|
503
|
+
privacySalt: 'demo-subject-hash-v1',
|
|
493
504
|
tracer: createConsoleTracer()
|
|
494
505
|
});
|
|
495
506
|
```
|
|
@@ -579,7 +590,7 @@ The volume funnel `level.start` → `level.fail` / `level.complete` is the diffi
|
|
|
579
590
|
|
|
580
591
|
Call `experiment.goal('session.duration', { value: durationMs })` when the play session ends (use case 14).
|
|
581
592
|
|
|
582
|
-
From MCP, after onboarding: `upsert_experiment` on the existing keys (`level.3.enemyHp`, …) with a hypothesis such as "Lower HP on level 3 increases session duration", `primaryMetric: 'session.time_ms'`, `
|
|
593
|
+
From MCP, after onboarding: `upsert_experiment` on the existing keys (`level.3.enemyHp`, …) with a hypothesis such as "Lower HP on level 3 increases session duration", `primaryMetric: 'session.time_ms'`, `goalMetric: 'session.duration'`, `assignmentUnitKind: 'session'`, `outcomeKind: 'mean'`, and the complete fixed-horizon sample/time/alpha/effect/direction/health policy. Later `analyze_experiment` uses only trusted, server-deduplicated evidence; follow its persisted terminal `decision` and compare `goalMean`. `ship_experiment` additionally requires the current `expectedVersion` and a reason. Compare current funnel counts with `get_aggregates` and completed baselines with `get_aggregate_history`.
|
|
583
594
|
|
|
584
595
|
## Use case 16: Surface an error so an agent can open the source
|
|
585
596
|
|
|
@@ -629,13 +640,13 @@ The log ring is recent only (`recentLogsMax`). It is not a history search. See `
|
|
|
629
640
|
| `timer(name, dims)` | Starts a timer. The returned function records milliseconds. |
|
|
630
641
|
| `event(name, attrs)` | Buffers a product event. |
|
|
631
642
|
| `log.debug\|info\|warn\|error(message, attrs)` | Buffers a structured log. |
|
|
632
|
-
| `identify(subjectId)` | Sets the default subject for this instance. `identify(null)` clears it.
|
|
643
|
+
| `identify(subjectId)` | Sets the default subject for this SDK instance. `identify(null)` clears it. Do not share that default across users on a multi-user server. |
|
|
633
644
|
| `config.get(key, fallback, context)` | Reads Remote Config. Uses `identify()` or `{ subjectId }`. A per-call `{ subjectId }` overrides `identify()`. Omit both for the shared value. |
|
|
634
645
|
| `experiment.goal(name, context)` | Emits `experiment.goal`. Needs a subject from `identify()` or `{ subjectId }`. Optional `value` for a quantitative goal such as session duration. |
|
|
635
646
|
| `flush()` | Sends pending frames now. Returns a Promise. |
|
|
636
647
|
| `shutdown()` | Stops timers, sends pending frames, and closes the HTTP agent. |
|
|
637
648
|
|
|
638
|
-
|
|
649
|
+
Each `createWardx()` SDK instance creates its own `instanceId` and `sessionId`. The IDs are ULIDs; they are not process-wide singletons or subject/journey keys.
|
|
639
650
|
|
|
640
651
|
Sync delay is `syncIntervalMs * random(syncJitterMin, syncJitterMax)`. The default interval is 15 seconds. The default jitter is 0.85 to 1.15.
|
|
641
652
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wardx",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.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.
|
|
40
|
+
"@wardx/core": "0.4.0"
|
|
41
41
|
}
|
|
42
|
-
}
|
|
42
|
+
}
|
package/src/WardxNode.js
CHANGED
|
@@ -23,6 +23,7 @@ export class WardxNode {
|
|
|
23
23
|
this._core = new WardxCore(settings);
|
|
24
24
|
this._transport = createHttpTransport(settings);
|
|
25
25
|
this._stopped = false;
|
|
26
|
+
this._shutdownPromise = null;
|
|
26
27
|
this._syncChain = Promise.resolve();
|
|
27
28
|
this._instanceId = ulid();
|
|
28
29
|
this._sessionId = ulid();
|
|
@@ -58,6 +59,10 @@ export class WardxNode {
|
|
|
58
59
|
return this._core.histogram(name, a, b);
|
|
59
60
|
}
|
|
60
61
|
|
|
62
|
+
distinct(name, dims) {
|
|
63
|
+
return this._core.distinct(name, dims);
|
|
64
|
+
}
|
|
65
|
+
|
|
61
66
|
timer(name, dims) {
|
|
62
67
|
return this._core.timer(name, dims);
|
|
63
68
|
}
|
|
@@ -70,8 +75,12 @@ export class WardxNode {
|
|
|
70
75
|
return this._enqueueSync({ flush: true });
|
|
71
76
|
}
|
|
72
77
|
|
|
73
|
-
|
|
74
|
-
if (this.
|
|
78
|
+
shutdown() {
|
|
79
|
+
if (this._shutdownPromise === null) this._shutdownPromise = this._shutdown();
|
|
80
|
+
return this._shutdownPromise;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async _shutdown() {
|
|
75
84
|
this._stopped = true;
|
|
76
85
|
clearInterval(this._aggregateTimer);
|
|
77
86
|
if (this._syncTimer) clearTimeout(this._syncTimer);
|
package/src/index.d.ts
CHANGED
|
@@ -7,6 +7,7 @@ import type {
|
|
|
7
7
|
CreateWardxOptions,
|
|
8
8
|
DimensionValue,
|
|
9
9
|
Dimensions,
|
|
10
|
+
DistinctHandle,
|
|
10
11
|
EventTraceRecord,
|
|
11
12
|
Experiment,
|
|
12
13
|
ExperimentGoalContext,
|
|
@@ -36,6 +37,7 @@ export type {
|
|
|
36
37
|
CreateWardxOptions,
|
|
37
38
|
DimensionValue,
|
|
38
39
|
Dimensions,
|
|
40
|
+
DistinctHandle,
|
|
39
41
|
EventTraceRecord,
|
|
40
42
|
Experiment,
|
|
41
43
|
ExperimentGoalContext,
|
|
@@ -78,6 +80,7 @@ export class WardxNode {
|
|
|
78
80
|
counter(name: string, dims?: Dimensions | null): CounterHandle;
|
|
79
81
|
gauge(name: string, dims?: Dimensions | null): GaugeHandle;
|
|
80
82
|
histogram(name: string, a?: HistogramOptions | null, b?: HistogramOptions | null): HistogramHandle;
|
|
83
|
+
distinct(name: string, dims?: Dimensions | null): DistinctHandle;
|
|
81
84
|
timer(name: string, dims?: Dimensions | null): StopTimer;
|
|
82
85
|
event(name: string, attrs?: Attrs | null): void;
|
|
83
86
|
flush(): Promise<void>;
|