@wireai/activation 0.13.3 → 0.13.5
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/AGENTS.md +3 -1
- package/CHANGELOG.md +63 -0
- package/INTEGRATION_PROMPT.md +7 -1
- package/README.md +52 -0
- package/dist/analytics/index.d.mts +86 -6
- package/dist/analytics/index.d.ts +86 -6
- package/dist/analytics/index.js +167 -2
- package/dist/analytics/index.js.map +1 -1
- package/dist/analytics/index.mjs +167 -3
- package/dist/analytics/index.mjs.map +1 -1
- package/dist/coachmarks/index.d.mts +1 -1
- package/dist/coachmarks/index.d.ts +1 -1
- package/dist/coachmarks/index.js.map +1 -1
- package/dist/coachmarks/index.mjs.map +1 -1
- package/dist/{currentSession-BoWtr3Jp.d.mts → currentSession-DgJf0fRz.d.mts} +2 -2
- package/dist/{currentSession-CCOMlaQ2.d.ts → currentSession-EOHU64QD.d.ts} +2 -2
- package/dist/{decision-Cjw0jbEj.d.mts → decision-Bgo17oH7.d.mts} +1 -1
- package/dist/{decision-GfpXAsk_.d.ts → decision-Bkh_LigV.d.ts} +1 -1
- package/dist/index.d.mts +4 -4
- package/dist/index.d.ts +4 -4
- package/dist/index.js +4 -2
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +4 -2
- package/dist/index.mjs.map +1 -1
- package/dist/questionnaire/index.d.mts +3 -3
- package/dist/questionnaire/index.d.ts +3 -3
- package/dist/questionnaire/index.js.map +1 -1
- package/dist/questionnaire/index.mjs.map +1 -1
- package/dist/reviews/index.d.mts +5 -5
- package/dist/reviews/index.d.ts +5 -5
- package/dist/reviews/index.js +4 -2
- package/dist/reviews/index.js.map +1 -1
- package/dist/reviews/index.mjs +4 -2
- package/dist/reviews/index.mjs.map +1 -1
- package/dist/showcase/index.d.mts +1 -1
- package/dist/showcase/index.d.ts +1 -1
- package/dist/showcase/index.js.map +1 -1
- package/dist/showcase/index.mjs.map +1 -1
- package/dist/{transport-DFuPyCoJ.d.ts → transport-B_0SgCBe.d.ts} +1 -1
- package/dist/{transport-CefdERPs.d.mts → transport-j5gFfJhK.d.mts} +1 -1
- package/dist/{types-CKFhyrMu.d.mts → types-BpwiRpA8.d.mts} +1 -1
- package/dist/{types-CKFhyrMu.d.ts → types-BpwiRpA8.d.ts} +1 -1
- package/dist/{types-l2tfg23P.d.mts → types-Cju-1_jT.d.mts} +1 -1
- package/dist/{types-UVYt9BJe.d.ts → types-h2BZvl1t.d.ts} +1 -1
- package/llms.txt +1 -1
- package/package.json +3 -1
- package/src/activation/revalidation.ts +9 -1
- package/src/analytics/currentSession.ts +10 -0
- package/src/analytics/eventQueue.ts +5 -0
- package/src/analytics/index.ts +6 -0
- package/src/analytics/reportClientEvent.ts +29 -7
- package/src/analytics/wireDoctor.ts +330 -0
- package/src/coachmarks/runtime.ts +10 -1
- package/src/context/deviceId.ts +10 -1
- package/src/features/WireFeaturesProvider.tsx +8 -0
- package/src/identity/identityRecord.ts +9 -1
- package/src/reviews/runtime.ts +11 -1
- package/src/theme/ThemeContext.tsx +7 -0
package/AGENTS.md
CHANGED
|
@@ -32,7 +32,7 @@ Everything an integration needs lives on the ROOT import (`@wireai/activation`).
|
|
|
32
32
|
subpaths are optional secondary feature modules; import one only if you use it.
|
|
33
33
|
|
|
34
34
|
- `@wireai/activation` (root): `WireOnboarding`, the helpers/types below, and the device-only event reporters (`reportClientEvent`, `makeSessionId`, `WIRE_ONBOARDING_EVENTS`, see "App events" below).
|
|
35
|
-
- `@wireai/activation/analytics`: the analytics FACADE and its queue: `createAnalytics`, `useAnalytics`, `createScreenTracker`, `reportAppEvent`, `createEventQueue`, `clearUserContext`. Except for `clearUserContext`, these are NOT root exports; importing them from the root does not compile. `clearUserContext` IS also a root export (`import { clearUserContext } from "@wireai/activation"`), which is the form the README teaches for the sign-out purge.
|
|
35
|
+
- `@wireai/activation/analytics`: the analytics FACADE and its queue: `createAnalytics`, `useAnalytics`, `createScreenTracker`, `reportAppEvent`, `createEventQueue`, `clearUserContext`, `wireDoctor`. Except for `clearUserContext`, these are NOT root exports; importing them from the root does not compile. `clearUserContext` IS also a root export (`import { clearUserContext } from "@wireai/activation"`), which is the form the README teaches for the sign-out purge. `wireDoctor({ target, storage })` is the DEV-ONLY integration self-check: it resolves `{ok, checks:[{name, ok, detail}]}`, never throws, and posts one `dry_run` probe event so a broken integration is caught on the first run instead of in a funnel report. It is dev-gated on `__DEV__` (a release build makes no network call), it reports the apiKey's SHAPE and never its value, and its verdict is read from the ack body, never from the status code. See the README "Verify your integration" section.
|
|
36
36
|
- `@wireai/activation/coachmarks`: in-app coachmark tour (`CoachmarkProvider`, `useCoachmarkTour`).
|
|
37
37
|
- `@wireai/activation/showcase`: post-onboarding feature showcase (`FeatureShowcase`).
|
|
38
38
|
- `@wireai/activation/reviews`: store-review gating (`ReviewGate`, `ReviewModal`, `useReviewGate`, `fetchReviewDecision`).
|
|
@@ -79,7 +79,9 @@ subpaths are optional secondary feature modules; import one only if you use it.
|
|
|
79
79
|
| `storage` | `WireOnboardingStorage` | no | Host-injected AsyncStorage-compatible storage. When set, the kit caches its session id so an app kill mid-onboarding resumes the SAME backend session (keeps the `started` count honest). Persists the correlation seed only, never answers. |
|
|
80
80
|
| `sessionTtlMs` | `number` | no | How long a persisted session id stays resumable, ms. Default `3600000` (1h). Only meaningful with `storage`. |
|
|
81
81
|
| `persistKey` | `string` | no | Override the storage key (default `wireai:session:<config.appId>`). Scope it per-user if one device runs onboarding for multiple accounts. Only meaningful with `storage`. |
|
|
82
|
+
| `retainSessionOnComplete` | `boolean` | no | Opt IN: keep the persisted session seed alive ACROSS completion. A multi-stage or replay-after-complete re-entry within one signup (typically a `key=` remount) then resumes the SAME session instead of minting a fresh `metadata.sessionId`, which the backend adopts as a second `contextId` and double-counts as a `session_started`. Omitted or `false` clears the seed on completion, the legacy single-stage behavior, so leaving it unset changes nothing. When `true`, freshness comes from `sessionTtlMs` plus an explicit new-run signal (scope a new `persistKey`, e.g. a per-signup id). Only meaningful with `storage`. |
|
|
82
83
|
| `userContext` | `Record<string, string \| number \| boolean>` | no | Host-injected, non-PII context (signup method, referral, plan tier, a hashed user id). Forwarded to the backend + client events for funnel segmentation. No raw PII. |
|
|
84
|
+
| `autoJoinKey` | `boolean` | no | Opt OUT of the automatic join key. Default `true`. When `userContext` carries no `device_key` and `storage` is present and actually persists (a storage adapter that throws or rejects is treated exactly like no `storage`), the kit injects its own per-install key, the same id `createAnalytics` / `createWireActivation` mint and persist, so the `activated` funnel joins with no host wiring. Pass `autoJoinKey={false}` for a genuinely UNLINKED session: that restores the pre-0.12.2 behavior and the dev warning fires again. It never overrides a `device_key` you passed, and it cannot inject without `storage`. |
|
|
83
85
|
| `userId` | `string` | no | The host's OPAQUE PSEUDONYMOUS user id (not an email/name/phone), so onboarding sessions reconcile to real users. Late binding supported (mount, mid-session, or post-flow via `identifyOnboarding`). Capped at 128 chars. |
|
|
84
86
|
|
|
85
87
|
### `config` fields (`WireOnboardingConfig`)
|
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,69 @@
|
|
|
3
3
|
All notable changes to `@wireai/activation` (formerly `wireai-onboarding`).
|
|
4
4
|
Historical entries below the rename keep the old package name on purpose.
|
|
5
5
|
|
|
6
|
+
## [0.13.5] - 2026-08-03
|
|
7
|
+
|
|
8
|
+
Two halves of one idea: stop finding out about a broken events integration from a funnel report.
|
|
9
|
+
|
|
10
|
+
`POST /v1/events` answers HTTP 200 for a batch it discards and reports the refusal only in the
|
|
11
|
+
response body, so a client/server contract drift breaks nothing anyone can see. It quietly flattens
|
|
12
|
+
a graph. That is exactly how a numeric `ts` threw away every `app_event` through 0.13.0. This
|
|
13
|
+
release adds a check on each side of that gap: one in CI, one on the developer's first run.
|
|
14
|
+
|
|
15
|
+
(0.13.4 is deliberately skipped.)
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
|
|
19
|
+
- **`wireDoctor`, a dev-only integration self-check**, exported from `@wireai/activation/analytics`.
|
|
20
|
+
It resolves a structured report and never throws, whatever the target, the storage adapter or the
|
|
21
|
+
network does. Four independent checks, each a `{name, ok, detail}` unit:
|
|
22
|
+
|
|
23
|
+
1. `target`: the `serverUrl` parses and an `apiKey` is present. It reports the key's SHAPE only,
|
|
24
|
+
its length and whether the prefix matches, and never any character of the key itself.
|
|
25
|
+
2. `reachability`: `GET /v1/events/contract` answers, so the URL really is a Wire server.
|
|
26
|
+
3. `storage`: a write / read / delete probe proves the offline queue can persist. The probe uses
|
|
27
|
+
its own key, outside the queue's namespace, and deletes it in a `finally` even when the read or
|
|
28
|
+
the write throws. A diagnostic that eats a pending backlog would be worse than no diagnostic.
|
|
29
|
+
4. `round_trip`: one synthetic event goes through the REAL transport with `dry_run: true`, so the
|
|
30
|
+
server validates it exactly as it would a production event and writes nothing. The verdict is
|
|
31
|
+
read from the ack body (`written === 1 && skipped === 0`), never from the status code. A
|
|
32
|
+
`ts`-shaped drift surfaces as `skipped: 1` carrying the server's own `field: "ts"`.
|
|
33
|
+
|
|
34
|
+
Dev-only by contract: the `__DEV__` guard is the first statement, so a release build makes no
|
|
35
|
+
network call and writes no storage key. The report says it was skipped rather than reporting a
|
|
36
|
+
pass it never earned. No new export subpath, no new `globalThis` slot, and a host that never calls
|
|
37
|
+
it does not bundle it.
|
|
38
|
+
|
|
39
|
+
- **An `events-contract` CI job** that validates the payloads `buildEventsRequest` actually produces
|
|
40
|
+
against the JSON Schema the production server publishes, one fixture per send-path family
|
|
41
|
+
(analytics façade, session-start lifecycle, review transport, `client_fallback`, the unmount
|
|
42
|
+
`dropped`, and an offline queue flush carrying a persisted numeric-`ts` backlog). It uses the real
|
|
43
|
+
transport rather than a copied envelope, and reads the schema live rather than from a vendored
|
|
44
|
+
snapshot, so it cannot pass by validating a replica of something that has already drifted.
|
|
45
|
+
|
|
46
|
+
The fixtures are defined once, in `test/fixtures/sendPathFamilies.ts`, and read by both this check
|
|
47
|
+
and the transport-parity test. Each one names the emitter (`file:line`) whose shape it was built
|
|
48
|
+
from, and the check prints that line beside its result, so "this is what that path really sends"
|
|
49
|
+
is a claim you can check rather than one you have to trust. The module is under `test/` and is
|
|
50
|
+
absent from `files`, so it never reaches the tarball.
|
|
51
|
+
|
|
52
|
+
It fails, it never skips. A network error, a non-200, an unparseable contract, a missing
|
|
53
|
+
`client_event_types`, an unknown `event_type` or any invalid event exits non-zero. Run it locally
|
|
54
|
+
with `npm run check:contract`. `ajv` is a devDependency and does not ship in the tarball.
|
|
55
|
+
|
|
56
|
+
### Changed
|
|
57
|
+
|
|
58
|
+
- **`buildEventsRequest` takes an optional third argument, `options?: { dryRun?: boolean }`.** The
|
|
59
|
+
`dry_run` key is added to the envelope ONLY on an explicit `true`, so every production send path
|
|
60
|
+
(which passes nothing) still serializes byte-identical bytes. That parity is asserted per
|
|
61
|
+
send-path family against golden strings captured from 0.13.3, because FIVE shipped call sites ride
|
|
62
|
+
this one envelope (the offline queue's `postBatch`, the fire-and-forget POST, the awaitable POST,
|
|
63
|
+
the review transport, and the session-start lifecycle) and a diagnostic has no business changing it
|
|
64
|
+
for them.
|
|
65
|
+
- **`readEventsAck` is exported from its module** so `wireDoctor` reuses the same ack reader the send
|
|
66
|
+
paths use. It is deliberately NOT added to the `./analytics` barrel: the module's surface widens,
|
|
67
|
+
the package's public API does not. The exports map still has exactly 8 keys.
|
|
68
|
+
|
|
6
69
|
## [0.13.3] — 2026-08-02
|
|
7
70
|
|
|
8
71
|
The headline is a fix nobody could see from inside the kit: **on the DEFAULT configuration, a
|
package/INTEGRATION_PROMPT.md
CHANGED
|
@@ -76,7 +76,13 @@ STEPS (do them in order, stop and ask if a convention is ambiguous):
|
|
|
76
76
|
onboarding on `user_context.device_key` and never on `session_id` (those are separate id spaces
|
|
77
77
|
and intersecting them returns zero rows). If my app has no single stable device key yet, say so
|
|
78
78
|
instead of inventing one.
|
|
79
|
-
11. Verify: type-check (and lint the changed files); test the flag-off + backend-error paths.
|
|
79
|
+
11. Verify: type-check (and lint the changed files); test the flag-off + backend-error paths. Then
|
|
80
|
+
prove the events wiring actually works, because `/v1/events` answers 200 for a batch it discards:
|
|
81
|
+
behind an `if (__DEV__)` guard, call
|
|
82
|
+
`wireDoctor({ target: { serverUrl: WIREAI_SERVER_URL, apiKey: WIREAI_API_KEY }, storage })` from
|
|
83
|
+
`@wireai/activation/analytics` once at app root and log the report. Every check must come back
|
|
84
|
+
`ok`. A failing `round_trip` names the field the server refused. Remove the call, or leave it
|
|
85
|
+
dev-gated, once the report is green.
|
|
80
86
|
|
|
81
87
|
Report back: the files you changed (path:line), the typecheck result, and anything you couldn't
|
|
82
88
|
infer about my conventions.
|
package/README.md
CHANGED
|
@@ -234,6 +234,7 @@ import { WireOnboarding } from "@wireai/activation";
|
|
|
234
234
|
| `onComplete` | `(result: { answers, raw }) => void` | Fires when the user taps the terminal recap's CTA. **Required.** |
|
|
235
235
|
| `theme` | `Partial<OnboardingTheme>` | Brand colors/fonts/radius/spacing, deep-merged over a neutral default. |
|
|
236
236
|
| `illustrations` | `Record<string, ReactNode>` | App artwork for `InterstitialCard`, keyed by name. |
|
|
237
|
+
| `icons` | `Record<string, ReactNode>` | Your own icon nodes, keyed by the semantic name the AI emits (`{ instagram: <BrandIg/> }`). Checked first, so use it to put your brand mark on a choice card, to add names the vocabulary does not carry, or to get icons at all without installing `@expo/vector-icons`. Anything you leave out falls back to that optional peer, then to no icon. Never a crash. See [Icons](#icons). |
|
|
237
238
|
| `validators` | `Record<string, StepValidator>` | Per-step, keyed by base-question key (e.g. `username`). Blocks advance + shows an inline error. When the backend sends a `progress.slot_id` for a screen, the kit looks up that slot FIRST and falls back to the question key, so a validator map written against today's keys keeps working. |
|
|
238
239
|
| `permissionScreens` | `PermissionScreenConfig[]` | Priming screens injected mid-flow, starting with notifications. The screen explains why, and the OS dialog opens **only** on the primary tap, never on mount. Zero new dependencies: you inject `request`. See [Permission screens](#permission-screens-the-priming-pattern). |
|
|
239
240
|
| `onSkip` | `() => void` | User skipped. |
|
|
@@ -249,6 +250,7 @@ import { WireOnboarding } from "@wireai/activation";
|
|
|
249
250
|
| `storage` | `WireOnboardingStorage` | Host-injected storage (AsyncStorage-compatible `getItem/setItem/removeItem`) for **session-id persistence**: an app KILL mid-onboarding resumes the SAME backend session instead of minting a new one, so the funnel's `started` count stays honest (no phantom drops). Pass AsyncStorage as-is, or a 3-line MMKV wrapper. Omit for the previous per-mount behavior. Persists the kit's own correlation seed only — answers stay the host's job via `onComplete`. |
|
|
250
251
|
| `sessionTtlMs` | `number` | How long a persisted session id stays resumable. Default `3600000` (1h, the backend's session TTL). With `storage` only. |
|
|
251
252
|
| `persistKey` | `string` | Override the storage key (default `wireai:session:<appId>`). Scope per-user if one device can onboard multiple accounts mid-flow. With `storage` only. |
|
|
253
|
+
| `retainSessionOnComplete` | `boolean` | Opt IN: keep the persisted session seed alive across completion. A multi-stage signup that re-enters onboarding (typically a `key=` remount) then resumes the SAME session instead of minting a fresh `metadata.sessionId`, which the backend adopts as a second `contextId` and double-counts as a `session_started`. Omitted or `false` clears the seed on completion, the legacy single-stage behavior, so leaving it unset changes nothing. With `true`, freshness comes from `sessionTtlMs` plus an explicit new-run signal (scope a new `persistKey`, e.g. a per-signup id). With `storage` only. |
|
|
252
254
|
| `userContext` | `Record<string, string \| number \| boolean>` | **This is where the join key goes.** `user_context.device_key` is the only thing that joins this onboarding session to the app's later events, so pass `userContext={activationJoinContext(deviceKey)}` (or `activationJoinContext(resolveAutoDeviceKey({ appId, storage }))` if your app owns no device id). Since 0.12.2, omitting it no longer empties the funnel silently: with `storage`, the kit injects its own key (see [The join key](#the-join-key-device_key-never-session_id) and `autoJoinKey` below). Second job: any other non-PII context the app already knows (signup method, referral, plan, a HASHED user id), forwarded on session metadata + client events for funnel segmentation. **No PII on THIS prop** (no raw emails/names); primitives only; the server caps size/keys. See [Device & user context](#device--user-context). ⚠️ Distinct from the analytics/activation **`WireUserContext`** object (`createAnalytics` / `useWireActivation`), which additionally accepts an opt-in raw `userEmail` — see [Rich user context & PII](#rich-user-context--pii). |
|
|
253
255
|
| `autoJoinKey` | `boolean` | Opt OUT of the automatic join key. Default `true`. When this prop is left alone, `storage` is present **and actually persists** (0.13.0: a `storage` adapter that rejects or throws is treated exactly like no `storage` — the kit declines and says so, because a key it cannot persist is a different key on every launch, which corrupts `min_sessions` rather than merely leaving the join empty), and `userContext` carries no `device_key`, the kit injects its own per-install key (the same id `createAnalytics` / `createWireActivation` mint and persist), so the `activated` funnel joins with no host wiring. Pass `autoJoinKey={false}` if you genuinely want an UNLINKED onboarding session: that restores the pre-0.12.2 behavior exactly, and the dev warning fires again. It never overrides a `device_key` you passed, and it cannot inject without `storage` (see [The join key](#the-join-key-device_key-never-session_id)). |
|
|
254
256
|
| `userId` | `string` | Your own OPAQUE user id, so onboarding sessions reconcile to real users later (console sessions to your user table / GA4 users). Optional and supports **late binding**: present at mount it rides the session-start metadata; if it changes mid-session (the user just registered) the kit emits an `identify` event; available only after the flow, use `identifyOnboarding(...)`. **No PII** (not an email/name/phone); trimmed and capped at 128 chars. See [User identity](#user-identity). |
|
|
@@ -714,6 +716,56 @@ check, because a component name a device cannot render does **not** degrade grac
|
|
|
714
716
|
`COMPONENT_NOT_FOUND` inside `wireai-rn` before the renderer ever sees it, and the user loses the
|
|
715
717
|
turn to the retry/fallback path.
|
|
716
718
|
|
|
719
|
+
## Verify your integration (`wireDoctor`)
|
|
720
|
+
|
|
721
|
+
`POST /v1/events` answers **HTTP 200 for a batch it throws away**. It reports the refusal in the
|
|
722
|
+
response body (`{written, skipped, errors: [{reason, field}]}`), never in the status code. So a
|
|
723
|
+
mis-wired integration looks healthy from the outside while every event disappears. That is how a
|
|
724
|
+
numeric `ts` discarded every `app_event` through 0.13.0.
|
|
725
|
+
|
|
726
|
+
`wireDoctor` catches that on the first run instead of in a funnel report three weeks later.
|
|
727
|
+
|
|
728
|
+
```tsx
|
|
729
|
+
import { useEffect } from "react";
|
|
730
|
+
import { wireDoctor } from "@wireai/activation/analytics";
|
|
731
|
+
import AsyncStorage from "@react-native-async-storage/async-storage";
|
|
732
|
+
|
|
733
|
+
useEffect(() => {
|
|
734
|
+
if (!__DEV__) return;
|
|
735
|
+
wireDoctor({
|
|
736
|
+
target: { serverUrl: DRIVELINE_SERVER_URL, apiKey: DRIVELINE_API_KEY },
|
|
737
|
+
storage: AsyncStorage,
|
|
738
|
+
}).then((report) => console.log(report.ok, report.checks));
|
|
739
|
+
}, []);
|
|
740
|
+
```
|
|
741
|
+
|
|
742
|
+
It resolves `{ ok, checks: [{ name, ok, detail }] }` and **never throws**, whatever the target, the
|
|
743
|
+
storage adapter or the network does. Four independent checks:
|
|
744
|
+
|
|
745
|
+
| `name` | What it proves |
|
|
746
|
+
|---|---|
|
|
747
|
+
| `target` | `serverUrl` is a valid http(s) URL and an `apiKey` is present. Reports the key's SHAPE only (its length, and whether the prefix matches), never its value. |
|
|
748
|
+
| `reachability` | `GET /v1/events/contract` answers, so the URL really points at a Wire server. |
|
|
749
|
+
| `storage` | The offline queue can persist, via a write / read / delete probe on its own key. Your pending backlog is never read, overwritten or deleted. |
|
|
750
|
+
| `round_trip` | One synthetic event survives real server validation. It goes out with `dry_run`, so nothing is written and no metric moves. The verdict is `written === 1 && skipped === 0`, read from the ack body, because the status code cannot tell you. |
|
|
751
|
+
|
|
752
|
+
A `ts`-shaped drift shows up as `skipped=1` carrying the server's own `field: ts`.
|
|
753
|
+
|
|
754
|
+
**Dev-only by contract.** The `__DEV__` guard is the first statement in the function, so a release
|
|
755
|
+
build does nothing at all: no network call, no storage write. In that case the report says it was
|
|
756
|
+
skipped rather than reporting a pass it never earned. A host that never imports `wireDoctor` does not
|
|
757
|
+
bundle it.
|
|
758
|
+
|
|
759
|
+
### The contract check
|
|
760
|
+
|
|
761
|
+
The kit's `events-contract` CI job validates the payloads `buildEventsRequest` actually produces
|
|
762
|
+
against the JSON Schema the production server publishes at
|
|
763
|
+
`https://wire-rn-dynamic-onboarding.fly.dev/v1/events/contract`, one fixture per send path. Run it
|
|
764
|
+
yourself with `npm run check:contract`.
|
|
765
|
+
|
|
766
|
+
It fails, it never skips. An unreachable endpoint, a non-200 or an unparseable contract is a red
|
|
767
|
+
job, because a check that has learned nothing must not report green.
|
|
768
|
+
|
|
717
769
|
## Consuming from source (Metro + tsconfig)
|
|
718
770
|
|
|
719
771
|
The monorepo apps resolve the kit from source. In each app:
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
export { R as ReportAppEventOptions, r as reportAppEvent } from '../transport-
|
|
2
|
-
import { W as WireUserContext, E as EventQueueOptions } from '../currentSession-
|
|
3
|
-
export { A as AUTO_DEVICE_ID_PREFIX, a as AnalyticsEvent,
|
|
4
|
-
import '../types-
|
|
1
|
+
export { R as ReportAppEventOptions, r as reportAppEvent } from '../transport-j5gFfJhK.mjs';
|
|
2
|
+
import { C as ClientEventTarget, W as WireUserContext, E as EventQueueOptions } from '../currentSession-DgJf0fRz.mjs';
|
|
3
|
+
export { A as AUTO_DEVICE_ID_PREFIX, a as AnalyticsEvent, b as ClearUserContextOptions, c as ClientEvent, d as ClientEventType, e as ContextEnvelope, f as ContextEnvelopeInput, D as DeviceKeyStorage, g as EnvelopeSource, h as EventQueue, R as ResolveAutoDeviceKeyOptions, i as WIRE_ONBOARDING_EVENTS, j as WireOnboardingEventName, k as analyticsUserIdStorageKey, l as buildContextEnvelope, m as clearPiiFromContext, n as clearUserContext, o as createEventQueue, p as deviceIdStorageKey, q as ensureCurrentSessionId, r as getCurrentSessionId, s as looksLikeEmail, t as makeSessionId, u as reportClientEvent, v as reportClientEventAwait, w as reportClientEvents, x as reportClientEventsAwait, y as resetAutoDeviceKeys, z as resetCurrentSessionId, B as resetEventQueueKeys, F as resolveAutoDeviceKey, G as setCurrentSessionId, H as toAnalyticsEvent } from '../currentSession-DgJf0fRz.mjs';
|
|
4
|
+
import { W as WireOnboardingStorage } from '../types-BpwiRpA8.mjs';
|
|
5
|
+
import '../types-Cju-1_jT.mjs';
|
|
5
6
|
import '../types-BKfpdZzX.mjs';
|
|
6
7
|
import '../types-BcmagF6K.mjs';
|
|
7
|
-
import '../types-CKFhyrMu.mjs';
|
|
8
8
|
import 'react';
|
|
9
9
|
import 'wireai-rn';
|
|
10
10
|
import 'react-native';
|
|
@@ -95,6 +95,86 @@ interface NavigationRefLike {
|
|
|
95
95
|
*/
|
|
96
96
|
declare const useScreenTracking: (navigationRef: NavigationRefLike | undefined, options?: ScreenTrackerOptions) => void;
|
|
97
97
|
|
|
98
|
+
/**
|
|
99
|
+
* wireDoctor: a DEV-ONLY, opt-in self-check that proves a fresh integration end to end before the
|
|
100
|
+
* first real user ever runs it.
|
|
101
|
+
*
|
|
102
|
+
* WHY IT EXISTS: the `/v1/events` endpoint answers HTTP **200** for a batch it throws away. It
|
|
103
|
+
* reports the refusal in the response body (`{written, skipped, errors:[{reason, field}]}`), so a
|
|
104
|
+
* mis-wired integration looks perfectly healthy from the outside while every event evaporates. A
|
|
105
|
+
* numeric `ts` did exactly that through 0.13.0. This turns that class of loss from something you
|
|
106
|
+
* discover in a funnel report weeks later into something the first run tells you.
|
|
107
|
+
*
|
|
108
|
+
* SHAPE: four independent checks, each a `{name, ok, detail}` unit that can be read (and tested)
|
|
109
|
+
* without the others. The report is data, never a thrown error and never a side effect on the host:
|
|
110
|
+
*
|
|
111
|
+
* 1. `target` : is there a server URL and a key, and do they look like a key and a URL?
|
|
112
|
+
* 2. `reachability`: is the server actually there? (`GET /v1/events/contract`, public and cheap)
|
|
113
|
+
* 3. `storage` : can the offline queue persist? (a write / read / delete probe)
|
|
114
|
+
* 4. `round_trip` : does a REAL event survive REAL server validation? (a `dry_run` POST)
|
|
115
|
+
*
|
|
116
|
+
* NEVER THROWS, under any input, any network condition, or any hostile response object. A doctor
|
|
117
|
+
* that can crash the screen it is diagnosing is worse than no doctor.
|
|
118
|
+
*
|
|
119
|
+
* ⛔ NEVER PRINTS A SECRET. The `target` check reports the key's SHAPE (present / absent, length,
|
|
120
|
+
* whether the prefix is the expected one) and never any character of the key itself, in any
|
|
121
|
+
* `detail`, any log line, or any error. `wireDoctor.test.ts` asserts that with a sentinel key.
|
|
122
|
+
*
|
|
123
|
+
* COSTS A NON-CALLER NOTHING: it is a plain function behind the `./analytics` subpath, the package
|
|
124
|
+
* is `sideEffects: false`, and this module has no top-level side effects, so a host that never
|
|
125
|
+
* imports it never bundles it. It also mints no `globalThis` slot and retains no timer or listener.
|
|
126
|
+
*
|
|
127
|
+
* USAGE (dev builds only):
|
|
128
|
+
*
|
|
129
|
+
* import { wireDoctor } from "@wireai/activation/analytics";
|
|
130
|
+
*
|
|
131
|
+
* const report = await wireDoctor({
|
|
132
|
+
* target: { serverUrl: "https://api.example.com", apiKey: DRIVELINE_KEY },
|
|
133
|
+
* storage: AsyncStorage,
|
|
134
|
+
* });
|
|
135
|
+
* console.log(report.ok, report.checks);
|
|
136
|
+
*/
|
|
137
|
+
|
|
138
|
+
/** One diagnosis. `name` is stable and machine-readable; `detail` is for a human reading a console. */
|
|
139
|
+
type WireDoctorCheck = {
|
|
140
|
+
/**
|
|
141
|
+
* Stable id: `target` | `reachability` | `storage` | `round_trip` | `dev_only` | `internal_error`.
|
|
142
|
+
*
|
|
143
|
+
* `dev_only` means ONE thing and only that thing: `__DEV__` is unset or false, so nothing ran.
|
|
144
|
+
* `internal_error` is the separate catch-all for a failure that got past every check's own
|
|
145
|
+
* swallow. They are distinct names because a consumer branching on `dev_only` would otherwise
|
|
146
|
+
* read an internal fault as "this is a release build" and report a healthy skip.
|
|
147
|
+
*/
|
|
148
|
+
name: string;
|
|
149
|
+
ok: boolean;
|
|
150
|
+
/** Human-readable result. ⛔ Never contains any part of an API key. */
|
|
151
|
+
detail: string;
|
|
152
|
+
};
|
|
153
|
+
/** What {@link wireDoctor} resolves to. `ok` is true only when EVERY check passed. */
|
|
154
|
+
type WireDoctorReport = {
|
|
155
|
+
ok: boolean;
|
|
156
|
+
checks: WireDoctorCheck[];
|
|
157
|
+
};
|
|
158
|
+
/** Input for {@link wireDoctor}. `storage` is optional: without it the queue runs in-memory only. */
|
|
159
|
+
type WireDoctorOptions = {
|
|
160
|
+
/** The same `{serverUrl, apiKey}` the kit is configured with. */
|
|
161
|
+
target: ClientEventTarget | undefined;
|
|
162
|
+
/**
|
|
163
|
+
* The host storage the offline queue would use (AsyncStorage-compatible). Omit it and the storage
|
|
164
|
+
* check reports the DEGRADED in-memory mode rather than failing.
|
|
165
|
+
*/
|
|
166
|
+
storage?: WireOnboardingStorage;
|
|
167
|
+
};
|
|
168
|
+
/**
|
|
169
|
+
* Run the full diagnosis. Resolves a report; NEVER throws and NEVER rejects.
|
|
170
|
+
*
|
|
171
|
+
* DEV-ONLY BY CONTRACT: the `__DEV__` guard is the FIRST statement, so a release build performs no
|
|
172
|
+
* network call, no storage write, and no work at all. The report says it was skipped rather than
|
|
173
|
+
* pretending everything passed, because a green report that never ran is the exact failure mode
|
|
174
|
+
* this whole feature exists to remove.
|
|
175
|
+
*/
|
|
176
|
+
declare const wireDoctor: (options: WireDoctorOptions) => Promise<WireDoctorReport>;
|
|
177
|
+
|
|
98
178
|
/** Arbitrary non-PII event properties. Serialized to the event's `meta` (a JSON string) on the wire. */
|
|
99
179
|
type AnalyticsProps = Record<string, unknown>;
|
|
100
180
|
/**
|
|
@@ -203,4 +283,4 @@ declare const createAnalytics: (config: CreateAnalyticsConfig, options?: Analyti
|
|
|
203
283
|
*/
|
|
204
284
|
declare const useAnalytics: (config: CreateAnalyticsConfig, options?: AnalyticsOptions) => Analytics;
|
|
205
285
|
|
|
206
|
-
export { type Analytics, type AnalyticsOptions, type AnalyticsProps, type CreateAnalyticsConfig, EventQueueOptions, type NavigationRefLike, type NavigationRouteLike, type NavigationStateLike, type ScreenTracker, type ScreenTrackerOptions, createAnalytics, createScreenTracker, getActiveRouteName, screenTrackingHandler, useAnalytics, useScreenTracking };
|
|
286
|
+
export { type Analytics, type AnalyticsOptions, type AnalyticsProps, ClientEventTarget, type CreateAnalyticsConfig, EventQueueOptions, type NavigationRefLike, type NavigationRouteLike, type NavigationStateLike, type ScreenTracker, type ScreenTrackerOptions, type WireDoctorCheck, type WireDoctorOptions, type WireDoctorReport, createAnalytics, createScreenTracker, getActiveRouteName, screenTrackingHandler, useAnalytics, useScreenTracking, wireDoctor };
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
export { R as ReportAppEventOptions, r as reportAppEvent } from '../transport-
|
|
2
|
-
import { W as WireUserContext, E as EventQueueOptions } from '../currentSession-
|
|
3
|
-
export { A as AUTO_DEVICE_ID_PREFIX, a as AnalyticsEvent,
|
|
4
|
-
import '../types-
|
|
1
|
+
export { R as ReportAppEventOptions, r as reportAppEvent } from '../transport-B_0SgCBe.js';
|
|
2
|
+
import { C as ClientEventTarget, W as WireUserContext, E as EventQueueOptions } from '../currentSession-EOHU64QD.js';
|
|
3
|
+
export { A as AUTO_DEVICE_ID_PREFIX, a as AnalyticsEvent, b as ClearUserContextOptions, c as ClientEvent, d as ClientEventType, e as ContextEnvelope, f as ContextEnvelopeInput, D as DeviceKeyStorage, g as EnvelopeSource, h as EventQueue, R as ResolveAutoDeviceKeyOptions, i as WIRE_ONBOARDING_EVENTS, j as WireOnboardingEventName, k as analyticsUserIdStorageKey, l as buildContextEnvelope, m as clearPiiFromContext, n as clearUserContext, o as createEventQueue, p as deviceIdStorageKey, q as ensureCurrentSessionId, r as getCurrentSessionId, s as looksLikeEmail, t as makeSessionId, u as reportClientEvent, v as reportClientEventAwait, w as reportClientEvents, x as reportClientEventsAwait, y as resetAutoDeviceKeys, z as resetCurrentSessionId, B as resetEventQueueKeys, F as resolveAutoDeviceKey, G as setCurrentSessionId, H as toAnalyticsEvent } from '../currentSession-EOHU64QD.js';
|
|
4
|
+
import { W as WireOnboardingStorage } from '../types-BpwiRpA8.js';
|
|
5
|
+
import '../types-h2BZvl1t.js';
|
|
5
6
|
import '../types-BKfpdZzX.js';
|
|
6
7
|
import '../types-BcmagF6K.js';
|
|
7
|
-
import '../types-CKFhyrMu.js';
|
|
8
8
|
import 'react';
|
|
9
9
|
import 'wireai-rn';
|
|
10
10
|
import 'react-native';
|
|
@@ -95,6 +95,86 @@ interface NavigationRefLike {
|
|
|
95
95
|
*/
|
|
96
96
|
declare const useScreenTracking: (navigationRef: NavigationRefLike | undefined, options?: ScreenTrackerOptions) => void;
|
|
97
97
|
|
|
98
|
+
/**
|
|
99
|
+
* wireDoctor: a DEV-ONLY, opt-in self-check that proves a fresh integration end to end before the
|
|
100
|
+
* first real user ever runs it.
|
|
101
|
+
*
|
|
102
|
+
* WHY IT EXISTS: the `/v1/events` endpoint answers HTTP **200** for a batch it throws away. It
|
|
103
|
+
* reports the refusal in the response body (`{written, skipped, errors:[{reason, field}]}`), so a
|
|
104
|
+
* mis-wired integration looks perfectly healthy from the outside while every event evaporates. A
|
|
105
|
+
* numeric `ts` did exactly that through 0.13.0. This turns that class of loss from something you
|
|
106
|
+
* discover in a funnel report weeks later into something the first run tells you.
|
|
107
|
+
*
|
|
108
|
+
* SHAPE: four independent checks, each a `{name, ok, detail}` unit that can be read (and tested)
|
|
109
|
+
* without the others. The report is data, never a thrown error and never a side effect on the host:
|
|
110
|
+
*
|
|
111
|
+
* 1. `target` : is there a server URL and a key, and do they look like a key and a URL?
|
|
112
|
+
* 2. `reachability`: is the server actually there? (`GET /v1/events/contract`, public and cheap)
|
|
113
|
+
* 3. `storage` : can the offline queue persist? (a write / read / delete probe)
|
|
114
|
+
* 4. `round_trip` : does a REAL event survive REAL server validation? (a `dry_run` POST)
|
|
115
|
+
*
|
|
116
|
+
* NEVER THROWS, under any input, any network condition, or any hostile response object. A doctor
|
|
117
|
+
* that can crash the screen it is diagnosing is worse than no doctor.
|
|
118
|
+
*
|
|
119
|
+
* ⛔ NEVER PRINTS A SECRET. The `target` check reports the key's SHAPE (present / absent, length,
|
|
120
|
+
* whether the prefix is the expected one) and never any character of the key itself, in any
|
|
121
|
+
* `detail`, any log line, or any error. `wireDoctor.test.ts` asserts that with a sentinel key.
|
|
122
|
+
*
|
|
123
|
+
* COSTS A NON-CALLER NOTHING: it is a plain function behind the `./analytics` subpath, the package
|
|
124
|
+
* is `sideEffects: false`, and this module has no top-level side effects, so a host that never
|
|
125
|
+
* imports it never bundles it. It also mints no `globalThis` slot and retains no timer or listener.
|
|
126
|
+
*
|
|
127
|
+
* USAGE (dev builds only):
|
|
128
|
+
*
|
|
129
|
+
* import { wireDoctor } from "@wireai/activation/analytics";
|
|
130
|
+
*
|
|
131
|
+
* const report = await wireDoctor({
|
|
132
|
+
* target: { serverUrl: "https://api.example.com", apiKey: DRIVELINE_KEY },
|
|
133
|
+
* storage: AsyncStorage,
|
|
134
|
+
* });
|
|
135
|
+
* console.log(report.ok, report.checks);
|
|
136
|
+
*/
|
|
137
|
+
|
|
138
|
+
/** One diagnosis. `name` is stable and machine-readable; `detail` is for a human reading a console. */
|
|
139
|
+
type WireDoctorCheck = {
|
|
140
|
+
/**
|
|
141
|
+
* Stable id: `target` | `reachability` | `storage` | `round_trip` | `dev_only` | `internal_error`.
|
|
142
|
+
*
|
|
143
|
+
* `dev_only` means ONE thing and only that thing: `__DEV__` is unset or false, so nothing ran.
|
|
144
|
+
* `internal_error` is the separate catch-all for a failure that got past every check's own
|
|
145
|
+
* swallow. They are distinct names because a consumer branching on `dev_only` would otherwise
|
|
146
|
+
* read an internal fault as "this is a release build" and report a healthy skip.
|
|
147
|
+
*/
|
|
148
|
+
name: string;
|
|
149
|
+
ok: boolean;
|
|
150
|
+
/** Human-readable result. ⛔ Never contains any part of an API key. */
|
|
151
|
+
detail: string;
|
|
152
|
+
};
|
|
153
|
+
/** What {@link wireDoctor} resolves to. `ok` is true only when EVERY check passed. */
|
|
154
|
+
type WireDoctorReport = {
|
|
155
|
+
ok: boolean;
|
|
156
|
+
checks: WireDoctorCheck[];
|
|
157
|
+
};
|
|
158
|
+
/** Input for {@link wireDoctor}. `storage` is optional: without it the queue runs in-memory only. */
|
|
159
|
+
type WireDoctorOptions = {
|
|
160
|
+
/** The same `{serverUrl, apiKey}` the kit is configured with. */
|
|
161
|
+
target: ClientEventTarget | undefined;
|
|
162
|
+
/**
|
|
163
|
+
* The host storage the offline queue would use (AsyncStorage-compatible). Omit it and the storage
|
|
164
|
+
* check reports the DEGRADED in-memory mode rather than failing.
|
|
165
|
+
*/
|
|
166
|
+
storage?: WireOnboardingStorage;
|
|
167
|
+
};
|
|
168
|
+
/**
|
|
169
|
+
* Run the full diagnosis. Resolves a report; NEVER throws and NEVER rejects.
|
|
170
|
+
*
|
|
171
|
+
* DEV-ONLY BY CONTRACT: the `__DEV__` guard is the FIRST statement, so a release build performs no
|
|
172
|
+
* network call, no storage write, and no work at all. The report says it was skipped rather than
|
|
173
|
+
* pretending everything passed, because a green report that never ran is the exact failure mode
|
|
174
|
+
* this whole feature exists to remove.
|
|
175
|
+
*/
|
|
176
|
+
declare const wireDoctor: (options: WireDoctorOptions) => Promise<WireDoctorReport>;
|
|
177
|
+
|
|
98
178
|
/** Arbitrary non-PII event properties. Serialized to the event's `meta` (a JSON string) on the wire. */
|
|
99
179
|
type AnalyticsProps = Record<string, unknown>;
|
|
100
180
|
/**
|
|
@@ -203,4 +283,4 @@ declare const createAnalytics: (config: CreateAnalyticsConfig, options?: Analyti
|
|
|
203
283
|
*/
|
|
204
284
|
declare const useAnalytics: (config: CreateAnalyticsConfig, options?: AnalyticsOptions) => Analytics;
|
|
205
285
|
|
|
206
|
-
export { type Analytics, type AnalyticsOptions, type AnalyticsProps, type CreateAnalyticsConfig, EventQueueOptions, type NavigationRefLike, type NavigationRouteLike, type NavigationStateLike, type ScreenTracker, type ScreenTrackerOptions, createAnalytics, createScreenTracker, getActiveRouteName, screenTrackingHandler, useAnalytics, useScreenTracking };
|
|
286
|
+
export { type Analytics, type AnalyticsOptions, type AnalyticsProps, ClientEventTarget, type CreateAnalyticsConfig, EventQueueOptions, type NavigationRefLike, type NavigationRouteLike, type NavigationStateLike, type ScreenTracker, type ScreenTrackerOptions, type WireDoctorCheck, type WireDoctorOptions, type WireDoctorReport, createAnalytics, createScreenTracker, getActiveRouteName, screenTrackingHandler, useAnalytics, useScreenTracking, wireDoctor };
|
package/dist/analytics/index.js
CHANGED
|
@@ -22,13 +22,15 @@ var toWireEvents = (events) => events.map((event) => {
|
|
|
22
22
|
return rest;
|
|
23
23
|
}
|
|
24
24
|
});
|
|
25
|
-
var buildEventsRequest = (target, events) => {
|
|
25
|
+
var buildEventsRequest = (target, events, options) => {
|
|
26
26
|
if (!(target == null ? void 0 : target.serverUrl) || events.length === 0) return null;
|
|
27
27
|
try {
|
|
28
28
|
const url = `${target.serverUrl.replace(/\/$/, "")}/v1/events`;
|
|
29
29
|
const headers = { "Content-Type": "application/json" };
|
|
30
30
|
if (target.apiKey) headers.Authorization = `Bearer ${target.apiKey}`;
|
|
31
|
-
const
|
|
31
|
+
const envelope = { events: toWireEvents(events) };
|
|
32
|
+
if ((options == null ? void 0 : options.dryRun) === true) envelope.dry_run = true;
|
|
33
|
+
const body = JSON.stringify(envelope);
|
|
32
34
|
return { url, init: { method: "POST", headers, body } };
|
|
33
35
|
} catch {
|
|
34
36
|
return null;
|
|
@@ -225,6 +227,168 @@ var useScreenTracking = (navigationRef, options = {}) => {
|
|
|
225
227
|
}, [navigationRef]);
|
|
226
228
|
};
|
|
227
229
|
|
|
230
|
+
// src/analytics/wireDoctor.ts
|
|
231
|
+
var PROBE_STORAGE_KEY = "wireai:doctor:probe";
|
|
232
|
+
var PROBE_QUESTION_KEY = "wire_doctor_probe";
|
|
233
|
+
var EXPECTED_KEY_PREFIX = "wai_";
|
|
234
|
+
var NETWORK_TIMEOUT_MS = 1e4;
|
|
235
|
+
var check = (name, ok, detail) => ({ name, ok, detail });
|
|
236
|
+
var fetchWithTimeout = async (url, init) => {
|
|
237
|
+
if (typeof fetch === "undefined") return void 0;
|
|
238
|
+
const controller = typeof AbortController !== "undefined" ? new AbortController() : void 0;
|
|
239
|
+
const timer = setTimeout(() => controller == null ? void 0 : controller.abort(), NETWORK_TIMEOUT_MS);
|
|
240
|
+
try {
|
|
241
|
+
return await fetch(url, controller ? { ...init, signal: controller.signal } : init);
|
|
242
|
+
} catch {
|
|
243
|
+
return void 0;
|
|
244
|
+
} finally {
|
|
245
|
+
clearTimeout(timer);
|
|
246
|
+
}
|
|
247
|
+
};
|
|
248
|
+
var checkTarget = (target) => {
|
|
249
|
+
const serverUrl = target == null ? void 0 : target.serverUrl;
|
|
250
|
+
if (typeof serverUrl !== "string" || serverUrl.trim().length === 0) {
|
|
251
|
+
return check("target", false, "no serverUrl configured: set `serverUrl` on the kit config.");
|
|
252
|
+
}
|
|
253
|
+
let parsedHost = false;
|
|
254
|
+
try {
|
|
255
|
+
const parsed = new URL(serverUrl);
|
|
256
|
+
parsedHost = parsed.protocol === "http:" || parsed.protocol === "https:";
|
|
257
|
+
} catch {
|
|
258
|
+
parsedHost = false;
|
|
259
|
+
}
|
|
260
|
+
if (!parsedHost) {
|
|
261
|
+
return check("target", false, "serverUrl is not a valid http(s) URL.");
|
|
262
|
+
}
|
|
263
|
+
const apiKey = target == null ? void 0 : target.apiKey;
|
|
264
|
+
if (typeof apiKey !== "string" || apiKey.length === 0) {
|
|
265
|
+
return check("target", false, "serverUrl looks valid, but no apiKey is configured.");
|
|
266
|
+
}
|
|
267
|
+
const prefixOk = apiKey.startsWith(EXPECTED_KEY_PREFIX);
|
|
268
|
+
return check(
|
|
269
|
+
"target",
|
|
270
|
+
true,
|
|
271
|
+
`serverUrl is a valid http(s) URL; apiKey present (${apiKey.length} chars, expected prefix: ${prefixOk ? "yes" : "no"}).`
|
|
272
|
+
);
|
|
273
|
+
};
|
|
274
|
+
var checkReachability = async (serverUrl) => {
|
|
275
|
+
const url = `${serverUrl.replace(/\/$/, "")}/v1/events/contract`;
|
|
276
|
+
const res = await fetchWithTimeout(url, { method: "GET" });
|
|
277
|
+
if (!res) {
|
|
278
|
+
return check("reachability", false, "could not reach the server (network error or timeout).");
|
|
279
|
+
}
|
|
280
|
+
const status = res.status;
|
|
281
|
+
const ok = !!res.ok;
|
|
282
|
+
if (!ok) {
|
|
283
|
+
return check(
|
|
284
|
+
"reachability",
|
|
285
|
+
false,
|
|
286
|
+
`the server answered ${typeof status === "number" ? status : "an error"} for the events contract; check the serverUrl points at a Wire server.`
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
return check("reachability", true, "the server answered the events contract.");
|
|
290
|
+
};
|
|
291
|
+
var checkStorage = async (storage) => {
|
|
292
|
+
if (!storage) {
|
|
293
|
+
return check(
|
|
294
|
+
"storage",
|
|
295
|
+
true,
|
|
296
|
+
"no storage injected: the queue runs in DEGRADED in-memory mode and loses pending events on an app kill. Pass AsyncStorage (or an MMKV wrapper) to make it durable."
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
const token = `probe_${Date.now().toString(36)}`;
|
|
300
|
+
try {
|
|
301
|
+
await storage.setItem(PROBE_STORAGE_KEY, token);
|
|
302
|
+
const read = await storage.getItem(PROBE_STORAGE_KEY);
|
|
303
|
+
if (read !== token) {
|
|
304
|
+
return check(
|
|
305
|
+
"storage",
|
|
306
|
+
false,
|
|
307
|
+
"the storage adapter accepted a write but did not read the same value back, so the queue cannot persist."
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
return check("storage", true, "storage write / read round trip succeeded.");
|
|
311
|
+
} catch {
|
|
312
|
+
return check("storage", false, "the storage adapter threw, so the queue cannot persist events.");
|
|
313
|
+
} finally {
|
|
314
|
+
try {
|
|
315
|
+
await storage.removeItem(PROBE_STORAGE_KEY);
|
|
316
|
+
} catch {
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
};
|
|
320
|
+
var checkRoundTrip = async (target) => {
|
|
321
|
+
var _a;
|
|
322
|
+
const event = {
|
|
323
|
+
event_type: "app_event",
|
|
324
|
+
session_id: makeSessionId(),
|
|
325
|
+
question_key: PROBE_QUESTION_KEY
|
|
326
|
+
};
|
|
327
|
+
const req = buildEventsRequest(target, [event], { dryRun: true });
|
|
328
|
+
if (!req) {
|
|
329
|
+
return check("round_trip", false, "could not build the events request from this target.");
|
|
330
|
+
}
|
|
331
|
+
const res = await fetchWithTimeout(req.url, req.init);
|
|
332
|
+
if (!res) {
|
|
333
|
+
return check("round_trip", false, "the events POST failed (network error or timeout).");
|
|
334
|
+
}
|
|
335
|
+
if (!res.ok) {
|
|
336
|
+
const status = res.status;
|
|
337
|
+
return check(
|
|
338
|
+
"round_trip",
|
|
339
|
+
false,
|
|
340
|
+
`the events endpoint answered ${typeof status === "number" ? status : "an error"}; a 401 here means the apiKey is not accepted.`
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
const ack = await readEventsAck(res);
|
|
344
|
+
if (!ack) {
|
|
345
|
+
return check(
|
|
346
|
+
"round_trip",
|
|
347
|
+
false,
|
|
348
|
+
"the server answered 2xx but sent no readable ack body, so it is unknown whether the event validated."
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
if (ack.written === 1 && ack.skipped === 0) {
|
|
352
|
+
return check("round_trip", true, "the server validated the probe event: written=1, skipped=0.");
|
|
353
|
+
}
|
|
354
|
+
return check(
|
|
355
|
+
"round_trip",
|
|
356
|
+
false,
|
|
357
|
+
`the server DISCARDED the probe event: written=${(_a = ack.written) != null ? _a : "unknown"}, skipped=${ack.skipped}` + (ack.reasons.length > 0 ? `, reasons: ${ack.reasons.join(", ")}.` : ". It gave no reason; check the server's ingest log for this request.")
|
|
358
|
+
);
|
|
359
|
+
};
|
|
360
|
+
var wireDoctor = async (options) => {
|
|
361
|
+
if (typeof __DEV__ === "undefined" || !__DEV__) {
|
|
362
|
+
return {
|
|
363
|
+
ok: false,
|
|
364
|
+
checks: [
|
|
365
|
+
check(
|
|
366
|
+
"dev_only",
|
|
367
|
+
false,
|
|
368
|
+
"wireDoctor is dev-only and did NOT run: __DEV__ is unset or false. Nothing was checked."
|
|
369
|
+
)
|
|
370
|
+
]
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
try {
|
|
374
|
+
const target = options == null ? void 0 : options.target;
|
|
375
|
+
const checks = [checkTarget(target)];
|
|
376
|
+
if (checks[0].ok && target) {
|
|
377
|
+
checks.push(await checkReachability(target.serverUrl));
|
|
378
|
+
checks.push(await checkStorage(options == null ? void 0 : options.storage));
|
|
379
|
+
checks.push(await checkRoundTrip(target));
|
|
380
|
+
} else {
|
|
381
|
+
checks.push(await checkStorage(options == null ? void 0 : options.storage));
|
|
382
|
+
}
|
|
383
|
+
return { ok: checks.every((c) => c.ok), checks };
|
|
384
|
+
} catch {
|
|
385
|
+
return {
|
|
386
|
+
ok: false,
|
|
387
|
+
checks: [check("internal_error", false, "wireDoctor could not complete: an unexpected error was swallowed.")]
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
};
|
|
391
|
+
|
|
228
392
|
// src/permissions/permissionEvents.ts
|
|
229
393
|
var WIRE_PERMISSION_EVENTS = {
|
|
230
394
|
/** The priming screen became visible. The denominator for every rate below. */
|
|
@@ -1132,5 +1296,6 @@ exports.setCurrentSessionId = setCurrentSessionId;
|
|
|
1132
1296
|
exports.toAnalyticsEvent = toAnalyticsEvent;
|
|
1133
1297
|
exports.useAnalytics = useAnalytics;
|
|
1134
1298
|
exports.useScreenTracking = useScreenTracking;
|
|
1299
|
+
exports.wireDoctor = wireDoctor;
|
|
1135
1300
|
//# sourceMappingURL=index.js.map
|
|
1136
1301
|
//# sourceMappingURL=index.js.map
|