wardx 0.1.6 → 0.2.2
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 +19 -47
- package/package.json +3 -3
- package/src/WardxNode.js +7 -2
package/README.md
CHANGED
|
@@ -2,47 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
`wardx` is the Node.js SDK for Wardx.
|
|
4
4
|
|
|
5
|
-
This SDK talks to that server. See [Wardx](https://github.com/clasen/Wardx).
|
|
5
|
+
This SDK talks to that server. See [Wardx](https://github.com/clasen/Wardx). Architecture: [docs/ARCHITECTURE.md](../../docs/ARCHITECTURE.md).
|
|
6
6
|
|
|
7
7
|
The SDK records logs, events, and metrics. The SDK also gets Remote Config and assigns experiment variants.
|
|
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
|
-
```text
|
|
12
|
-
AGENT
|
|
13
|
-
arisa.sh / Codex / Claude
|
|
14
|
-
│
|
|
15
|
-
MCP stdio
|
|
16
|
-
tools + wardx://project/{name}
|
|
17
|
-
▼
|
|
18
|
-
┌───────────────────────────────────────────────────┐
|
|
19
|
-
│ wardx-server (one process) │
|
|
20
|
-
│ N isolated projects │
|
|
21
|
-
│ │
|
|
22
|
-
│ MCP ──► ControlService │
|
|
23
|
-
│ ├── Remote Config snapshot │
|
|
24
|
-
│ ├── Experiment definitions │
|
|
25
|
-
│ ├── Aggregates │
|
|
26
|
-
│ ├── Recent logs │
|
|
27
|
-
│ └── Catalog │
|
|
28
|
-
│ │
|
|
29
|
-
│ HTTP POST /v1/sync │
|
|
30
|
-
│ ├── envelope store (config.sink) │
|
|
31
|
-
│ │ null | memory | ndjson │
|
|
32
|
-
│ └── per-project ingest │
|
|
33
|
-
│ aggregator, recent logs, clients │
|
|
34
|
-
│ config reply filtered by client.role │
|
|
35
|
-
└─────────────────────────▲─────────────────────────┘
|
|
36
|
-
│
|
|
37
|
-
frames up / that role's config down
|
|
38
|
-
┌───────────────┴───────────────┐
|
|
39
|
-
▼ ▼
|
|
40
|
-
Node SDK C# / Unity SDK
|
|
41
|
-
wardx / @wardx/core clients/csharp
|
|
42
|
-
role: game-server role: mobile
|
|
43
|
-
metrics / config.get same /v1/sync
|
|
44
|
-
```
|
|
45
|
-
|
|
46
11
|
## Install
|
|
47
12
|
|
|
48
13
|
```bash
|
|
@@ -67,8 +32,10 @@ To receive frames, run an ingest server. Install `@wardx/server` and start it wi
|
|
|
67
32
|
- A measure call does not send network data.
|
|
68
33
|
- A measure call does not wait for a Promise.
|
|
69
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`.
|
|
70
36
|
- The application has priority over telemetry.
|
|
71
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.
|
|
72
39
|
- The SDK sends names only. Descriptions live in the server catalog: ship them in the config file, or fill them during MCP onboarding.
|
|
73
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()`.
|
|
74
41
|
|
|
@@ -83,11 +50,12 @@ To receive frames, run an ingest server. Install `@wardx/server` and start it wi
|
|
|
83
50
|
| `endpoint` | Base URL of the ingest server, for example `http://127.0.0.1:8787`. |
|
|
84
51
|
| `projectKey` | Value of header `X-Wardx-Key`. |
|
|
85
52
|
| `project` | Project name. The name must match the server mapping. |
|
|
86
|
-
| `role` |
|
|
53
|
+
| `role` | Routing name of this instance inside the project, for example `client`, `unity`, `game-server`, `desktop`. Not `*`; not an authorization boundary. |
|
|
87
54
|
| `appVersion` | Application version. |
|
|
88
55
|
| `environment` | Environment name. |
|
|
56
|
+
| `privacySalt` | Required stable, project-specific salt for one-way subject hashes. |
|
|
89
57
|
|
|
90
|
-
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.
|
|
91
59
|
|
|
92
60
|
The SDK starts a bootstrap sync immediately. The SDK then syncs on `syncIntervalMs` with jitter.
|
|
93
61
|
|
|
@@ -106,7 +74,8 @@ const wardx = createWardx({
|
|
|
106
74
|
project: 'demo',
|
|
107
75
|
role: 'client',
|
|
108
76
|
appVersion: '2.4.1',
|
|
109
|
-
environment: 'production'
|
|
77
|
+
environment: 'production',
|
|
78
|
+
privacySalt: 'demo-subject-hash-v1'
|
|
110
79
|
});
|
|
111
80
|
|
|
112
81
|
wardx.log.info('match_started', { mode: 'ranked', players: 4 });
|
|
@@ -423,7 +392,7 @@ The SDK updates the snapshot when a sync response contains a newer `configVersio
|
|
|
423
392
|
|
|
424
393
|
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.
|
|
425
394
|
|
|
426
|
-
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
|
|
395
|
+
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.
|
|
427
396
|
|
|
428
397
|
```js
|
|
429
398
|
wardx.identify(userId);
|
|
@@ -454,7 +423,7 @@ The first `config.get` that has a subject in a session can emit event `experimen
|
|
|
454
423
|
|
|
455
424
|
The payload does not contain the raw `subjectId`.
|
|
456
425
|
|
|
457
|
-
`experiment.goal` needs a subject: from `identify()` or from `{ subjectId }` on that call.
|
|
426
|
+
`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.
|
|
458
427
|
|
|
459
428
|
## Use case 11: Continue when the ingest server is down
|
|
460
429
|
|
|
@@ -469,7 +438,8 @@ const wardx = createWardx({
|
|
|
469
438
|
project: 'demo',
|
|
470
439
|
role: 'client',
|
|
471
440
|
appVersion: '0.1.0',
|
|
472
|
-
environment: 'development'
|
|
441
|
+
environment: 'development',
|
|
442
|
+
privacySalt: 'demo-subject-hash-v1'
|
|
473
443
|
});
|
|
474
444
|
|
|
475
445
|
wardx.counter('jobs.completed').inc();
|
|
@@ -493,7 +463,8 @@ const wardx = createWardx({
|
|
|
493
463
|
project: 'demo',
|
|
494
464
|
role: 'client',
|
|
495
465
|
appVersion: '0.1.0',
|
|
496
|
-
environment: 'production'
|
|
466
|
+
environment: 'production',
|
|
467
|
+
privacySalt: 'demo-subject-hash-v1'
|
|
497
468
|
});
|
|
498
469
|
|
|
499
470
|
async function onStop() {
|
|
@@ -505,7 +476,7 @@ process.on('SIGTERM', onStop);
|
|
|
505
476
|
process.on('SIGINT', onStop);
|
|
506
477
|
```
|
|
507
478
|
|
|
508
|
-
`shutdown` is safe to call more than
|
|
479
|
+
`shutdown` is safe to call more than once. Concurrent callers receive the same promise and all await the final flush and single transport close.
|
|
509
480
|
|
|
510
481
|
`flush` sends the current pending frames and does not stop the timers. Use `shutdown` when the process stops.
|
|
511
482
|
|
|
@@ -525,6 +496,7 @@ const wardx = createWardx({
|
|
|
525
496
|
role: 'client',
|
|
526
497
|
appVersion: '0.1.0',
|
|
527
498
|
environment: 'development',
|
|
499
|
+
privacySalt: 'demo-subject-hash-v1',
|
|
528
500
|
tracer: createConsoleTracer()
|
|
529
501
|
});
|
|
530
502
|
```
|
|
@@ -614,7 +586,7 @@ The volume funnel `level.start` → `level.fail` / `level.complete` is the diffi
|
|
|
614
586
|
|
|
615
587
|
Call `experiment.goal('session.duration', { value: durationMs })` when the play session ends (use case 14).
|
|
616
588
|
|
|
617
|
-
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'`, `
|
|
589
|
+
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`.
|
|
618
590
|
|
|
619
591
|
## Use case 16: Surface an error so an agent can open the source
|
|
620
592
|
|
|
@@ -664,13 +636,13 @@ The log ring is recent only (`recentLogsMax`). It is not a history search. See `
|
|
|
664
636
|
| `timer(name, dims)` | Starts a timer. The returned function records milliseconds. |
|
|
665
637
|
| `event(name, attrs)` | Buffers a product event. |
|
|
666
638
|
| `log.debug\|info\|warn\|error(message, attrs)` | Buffers a structured log. |
|
|
667
|
-
| `identify(subjectId)` | Sets the default subject for this instance. `identify(null)` clears it.
|
|
639
|
+
| `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. |
|
|
668
640
|
| `config.get(key, fallback, context)` | Reads Remote Config. Uses `identify()` or `{ subjectId }`. A per-call `{ subjectId }` overrides `identify()`. Omit both for the shared value. |
|
|
669
641
|
| `experiment.goal(name, context)` | Emits `experiment.goal`. Needs a subject from `identify()` or `{ subjectId }`. Optional `value` for a quantitative goal such as session duration. |
|
|
670
642
|
| `flush()` | Sends pending frames now. Returns a Promise. |
|
|
671
643
|
| `shutdown()` | Stops timers, sends pending frames, and closes the HTTP agent. |
|
|
672
644
|
|
|
673
|
-
|
|
645
|
+
Each `createWardx()` SDK instance creates its own `instanceId` and `sessionId`. The IDs are ULIDs; they are not process-wide singletons or subject/journey keys.
|
|
674
646
|
|
|
675
647
|
Sync delay is `syncIntervalMs * random(syncJitterMin, syncJitterMax)`. The default interval is 15 seconds. The default jitter is 0.85 to 1.15.
|
|
676
648
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wardx",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.2",
|
|
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.2.2"
|
|
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();
|
|
@@ -70,8 +71,12 @@ export class WardxNode {
|
|
|
70
71
|
return this._enqueueSync({ flush: true });
|
|
71
72
|
}
|
|
72
73
|
|
|
73
|
-
|
|
74
|
-
if (this.
|
|
74
|
+
shutdown() {
|
|
75
|
+
if (this._shutdownPromise === null) this._shutdownPromise = this._shutdown();
|
|
76
|
+
return this._shutdownPromise;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async _shutdown() {
|
|
75
80
|
this._stopped = true;
|
|
76
81
|
clearInterval(this._aggregateTimer);
|
|
77
82
|
if (this._syncTimer) clearTimeout(this._syncTimer);
|