@drip-apex/app-sdk 0.0.0-stage → 0.1.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 +400 -2
- package/dist/index.cjs +1217 -0
- package/dist/index.d.cts +200 -0
- package/dist/index.d.ts +200 -0
- package/dist/index.js +1194 -0
- package/package.json +27 -4
package/README.md
CHANGED
|
@@ -1,3 +1,401 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Apex app SDK
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
`@drip-apex/app-sdk` adds server-assigned flags, exposures, goals and purchases to
|
|
4
|
+
hybrid apps built with Ionic, Angular, Capacitor or Cordova. It has zero runtime
|
|
5
|
+
dependencies and uses WebView APIs: fetch, timers, Web Crypto, localStorage and
|
|
6
|
+
visibility events. It does not change the DOM or load the storefront SDK.
|
|
7
|
+
|
|
8
|
+
```sh
|
|
9
|
+
npm install @drip-apex/app-sdk
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
For Cordova, allow the events host in your app's CSP **before testing**. Without
|
|
13
|
+
`connect-src https://events.drip-apex.com`, every SDK request fails with a network
|
|
14
|
+
error. Preserve any other origins your app needs:
|
|
15
|
+
|
|
16
|
+
```html
|
|
17
|
+
<meta http-equiv="Content-Security-Policy"
|
|
18
|
+
content="default-src 'self'; connect-src 'self' https://events.drip-apex.com">
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Cordova iOS apps need `cordova-ios` 8+ to build with current Xcode. Older shells
|
|
22
|
+
lack UIScene support and can crash at launch on current iOS; this is independent
|
|
23
|
+
of Apex. When reinstalling the same SDK version, clear Angular's `.angular/cache`
|
|
24
|
+
before rebuilding so the app loads the updated package.
|
|
25
|
+
|
|
26
|
+
Configure once at app initialization. Create a **publishable** `apx_pk_…` key in
|
|
27
|
+
Apex **Settings → Developers**. Never put a private API key or shop secret in the
|
|
28
|
+
app. Await configure for storage hydration and the first assignment fetch to settle.
|
|
29
|
+
`initTimeoutMs` (default 3,000 ms) bounds the wait for that fetch; initialization
|
|
30
|
+
never rejects. Reads before resolution use defaults or cached assignments, with
|
|
31
|
+
`variation(key).reason` reporting `default`, `cached`, or `stale` as appropriate.
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
import { Apex } from '@drip-apex/app-sdk';
|
|
35
|
+
|
|
36
|
+
await Apex.configure({
|
|
37
|
+
publishableKey: 'apx_pk_live_…',
|
|
38
|
+
shopId: 'YOUR_SHOP_UUID',
|
|
39
|
+
applicationId: 'ch.coop.supercard',
|
|
40
|
+
appVersion: '1.0.0',
|
|
41
|
+
screen: () => 'home',
|
|
42
|
+
qaMode: false,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
await Apex.identify('app-user-id'); // Wait for this user's assignments (or initTimeoutMs).
|
|
46
|
+
const enabled = Apex.flag('welcome', false); // New user's value after a successful load.
|
|
47
|
+
// Use await Apex.identify(null) on logout to restore the device UUID.
|
|
48
|
+
await Apex.track('signup_completed');
|
|
49
|
+
await Apex.trackPurchase('order-42', 42.5, 'CHF');
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Attributes passed to `identify(userId, attributes)` are reserved for a future
|
|
53
|
+
contract and are not sent in v1. `identify()` never rejects and resolves when the
|
|
54
|
+
new identity's assignments load or `initTimeoutMs` elapses. Await it before reading
|
|
55
|
+
flags for the new user. Reads before the matching assignments load return the
|
|
56
|
+
previous user's values **without counting exposures**, including reads immediately
|
|
57
|
+
after calling identify. `logExposure` is also deferred during this period; it does
|
|
58
|
+
not replay an old assignment's exposure. Read the flag or call `logExposure` again
|
|
59
|
+
after the new assignments load to count them normally. If the wait times out,
|
|
60
|
+
previous values remain uncounted until a matching response succeeds. A later
|
|
61
|
+
identify supersedes an earlier response. The identified user is persisted per
|
|
62
|
+
shop/environment. On a cold launch, the SDK restores that user's cached
|
|
63
|
+
assignments immediately, including their exposure and purchase attribution,
|
|
64
|
+
before waiting for the network. Without a saved user it restores the anonymous
|
|
65
|
+
device cache. `identify(null)` clears the saved user for the next launch. A new
|
|
66
|
+
in-session identify still serves the previous assignments until its fetch succeeds.
|
|
67
|
+
Purchases during this pending period keep the existing attribution rule: the base
|
|
68
|
+
purchase uses the new current identity, while experiment copies use the previous
|
|
69
|
+
served assignment and its identity. After the new assignments load, both use the
|
|
70
|
+
new identity.
|
|
71
|
+
|
|
72
|
+
## Angular service
|
|
73
|
+
|
|
74
|
+
Provide one service at the root, configure through Angular app initialization,
|
|
75
|
+
and inject that service into components. Change callbacks execute asynchronously;
|
|
76
|
+
update Angular state through your usual change detection or signals.
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
import { Injectable, NgZone, inject, provideAppInitializer } from '@angular/core';
|
|
80
|
+
import { Apex } from '@drip-apex/app-sdk';
|
|
81
|
+
|
|
82
|
+
@Injectable({ providedIn: 'root' })
|
|
83
|
+
export class ApexService {
|
|
84
|
+
private readonly zone = inject(NgZone);
|
|
85
|
+
init(): Promise<void> {
|
|
86
|
+
return this.zone.runOutsideAngular(() => Apex.configure({
|
|
87
|
+
publishableKey: 'apx_pk_live_…', shopId: 'YOUR_SHOP_UUID',
|
|
88
|
+
applicationId: 'ch.coop.supercard',
|
|
89
|
+
screen: () => 'home', // Replace with your router's route, excluding query/fragment.
|
|
90
|
+
}));
|
|
91
|
+
}
|
|
92
|
+
welcome() { return Apex.flag('welcome', false); }
|
|
93
|
+
subscribe(update: () => void) {
|
|
94
|
+
return Apex.onChange(change => {
|
|
95
|
+
if (change.type === 'changed') this.zone.run(update);
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Add this provider to ApplicationConfig.providers:
|
|
101
|
+
export const apexInitializer = provideAppInitializer(() => inject(ApexService).init());
|
|
102
|
+
|
|
103
|
+
// In a component:
|
|
104
|
+
// private readonly experiments = inject(ApexService);
|
|
105
|
+
// enabled = this.experiments.welcome();
|
|
106
|
+
// private readonly subscription = this.experiments.subscribe(() => {
|
|
107
|
+
// this.enabled = this.experiments.welcome();
|
|
108
|
+
// });
|
|
109
|
+
// ngOnDestroy() { this.subscription.cancel(); }
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
For apps that react to `onChange`, start configuration in `NgZone.runOutsideAngular`
|
|
113
|
+
so SDK timers and network completions do not trigger unnecessary change detection.
|
|
114
|
+
Re-enter with `zone.run` only to update UI, as the service above does. Older Angular
|
|
115
|
+
versions without `provideAppInitializer` can provide `APP_INITIALIZER` with
|
|
116
|
+
`multi: true` and a factory that captures `const service = inject(ApexService)`
|
|
117
|
+
synchronously and returns `() => service.init()`.
|
|
118
|
+
|
|
119
|
+
## What your developers code
|
|
120
|
+
|
|
121
|
+
- Read a flag where the app selects its content or code variant. Exposures are
|
|
122
|
+
automatic on compatible flag reads, deduped per experiment, variation and epoch
|
|
123
|
+
in a session. A missing epoch still exposes with `assignment_epoch: null`.
|
|
124
|
+
- Call `track("goal_name")` once at each goal action. Custom events are
|
|
125
|
+
unattributed; purchase attribution is handled separately.
|
|
126
|
+
- Call `trackPurchase(orderId, revenue, currency)` at order confirmation with a
|
|
127
|
+
stable order ID, a finite non-negative number in currency units and a
|
|
128
|
+
three-letter currency code. The SDK enqueues a base row plus one row for every
|
|
129
|
+
served assignment. These rows share an event ID and retain their IDs on retry.
|
|
130
|
+
- Call `identify(userId)` on login and `identify(null)` on logout.
|
|
131
|
+
- Set `qaMode: true` in test builds. Non-production environments are also QA.
|
|
132
|
+
Store builds use `environment: "production"` and `qaMode: false`.
|
|
133
|
+
- After configure, use `Apex.deviceId` for an anonymous QA registration or event
|
|
134
|
+
lookup. After login use `Apex.userId`. Register that identity with the flag's
|
|
135
|
+
QA devices in Apex to pin a variation. Use the trusted-terminal tracking
|
|
136
|
+
verify endpoint described in the [wire contract](../../docs/mobile-sdk/wire-contract.md#check-a-test-builds-events)
|
|
137
|
+
to inspect recent events. During identify transitions, also check the previous
|
|
138
|
+
assignment identity. The app's publishable key cannot read management APIs.
|
|
139
|
+
|
|
140
|
+
Content and configuration tests can use remote values. Layout and flow tests
|
|
141
|
+
still require both versions in your app code behind the flag. Apex cannot remove
|
|
142
|
+
that code from the app for you.
|
|
143
|
+
|
|
144
|
+
## Typed objects and manual exposures
|
|
145
|
+
|
|
146
|
+
```ts
|
|
147
|
+
type Welcome = { title: string; button: { label: string; enabled: boolean } };
|
|
148
|
+
const fallback: Welcome = { title: 'Welcome', button: { label: 'Start', enabled: false } };
|
|
149
|
+
const validate = (v: unknown): v is Welcome => {
|
|
150
|
+
if (!v || typeof v !== 'object') return false;
|
|
151
|
+
const x = v as Partial<Welcome>;
|
|
152
|
+
return typeof x.title === 'string' && typeof x.button?.label === 'string'
|
|
153
|
+
&& typeof x.button?.enabled === 'boolean';
|
|
154
|
+
};
|
|
155
|
+
const content = Apex.flagObject('welcome', fallback, { validate, exposure: 'manual' });
|
|
156
|
+
// When the content is visible:
|
|
157
|
+
Apex.logExposure('welcome');
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
A validator checks unknown JSON without throwing into app code. Without it,
|
|
161
|
+
`flagObject` checks the outer JSON type only; TypeScript types do not validate
|
|
162
|
+
nested fields at runtime. Returned values are copies. Unknown keys remain
|
|
163
|
+
present. Assigned object reads expose only after successful validation. Failed or
|
|
164
|
+
throwing validators return the default without exposure. This differs from the
|
|
165
|
+
native typed getter. Missing assignments do not expose. Primitive reads require
|
|
166
|
+
matching boolean/string/number types, finite numbers and safe integers; no string
|
|
167
|
+
coercion. Missing or incompatible reads return the default and log once per key
|
|
168
|
+
per session, only after an assignment set has loaded from cache or the network.
|
|
169
|
+
Reads before that first load use defaults without warnings.
|
|
170
|
+
`variation(key)` also exposes. A configured client returns a default variation
|
|
171
|
+
with `reason: "default"` when unassigned; the unconfigured facade returns null.
|
|
172
|
+
|
|
173
|
+
## Storage and lifecycle
|
|
174
|
+
|
|
175
|
+
The default adapter uses localStorage. To use Capacitor Preferences, supply an
|
|
176
|
+
adapter; Capacitor remains an app dependency, not an SDK dependency:
|
|
177
|
+
|
|
178
|
+
```ts
|
|
179
|
+
import { Preferences } from '@capacitor/preferences';
|
|
180
|
+
import { Apex, type ApexStorage } from '@drip-apex/app-sdk';
|
|
181
|
+
|
|
182
|
+
const storage: ApexStorage = {
|
|
183
|
+
async get(key) { return (await Preferences.get({ key })).value; },
|
|
184
|
+
async set(key, value) { await Preferences.set({ key, value }); },
|
|
185
|
+
};
|
|
186
|
+
await Apex.configure({ publishableKey: 'apx_pk_live_…', shopId: 'YOUR_SHOP_UUID', storage });
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
Adapters must atomically replace a string value or reject without changing it.
|
|
190
|
+
The SDK stages queued events in memory and coalesces bursts into the latest queue
|
|
191
|
+
snapshot, with at most one storage write in flight and one pending snapshot.
|
|
192
|
+
`track()` and `trackPurchase()` wait for the write containing their events before
|
|
193
|
+
resolving. A failed write restores the previous durable queue and logs the failure;
|
|
194
|
+
purchase groups remain atomic. If an identify identity write fails, the SDK restores
|
|
195
|
+
the previous in-memory identity, logs once that the identity could not be saved, and stops assignment
|
|
196
|
+
fetches and event sends until a later identify successfully saves its identity.
|
|
197
|
+
Queue/cache read failures log and use available defaults.
|
|
198
|
+
|
|
199
|
+
Assignments refresh on configure, identify, explicit `refresh()`, and foreground
|
|
200
|
+
transitions, matching the native SDKs. There is no periodic assignment polling;
|
|
201
|
+
60 seconds is the cached assignment freshness window, not a network interval.
|
|
202
|
+
Foreground refresh uses the configured jitter and respects retry deadlines.
|
|
203
|
+
Startup schedules delivery of a hydrated nonempty queue on the first tick.
|
|
204
|
+
Foreground transitions also flush a nonempty queue, respecting event retry deadlines.
|
|
205
|
+
|
|
206
|
+
The SDK listens to browser `visibilitychange`, Cordova document `pause`/`resume`,
|
|
207
|
+
and `globalThis.Capacitor?.Plugins?.App?.addListener('appStateChange', ...)` when
|
|
208
|
+
available. It does not import or require a shell plugin. If your Capacitor version
|
|
209
|
+
exposes App only as an imported module, forward its callback explicitly:
|
|
210
|
+
|
|
211
|
+
```ts
|
|
212
|
+
import { App } from '@capacitor/app';
|
|
213
|
+
App.addListener('appStateChange', ({ isActive }) => {
|
|
214
|
+
Apex.setAppState(isActive ? 'foreground' : 'background');
|
|
215
|
+
});
|
|
216
|
+
// Or call Apex.setAppState('background' / 'foreground') from your own shell hooks.
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
Repeated state signals and rapid overlay transitions coalesce within one second.
|
|
220
|
+
Backgrounding first completes any pending queue write, then flushes the persisted
|
|
221
|
+
queue with best-effort fetch keepalive if the UTF-8 request body fits 64 KiB; larger batches use normal fetch. Disk remains
|
|
222
|
+
authoritative until the server acknowledges a batch and the acknowledgement is
|
|
223
|
+
persisted. Thirty continuous minutes in the background rotate the session and
|
|
224
|
+
clear exposure/warning dedupe. Lifecycle listeners are removed on shutdown.
|
|
225
|
+
Every cold launch creates a new session ID and allows a new exposure for each
|
|
226
|
+
served assignment, even when relaunching within thirty minutes. This matches
|
|
227
|
+
both native clients: iOS initializes `sessionId` with `UUID().uuidString` and
|
|
228
|
+
Android with `UUID.randomUUID().toString()`; neither persists sessions across
|
|
229
|
+
launches. The thirty-minute rule applies within a running client.
|
|
230
|
+
|
|
231
|
+
`track`, `trackPurchase`, `refresh` and `flush` return promises for their local work,
|
|
232
|
+
not server delivery. `identify` additionally waits for matching assignments or
|
|
233
|
+
`initTimeoutMs`, without rejecting. `configure` resolves after storage hydration and
|
|
234
|
+
the first assignment fetch settles (including failures), or its configurable
|
|
235
|
+
3,000 ms fetch wait expires. A late response still updates flags. Use `onChange`
|
|
236
|
+
to observe changed keys or a fatal stop (`{ type: "stopped", code }`). Subscriptions
|
|
237
|
+
have an idempotent `cancel()`.
|
|
238
|
+
To use a separate context, construct `new ApexClient(options)` and await its
|
|
239
|
+
`ready` promise. Call `shutdown()` when retiring it. Do not run multiple clients
|
|
240
|
+
with the same storage and shop/environment namespace simultaneously.
|
|
241
|
+
|
|
242
|
+
## Delivery and options
|
|
243
|
+
|
|
244
|
+
| Option | Default / limit |
|
|
245
|
+
| --- | --- |
|
|
246
|
+
| `environment` | `production` |
|
|
247
|
+
| `qaMode` | `false`; other environments also stamp `qa_mode: true` |
|
|
248
|
+
| `flushIntervalSeconds` | 5; clamped to 0.1–86,400 |
|
|
249
|
+
| `batchSize` | 20; clamped to 1–20 |
|
|
250
|
+
| `queueCapacity` | 2,000; clamped to 1–2,000 |
|
|
251
|
+
| `queueRetentionHours` | 47; clamped to 0–47, below the 48-hour replay window |
|
|
252
|
+
| `refreshJitterSeconds` | `[0, 10]` |
|
|
253
|
+
| `initTimeoutMs` | 3,000; zero skips assignment waiting on configure and identify |
|
|
254
|
+
| `platform` | Detected `ios`, `android`, or `web`; explicit override takes precedence |
|
|
255
|
+
| `applicationId` / `appVersion` | `unknown` / empty string |
|
|
256
|
+
| `screen` | `home`; an event's string `properties.screen` takes precedence |
|
|
257
|
+
| `logger` | Optional; callback errors are isolated |
|
|
258
|
+
| `fetch` / `baseUrl` | WebView fetch / `https://events.drip-apex.com` |
|
|
259
|
+
|
|
260
|
+
Queue capacity evicts the oldest rows, dropping a whole older purchase group if
|
|
261
|
+
the cut would split it. A new purchase larger than capacity or 200 rows is dropped
|
|
262
|
+
whole with a diagnostic. Delivery keeps each purchase group in one request,
|
|
263
|
+
expanding the batch target when needed within the 200-event server request limit.
|
|
264
|
+
Expired rows are removed at enqueue/startup/flush. Future timestamps survive clock
|
|
265
|
+
rollback. Events use `app://<lowercase applicationId>/<encoded screen path>` with
|
|
266
|
+
no query or fragment. Platform detection uses Capacitor `getPlatform()`, then Cordova
|
|
267
|
+
`device.platform` (case-insensitive), then iPhone/iPad/iPod or Android user-agent
|
|
268
|
+
detection, finally `web`. Explicit `platform` overrides detection. The production
|
|
269
|
+
Worker already accepts all three values; no Worker change is required.
|
|
270
|
+
|
|
271
|
+
Fatal 401 codes `invalid_publishable_key` and `publishable_key_not_allowed` stop
|
|
272
|
+
both network lanes while reads retain values. Retryable responses respect
|
|
273
|
+
`Retry-After` seconds or HTTP dates. Missing lookup-unavailable retry headers
|
|
274
|
+
use five seconds; ordinary 429 responses use exponential backoff with jitter.
|
|
275
|
+
Other network/server failures use exponential 5/10/20/40/60-second
|
|
276
|
+
backoff with 0–20% positive jitter, capped at 60 seconds. Unknown 401/403 use
|
|
277
|
+
300 seconds; explicitly retryable assignment 4xx use 30 seconds. Other
|
|
278
|
+
non-retryable assignment 4xx end that request without automatic retries. Event
|
|
279
|
+
non-retryable 4xx drop the
|
|
280
|
+
batch, except 401/403/408/429 which are retained. Only HTTP 202 with `ok: true`
|
|
281
|
+
acknowledges events. A failed acknowledgement write retains the rows for replay.
|
|
282
|
+
Requests time out after 30 seconds and refuse redirects.
|
|
283
|
+
|
|
284
|
+
Supply `logger: line => ...` to receive developer diagnostics. Assignment fetch
|
|
285
|
+
and event delivery failures report network errors or HTTP status, plus whether
|
|
286
|
+
a retry is scheduled or the request/batch was dropped. Each lane and failure
|
|
287
|
+
class (network error or distinct HTTP status) logs at most once per sixty seconds
|
|
288
|
+
per client. Fatal auth logs its reason and that the SDK stopped; an obviously
|
|
289
|
+
malformed publishable-key prefix also logs at configure. Keys, Authorization
|
|
290
|
+
headers, response bodies and exception details are never included in these
|
|
291
|
+
diagnostics. Without a logger the SDK emits no logs, including to the console.
|
|
292
|
+
|
|
293
|
+
`track` rejects malformed revenue and property keys `email`, `phone`, `password`,
|
|
294
|
+
`ssn`, `credit_card`, `card_number` (case/diacritic insensitive). Numeric revenue
|
|
295
|
+
strings must match the Worker's strict decimal grammar, including comma decimals.
|
|
296
|
+
Purchases strip caller order/revenue/currency aliases before adding explicit
|
|
297
|
+
arguments. Properties must be JSON, including finite numbers.
|
|
298
|
+
|
|
299
|
+
Authentication uses **only** `Authorization: Bearer <publishableKey>` on assignment
|
|
300
|
+
and event requests. Cookie auth does not work from `capacitor://localhost`.
|
|
301
|
+
The SDK never sends `X-Apex-Publishable-Key`; that header is not allowed by the
|
|
302
|
+
production WebView CORS policy. No cookie credentials are needed.
|
|
303
|
+
|
|
304
|
+
Importing or configuring the SDK during SSR is safe: it logs once per context,
|
|
305
|
+
performs no storage/network work, and returns default flags. Configure again in
|
|
306
|
+
the browser to activate it.
|
|
307
|
+
|
|
308
|
+
Builds are ESM and CJS targeting ES2019, with declarations compatible with
|
|
309
|
+
TypeScript 4.9+ and Angular 15+ webpack/esbuild builders, with no Node APIs in
|
|
310
|
+
either runtime bundle. The package tests bundle consumers with esbuild, verify
|
|
311
|
+
both declaration formats, and use offline TypeScript 4.9 when installed. If the
|
|
312
|
+
4.9 check is skipped, the orchestrator must rerun the package tests with a locally
|
|
313
|
+
available TypeScript 4.9 installation in node_modules before claiming that compiler
|
|
314
|
+
was verified. Older Angular versions use `APP_INITIALIZER` as described above.
|
|
315
|
+
Publishing is gated on main and npm trusted publishing; the repository
|
|
316
|
+
owner must configure the `@drip-apex/app-sdk` trusted publisher for
|
|
317
|
+
`publish-app-sdk.yml` in the `npm-production` environment before the first release.
|
|
318
|
+
|
|
319
|
+
## Public failure contracts
|
|
320
|
+
|
|
321
|
+
All instance and singleton methods contain invalid app input and app callback errors:
|
|
322
|
+
no synchronous throw and no rejected SDK promise. Invalid input leaves identity,
|
|
323
|
+
assignments, queue and lifecycle state unchanged. This is validation at the method
|
|
324
|
+
boundary; a callback failure has its own recovery behavior below.
|
|
325
|
+
|
|
326
|
+
| Method | Invalid input / failure outcome | Return |
|
|
327
|
+
| --- | --- | --- |
|
|
328
|
+
| `new ApexClient(options)`, `Apex.configure(options)` | Invalid options stop the new context before storage, listeners or network. One diagnostic through a callable logger. Reconfigure retires the previous context first. | `ready` / configure resolves `undefined` |
|
|
329
|
+
| `identify(id, attributes?)` | Only nonempty string IDs up to 1,024 characters or `null` (anonymous) are accepted. Attributes, when supplied, must be a JSON record of strings up to 1,024 characters; they remain unused by evaluation. Invalid input logs and does not start a pending identity change. | Resolves `undefined` |
|
|
330
|
+
| `flag(key, default, options?)` | Invalid key, primitive default or options returns the exact supplied default, without exposure. A missing or incompatible assignment warns once per key after assignments load. | Supplied default |
|
|
331
|
+
| `flagObject(key, default, options?)` | Invalid key, JSON default or options returns the exact supplied default without exposure. Failed or throwing validators return the default, warn once per key, and never expose. Successful decoding isolates the returned value from cached data. | Supplied default |
|
|
332
|
+
| `variation(key)` | Invalid key returns `null` without exposure. A valid missing key retains the existing `reason: 'default'` descriptor. Unsafe assigned JSON returns `null` with the typed warning. | `null` / default descriptor |
|
|
333
|
+
| `logExposure(key)` | Invalid key is ignored without exposure. Pending identify suppresses all exposure entry points. | `undefined` |
|
|
334
|
+
| `track(name, properties?)` | Nonempty names up to 256 characters; properties omitted/`undefined` mean `{}`. Non-record properties (including `null`, arrays and primitives), unsafe JSON, PII or invalid revenue log and drop before enqueue. | Resolves `undefined` |
|
|
335
|
+
| `trackPurchase(id, revenue, currency, properties?)` | Nonempty string ID up to 1,024 characters, finite safe nonnegative revenue, three-letter currency after trimming/uppercasing. Properties have the same record contract as `track`. Invalid input logs and drops the whole purchase before enqueue. | Resolves `undefined` |
|
|
336
|
+
| `onChange(handler)` | Nonfunctions log and return an inert subscription. A throwing handler logs; every other handler still runs. | `{ cancel() }`; cancel is idempotent |
|
|
337
|
+
| `setAppState(state)` | Only `foreground` and `background` are accepted. Invalid input logs and leaves state/timers unchanged. | `undefined` |
|
|
338
|
+
| `refresh()`, `flush()` | No arguments required; extra arguments ignored. Transport failure uses the documented lane retry policy; stopped contexts do nothing. | Resolves `undefined` |
|
|
339
|
+
| `shutdown()` | Extra arguments ignored; stops networking and settles waiters, then waits for outstanding serialized storage work. | Resolves `undefined` |
|
|
340
|
+
| `sdkVersion`, `deviceId`, `userId` | Read-only; no input or side effect. Singleton IDs are `undefined` before configure; instance IDs are empty before hydration. | String (or singleton `undefined`) |
|
|
341
|
+
|
|
342
|
+
JSON input rejects cycles, nonfinite/unsafe integer numbers, functions, custom
|
|
343
|
+
prototypes, accessors, and `__proto__`, `constructor`, or `prototype` keys at any
|
|
344
|
+
level. Limits are 32 nesting levels, 10,000 visited nodes, 10,000 array slots
|
|
345
|
+
(including sparse slots), and 1 MiB of combined
|
|
346
|
+
UTF-16 string/key characters. Primitive flag defaults follow these limits too.
|
|
347
|
+
Flag keys are nonempty strings up to 1,024 characters. Options are records with
|
|
348
|
+
only the documented keys; callbacks must be functions, numeric options finite
|
|
349
|
+
safe numbers, jitter a nonnegative ordered pair, and `baseUrl` an HTTP(S) URL
|
|
350
|
+
without embedded credentials. Existing numeric option clamping still applies.
|
|
351
|
+
|
|
352
|
+
Throwing loggers are isolated. Throwing `screen()` callbacks, unusable screen
|
|
353
|
+
strings, or malformed Unicode screen paths fall back to `home`. Storage adapter
|
|
354
|
+
get/set may throw synchronously or reject: device persistence failure stops
|
|
355
|
+
initialization, cache read failure gives defaults, event write failure keeps the
|
|
356
|
+
previous durable queue, acknowledgement write failure retains IDs for safe replay,
|
|
357
|
+
and identity write failure uses the suspension/recovery contract above. A queue
|
|
358
|
+
restored under a smaller capacity is activated only after successful trimming;
|
|
359
|
+
if that write fails, the new context stops with disk untouched. Reconfigure
|
|
360
|
+
after storage recovery to retry the restore. Atomic
|
|
361
|
+
replace-or-reject is required of the adapter; an adapter that hangs forever can
|
|
362
|
+
keep a storage operation (and shutdown) pending.
|
|
363
|
+
|
|
364
|
+
Capacity and retention removals log their drop reasons with each coalesced write.
|
|
365
|
+
Purchases larger than queue capacity or 200 rows are rejected whole with a diagnostic. A purchase group
|
|
366
|
+
is delivered in one request: the batch target may expand to include its last
|
|
367
|
+
whole group, without exceeding the server's 200-event request limit. Restored oversized
|
|
368
|
+
legacy groups are dropped whole with a diagnostic. Event IDs intentionally match
|
|
369
|
+
across the base and attributed purchase rows. Exactly-once server ingestion uses
|
|
370
|
+
`(event_id, experiment_id, variation_id)` as its delivery key; network retry or
|
|
371
|
+
failed local acknowledgement persistence can retransmit the same key.
|
|
372
|
+
|
|
373
|
+
Long sessions bound warning keys at 256, diagnostic rate-limit keys at 128,
|
|
374
|
+
listeners at 1,024, identity cache entries at four, and exposure dedupe keys at
|
|
375
|
+
4,096 per session. Once the exposure bound is reached, new tuples are dropped
|
|
376
|
+
with a diagnostic, preserving dedupe for tuples already counted.
|
|
377
|
+
Session rotation clears exposure and typed-warning dedupe. At most three
|
|
378
|
+
assignment requests remain outstanding (older requests are aborted on saturation),
|
|
379
|
+
plus one event request; abort/timeout releases SDK bookkeeping even if a custom
|
|
380
|
+
fetch ignores its signal. A non-aborted superseded fatal auth reply still stops
|
|
381
|
+
both lanes.
|
|
382
|
+
|
|
383
|
+
## Deterministic stress verification
|
|
384
|
+
|
|
385
|
+
`npm test` includes 2,000 seeds × 200 operations (400,000 operations), input and
|
|
386
|
+
assigned-value fuzzing, simulated process death with atomic disk survival, slow /
|
|
387
|
+
failed persistence, unordered and abort-ignoring transports, and all lifecycle
|
|
388
|
+
sources. Tracking-promise resolution and process death both check that resolved
|
|
389
|
+
events remain on disk, delivered, or removed by a logged drop rule. Slow-adapter
|
|
390
|
+
burst tests cover 500 and 2,000 events and require at most two writes, including
|
|
391
|
+
bursts arriving during an existing write. Failures report the seed and operation.
|
|
392
|
+
Replay one seed with
|
|
393
|
+
`APEX_CHAOS_FIRST_SEED=424 APEX_CHAOS_SEEDS=424 npm test`; run a larger soak with
|
|
394
|
+
`APEX_CHAOS_SEEDS=20000 APEX_CHAOS_OPERATIONS=1000 npm test`.
|
|
395
|
+
|
|
396
|
+
The CI performance guard starts with a saturated 2,000-row durable backlog,
|
|
397
|
+
then feeds 10,000 additional events through flush and atomic storage writes.
|
|
398
|
+
It requires all 12,000 rows delivered within a generous 15-second wall-clock
|
|
399
|
+
budget on the CI runner. This exercises the shipped 2,000-row capacity rather
|
|
400
|
+
than allowing an impossible 10,000-row simultaneous queue. A separate 5,000-cycle
|
|
401
|
+
session checks internal map/set, waiter, request and timer bounds.
|