@wardx/core 0.1.2 → 0.1.3
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 +14 -8
- package/package.json +1 -1
- package/src/WardxCore.js +27 -8
package/README.md
CHANGED
|
@@ -27,6 +27,7 @@ import { WardxCore, assignVariant, loadSdkDefaults } from '@wardx/core';
|
|
|
27
27
|
- A measure call does not wait for a Promise.
|
|
28
28
|
- If a buffer is full, the engine discards data. The engine does not block the application.
|
|
29
29
|
- Counters in a frame are window deltas. Counters are not lifetime totals.
|
|
30
|
+
- `identify(subjectId)` sets the default subject for this instance. A per-call `{ subjectId }` overrides it.
|
|
30
31
|
|
|
31
32
|
## Settings
|
|
32
33
|
|
|
@@ -125,7 +126,7 @@ A dimension value must be a string, a number, or a boolean.
|
|
|
125
126
|
```js
|
|
126
127
|
core.event('purchase', { product: 'premium' });
|
|
127
128
|
core.log.info('match_started', { mode: 'ranked', players: 4 });
|
|
128
|
-
core.log.error('payment_failed', { code: 'timeout' });
|
|
129
|
+
core.log.error('payment_failed', { code: 'timeout', stack: 'PaymentError: timeout' });
|
|
129
130
|
```
|
|
130
131
|
|
|
131
132
|
Log levels: `debug`, `info`, `warn`, `error`.
|
|
@@ -138,7 +139,7 @@ Log levels: `debug`, `info`, `warn`, `error`.
|
|
|
138
139
|
|
|
139
140
|
Dropped items increment `wardx.internal.events_dropped` or `wardx.internal.logs_dropped`.
|
|
140
141
|
|
|
141
|
-
Use counters and histograms for rates and latency. Use events for rare product facts: a purchase, an experiment exposure or goal, a named screen. Use logs for failures. Do not put user ids on metric dimensions.
|
|
142
|
+
Use counters and histograms for rates and latency. Use events for rare product facts: a purchase, an experiment exposure or goal, a named screen. Use logs for failures. Put a clipped `stack` or a provider `code` on the log attrs so MCP `get_recent_logs` can show an agent where to look. The engine does not edit source. Do not put user ids on metric dimensions.
|
|
142
143
|
|
|
143
144
|
A volume funnel is one event name and one counter per step. The engine does not join events by subject. See `docs/ARCHITECTURE.md`.
|
|
144
145
|
|
|
@@ -148,6 +149,8 @@ A volume funnel is one event name and one counter per step. The engine does not
|
|
|
148
149
|
|
|
149
150
|
**Objective:** Get a config value. If an experiment applies, get the variant value.
|
|
150
151
|
|
|
152
|
+
There is a default subject after `identify(subjectId)`. Pass `{ subjectId }` on a call to override it, or when one process serves many users. Use a stable account id, not `sessionId`. With no subject, `configGet` returns the Remote Config value and that call is not in the A/B test.
|
|
153
|
+
|
|
151
154
|
```js
|
|
152
155
|
core.applyConfig(13, {
|
|
153
156
|
values: {
|
|
@@ -170,23 +173,26 @@ core.applyConfig(13, {
|
|
|
170
173
|
});
|
|
171
174
|
|
|
172
175
|
const fallback = 1000;
|
|
173
|
-
const
|
|
174
|
-
core.
|
|
176
|
+
const shared = core.configGet('message.delayMs', fallback);
|
|
177
|
+
core.identify('user-1');
|
|
178
|
+
const delay = core.configGet('message.delayMs', fallback);
|
|
179
|
+
core.experimentGoal('message.sent', { value: 1 });
|
|
180
|
+
const other = core.configGet('message.delayMs', fallback, { subjectId: 'user-2' });
|
|
175
181
|
```
|
|
176
182
|
|
|
183
|
+
`shared` is always the snapshot value (`1000`). `delay` is `1000` or `400` for `user-1`. `other` is the variant for `user-2`. The same `subjectId`, experiment `id`, and `salt` always map to the same variant. You do not persist the group. Changing `salt` redistributes the population. `identify(null)` clears the default.
|
|
184
|
+
|
|
177
185
|
### Resolution order
|
|
178
186
|
|
|
179
187
|
1. If the key is not in the snapshot, return `fallback`.
|
|
180
|
-
2. If `
|
|
188
|
+
2. If there is no subject (`identify` unset and no `{ subjectId }`), return the Remote Config value.
|
|
181
189
|
3. If no enabled experiment contains the key, return the Remote Config value.
|
|
182
190
|
4. If the subject is not in the allocation, return the Remote Config value.
|
|
183
191
|
5. If the subject is in the allocation, return the variant value.
|
|
184
192
|
|
|
185
|
-
The assignment is deterministic. The same `experimentId`, `subjectId`, and `salt` always give the same variant.
|
|
186
|
-
|
|
187
193
|
The first resolve for a subject in a session emits event `experiment.exposure`. The payload contains a hashed subject. The payload does not contain the raw `subjectId`.
|
|
188
194
|
|
|
189
|
-
`experimentGoal` emits event `experiment.goal`.
|
|
195
|
+
`experimentGoal` emits event `experiment.goal`. The subject comes from `identify()` or from `{ subjectId }` on that call. You can supply `value`. Use milliseconds for a session-duration goal. The server stores that number as `goalSum` and `goalMean` per variant. Without a subject, the call throws. One experiment should have one quantitative goal name.
|
|
190
196
|
|
|
191
197
|
## Use case 4: Assign a variant without WardxCore
|
|
192
198
|
|
package/package.json
CHANGED
package/src/WardxCore.js
CHANGED
|
@@ -40,6 +40,7 @@ export class WardxCore {
|
|
|
40
40
|
this.seq = 0;
|
|
41
41
|
this.pendingFrames = [];
|
|
42
42
|
this.windowStart = Date.now();
|
|
43
|
+
this._subjectId = null;
|
|
43
44
|
this.log = {
|
|
44
45
|
debug: (message, attrs) => this._log('debug', message, attrs),
|
|
45
46
|
info: (message, attrs) => this._log('info', message, attrs),
|
|
@@ -96,30 +97,48 @@ export class WardxCore {
|
|
|
96
97
|
return wrapped;
|
|
97
98
|
}
|
|
98
99
|
|
|
100
|
+
identify(subjectId) {
|
|
101
|
+
if (subjectId === undefined || subjectId === null) {
|
|
102
|
+
this._subjectId = null;
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
if (typeof subjectId !== 'string' || subjectId.length === 0) {
|
|
106
|
+
throw new Error('identify requires a non-empty subjectId');
|
|
107
|
+
}
|
|
108
|
+
this._subjectId = subjectId;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
_subjectIdFrom(context) {
|
|
112
|
+
if (context && context.subjectId !== undefined && context.subjectId !== null) {
|
|
113
|
+
return context.subjectId;
|
|
114
|
+
}
|
|
115
|
+
return this._subjectId;
|
|
116
|
+
}
|
|
117
|
+
|
|
99
118
|
configGet(key, fallback, context) {
|
|
100
119
|
if (!this.configStore.has(key)) return fallback;
|
|
101
120
|
const remote = this.configStore.getRaw(key);
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
}
|
|
121
|
+
const subjectId = this._subjectIdFrom(context);
|
|
122
|
+
if (subjectId === undefined || subjectId === null) return remote;
|
|
105
123
|
return this.experiments.resolve(
|
|
106
124
|
key,
|
|
107
125
|
remote,
|
|
108
|
-
|
|
126
|
+
subjectId,
|
|
109
127
|
this.configStore.experimentsByKey
|
|
110
128
|
);
|
|
111
129
|
}
|
|
112
130
|
|
|
113
131
|
experimentGoal(name, context) {
|
|
114
|
-
|
|
132
|
+
const subjectId = this._subjectIdFrom(context);
|
|
133
|
+
if (subjectId === undefined || subjectId === null) {
|
|
115
134
|
throw new Error('experiment.goal requires subjectId');
|
|
116
135
|
}
|
|
117
136
|
if (typeof name !== 'string' || name.length === 0) {
|
|
118
137
|
throw new Error('experiment.goal requires a metric name');
|
|
119
138
|
}
|
|
120
|
-
const subject = this.experiments.hashSubject(
|
|
139
|
+
const subject = this.experiments.hashSubject(subjectId);
|
|
121
140
|
const experiments = this.experiments.relevantExperiments(
|
|
122
|
-
|
|
141
|
+
subjectId,
|
|
123
142
|
this.configStore.experiments
|
|
124
143
|
);
|
|
125
144
|
const payload = {
|
|
@@ -127,7 +146,7 @@ export class WardxCore {
|
|
|
127
146
|
subject,
|
|
128
147
|
experiments
|
|
129
148
|
};
|
|
130
|
-
if (context.value !== undefined) payload.value = context.value;
|
|
149
|
+
if (context && context.value !== undefined) payload.value = context.value;
|
|
131
150
|
this.event('experiment.goal', payload);
|
|
132
151
|
}
|
|
133
152
|
|