wardx 0.8.0 → 0.9.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
@@ -522,7 +522,15 @@ If there is no identified subject and you omit `{ subjectId }`, that call is not
522
522
  2. If there is no subject (`identify` unset and no `{ subjectId }`), return the Remote Config value.
523
523
  3. If an experiment applies to the subject, return the variant value.
524
524
 
525
- The SDK updates the snapshot when a sync response contains a newer `configVersion`. Until that sync, `config.get` returns the fallback or the last snapshot.
525
+ The server first filters keys and experiments by role, then resolves any conditional base values against the instance's metadata and attributes. An applicable experiment still overrides that resolved base. Attributes do not change experiment eligibility, allocation, assignment, or exposure tracking.
526
+
527
+ SDK 0.9.0 requires `@wardx/server` 0.9.0 or newer. Upgrade the server first: older servers reject the new client fields even when attributes are empty.
528
+
529
+ Pass an optional flat `attributes` object to `createWardx`, for example `attributes: { os: 'android', build: 119, channel: 'stable' }`. Names are application-defined; values must be strings, finite numbers, or booleans. Wardx's `platform` identifies the SDK runtime (`node`, `csharp`, or `unity`), so use a custom attribute for the operating system. Attributes are shared by the SDK instance, not set per `config.get` subject; do not use an instance's attributes to switch between concurrent users.
530
+
531
+ `wardx.setAttributes({ os: 'android', build: 120 })` replaces the entire attribute map with a copy. Use `{}` to clear it. The next successful sync resolves the new context; `await wardx.flush()` requests a sync now. Reads remain local and use the last snapshot until then. Attributes travel as client metadata; send only non-secret values.
532
+
533
+ The SDK updates the snapshot when the project version or the resolved configuration changes. Every successful server response includes an opaque `configContext` token, which the SDK returns on subsequent syncs. The same contract applies with or without rules, allowing context changes to refresh values at the same `configVersion`. Until a successful sync, `config.get` returns the fallback or the last snapshot.
526
534
 
527
535
  ## Use case 10: Run an A/B experiment and record a goal
528
536
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wardx",
3
- "version": "0.8.0",
3
+ "version": "0.9.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.8.0"
40
+ "@wardx/core": "0.9.0"
41
41
  }
42
42
  }
package/src/WardxNode.js CHANGED
@@ -18,6 +18,19 @@ const pkg = JSON.parse(
18
18
  readFileSync(join(dirname(fileURLToPath(import.meta.url)), '..', 'package.json'), 'utf8')
19
19
  );
20
20
 
21
+ function copyAttributes(attributes) {
22
+ if (!attributes || typeof attributes !== 'object' || Array.isArray(attributes)) {
23
+ throw new Error('attributes must be an object');
24
+ }
25
+ for (const [key, value] of Object.entries(attributes)) {
26
+ if (key.length === 0) throw new Error('attributes keys must be non-empty strings');
27
+ if (typeof value !== 'string' && typeof value !== 'boolean' && !(typeof value === 'number' && Number.isFinite(value))) {
28
+ throw new Error(`attributes.${key} must be a string, finite number, or boolean`);
29
+ }
30
+ }
31
+ return Object.fromEntries(Object.entries(attributes));
32
+ }
33
+
21
34
  export class WardxNode {
22
35
  constructor(settings) {
23
36
  this.settings = settings;
@@ -31,6 +44,8 @@ export class WardxNode {
31
44
  goal: (name, context) => this._core.experimentGoal(name, context)
32
45
  };
33
46
  if (this._disabled) return;
47
+ this._attributes = copyAttributes(settings.attributes === undefined ? {} : settings.attributes);
48
+ this._configContext = undefined;
34
49
  this._transport = createHttpTransport(settings);
35
50
  this._stopped = false;
36
51
  this._shutdownPromise = null;
@@ -54,6 +69,11 @@ export class WardxNode {
54
69
  this._core.identify(subjectId);
55
70
  }
56
71
 
72
+ setAttributes(attributes) {
73
+ if (this._disabled) return;
74
+ this._attributes = copyAttributes(attributes);
75
+ }
76
+
57
77
  counter(name, dims) {
58
78
  return this._core.counter(name, dims);
59
79
  }
@@ -126,7 +146,7 @@ export class WardxNode {
126
146
  this._core.snapshotIfDirty();
127
147
  }
128
148
  const frames = this._core.takePendingFrames();
129
- if (!flags.bootstrap && frames.length === 0) return;
149
+ if (!flags.bootstrap && !flags.flush && frames.length === 0) return;
130
150
  const envelope = {
131
151
  protocol: PROTOCOL_VERSION,
132
152
  project: this.settings.project,
@@ -140,9 +160,11 @@ export class WardxNode {
140
160
  role: this.settings.role,
141
161
  appVersion: this.settings.appVersion,
142
162
  environment: this.settings.environment,
143
- platform: PLATFORM
163
+ platform: PLATFORM,
164
+ attributes: this._attributes
144
165
  },
145
166
  configVersion: this._core.configStore.version,
167
+ configContext: this._configContext,
146
168
  frames
147
169
  };
148
170
  const json = JSON.stringify(envelope);
@@ -210,6 +232,7 @@ export class WardxNode {
210
232
  }
211
233
  if (json.config) {
212
234
  this._core.applyConfig(json.configVersion, json.config);
235
+ this._configContext = json.configContext;
213
236
  }
214
237
  }
215
238
  }
package/src/index.d.ts CHANGED
@@ -80,6 +80,7 @@ export class WardxNode {
80
80
  constructor(settings: ResolvedSettings | DisabledWardxOptions);
81
81
  retentionActivity(userId: string): void;
82
82
  identify(subjectId: string | null | undefined): void;
83
+ setAttributes(attributes: Record<string, string | number | boolean>): void;
83
84
  counter(name: string, dims?: Dimensions | null): CounterHandle;
84
85
  gauge(name: string, dims?: Dimensions | null): GaugeHandle;
85
86
  histogram(name: string, a?: HistogramOptions | null, b?: HistogramOptions | null): HistogramHandle;