@wireai/activation 0.3.0 → 0.7.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/AGENTS.md +1 -1
- package/CHANGELOG.md +57 -0
- package/dist/analytics/index.d.mts +15 -2
- package/dist/analytics/index.d.ts +15 -2
- package/dist/analytics/index.js +224 -14
- package/dist/analytics/index.js.map +1 -1
- package/dist/analytics/index.mjs +222 -15
- package/dist/analytics/index.mjs.map +1 -1
- package/dist/coachmarks/index.js +6 -1
- package/dist/coachmarks/index.js.map +1 -1
- package/dist/coachmarks/index.mjs +6 -1
- package/dist/coachmarks/index.mjs.map +1 -1
- package/dist/{eventQueue-CA1d8Fmn.d.mts → currentSession-BJBB7i4-.d.mts} +143 -15
- package/dist/{eventQueue-CrNB9gzH.d.ts → currentSession-CxnP7gAa.d.ts} +143 -15
- package/dist/index.d.mts +40 -39
- package/dist/index.d.ts +40 -39
- package/dist/index.js +378 -154
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +370 -156
- package/dist/index.mjs.map +1 -1
- package/dist/questionnaire/index.d.mts +1 -1
- package/dist/questionnaire/index.d.ts +1 -1
- package/dist/questionnaire/index.js +29 -5
- package/dist/questionnaire/index.js.map +1 -1
- package/dist/questionnaire/index.mjs +30 -6
- package/dist/questionnaire/index.mjs.map +1 -1
- package/dist/reviews/index.d.mts +1 -1
- package/dist/reviews/index.d.ts +1 -1
- package/dist/reviews/index.js +40 -9
- package/dist/reviews/index.js.map +1 -1
- package/dist/reviews/index.mjs +42 -11
- package/dist/reviews/index.mjs.map +1 -1
- package/dist/showcase/index.d.mts +1 -14
- package/dist/showcase/index.d.ts +1 -14
- package/dist/showcase/index.js +8 -2
- package/dist/showcase/index.js.map +1 -1
- package/dist/showcase/index.mjs +8 -2
- package/dist/showcase/index.mjs.map +1 -1
- package/package.json +3 -2
- package/src/OnboardingFlow.tsx +8 -4
- package/src/WireOnboarding.tsx +12 -6
- package/src/analytics/analyticsFacade.ts +86 -10
- package/src/analytics/contextEnvelope.ts +13 -7
- package/src/analytics/currentSession.ts +35 -0
- package/src/analytics/eventQueue.ts +5 -0
- package/src/analytics/index.ts +3 -0
- package/src/analytics/useAnalytics.ts +7 -1
- package/src/analytics/useScreenTracking.ts +23 -1
- package/src/cards/InterstitialCard.tsx +1 -1
- package/src/cards/NumberStepperCard.tsx +12 -7
- package/src/cards/StatusCard.tsx +13 -8
- package/src/cards/TextInputCard.tsx +10 -5
- package/src/components/AnimatedSparkle.tsx +20 -3
- package/src/components/Button.tsx +10 -1
- package/src/components/CardHandoff.tsx +2 -6
- package/src/components/CardLayout.tsx +16 -10
- package/src/components/Illustration.tsx +9 -5
- package/src/components/LoadingBlock.tsx +44 -29
- package/src/components/LoadingScreen.tsx +3 -2
- package/src/components/OnboardingScaffold.tsx +21 -23
- package/src/context/userContext.ts +210 -0
- package/src/device/appVersion.ts +103 -0
- package/src/device/deviceContext.ts +17 -5
- package/src/features/WireFeaturesProvider.tsx +7 -2
- package/src/identity/userIdentity.ts +5 -0
- package/src/index.ts +23 -0
- package/src/questionnaire/QuestionnaireGate.tsx +15 -3
- package/src/reviews/ReviewGate.tsx +27 -7
- package/src/reviews/ReviewModal.tsx +4 -0
- package/src/session-analytics/reportSessionStart.ts +7 -0
- package/src/session-analytics/useLifecycleEvents.ts +4 -2
- package/src/session-analytics/useSessionStart.ts +2 -1
- package/src/showcase/FeatureShowcase.tsx +2 -1
- package/src/types.ts +6 -5
- package/src/components/loaderChrome.ts +0 -28
package/AGENTS.md
CHANGED
|
@@ -78,7 +78,7 @@ There is no `analytics` subpath: the app-event / analytics API is exported from
|
|
|
78
78
|
| `serverUrl` | `string` | yes | Base server URL; the kit appends `/a2a`. |
|
|
79
79
|
| `appId` | `string` | yes | Passed as the A2A `model` (informational; the key resolves the app). |
|
|
80
80
|
| `metadata` | `Record<string, unknown>` | no | Merged into every A2A request (e.g. install attribution). The kit reserves `sessionId` + `supportedComponents`; do not override them. |
|
|
81
|
-
| `appVersion` | `string` | no |
|
|
81
|
+
| `appVersion` | `string` | no | App version string (e.g. `"1.4.2"`); forwarded to the backend + client events so analytics can segment the funnel by app version. Auto-detected best-effort from `expo-constants`/`expo-application` (dependency-free) when omitted; pass it to override (yours wins). |
|
|
82
82
|
|
|
83
83
|
## App events / analytics (root export)
|
|
84
84
|
|
package/CHANGELOG.md
CHANGED
|
@@ -20,6 +20,63 @@ Historical entries below the rename keep the old package name on purpose.
|
|
|
20
20
|
|
|
21
21
|
## [0.7.0] — Unreleased
|
|
22
22
|
|
|
23
|
+
### Added: `WireUserContext` — one rich user-context object → every event's `user_context`
|
|
24
|
+
|
|
25
|
+
- New `context/userContext.ts` + public export `WireUserContext`: a single, extensible object a host
|
|
26
|
+
passes ONCE (`appVersion?`, `deviceKey?`, `userId?`, `userEmail?` + `hashEmail?`, `extra?`) that the
|
|
27
|
+
kit flows into every analytics event. Pure `resolveUserContext(ctx, { autoAppVersion })` merges it
|
|
28
|
+
with ONE precedence rule — an explicit field WINS over the auto-detected `device`/`appVersion` —
|
|
29
|
+
and omits missing fields (never sent empty). Also exported: `namespaceExtra`, `hashEmailFnv1a`,
|
|
30
|
+
`isWireScalar`, `RESERVED_USER_CONTEXT_KEYS`, `EXTRA_KEY_PREFIX`.
|
|
31
|
+
- Deliberate bucket separation (nothing leaks): `userId` → the event's TOP-LEVEL opaque `user_id`
|
|
32
|
+
(via `sanitizeUserId`, capped 128) and NEVER the `user_context` bucket; `deviceKey` →
|
|
33
|
+
`user_context.device_key` (NOT `session_id`); `appVersion` → `user_context.app_version`; `extra`
|
|
34
|
+
entries are coerced to `string | number | boolean` (non-scalars/NaN/Infinity dropped) and
|
|
35
|
+
NAMESPACED under a `custom.` key prefix so a host extra can never collide with a reserved key.
|
|
36
|
+
- **`userEmail` is OPT-IN PII in its OWN field** (`user_context.user_email`) — never merged into
|
|
37
|
+
`userId`. The kit NEVER auto-collects it; a host passes it only with the user's consent (EU users:
|
|
38
|
+
personal data). Set `hashEmail: true` to send a dependency-free FNV-1a fold instead of the raw
|
|
39
|
+
address (`user_context.user_email_hashed: true`). NOTE: FNV-1a is a lightweight non-cryptographic
|
|
40
|
+
fold; for a cryptographic digest, pre-hash host-side (e.g. `expo-crypto` SHA-256) and pass that as
|
|
41
|
+
`userEmail` with `hashEmail` falsy. Documented in the field's TSDoc.
|
|
42
|
+
- The analytics façade (`createAnalytics`) now accepts `userContext` at init AND exposes
|
|
43
|
+
`setUserContext(partial)` so a host can attach `userId`/`userEmail` at LOGIN without remounting;
|
|
44
|
+
both flow into subsequent `track`/`screen`/`identify` events' `user_context`. Backward compatible:
|
|
45
|
+
`createAnalytics({ serverUrl, apiKey })` with no `userContext` behaves exactly as before.
|
|
46
|
+
|
|
47
|
+
### Fixed: `identify` + host app-events reuse the live per-open session (kills the phantom-session)
|
|
48
|
+
|
|
49
|
+
- New `analytics/currentSession.ts` (`getCurrentSessionId` / `setCurrentSessionId` /
|
|
50
|
+
`resetCurrentSessionId`): a process-local registry of the CURRENT per-open `session_id`.
|
|
51
|
+
`reportSessionStart` registers each per-open id here on every app-open.
|
|
52
|
+
- The façade's `identify` (and `track`/`screen` app-events) now source their `session_id` from
|
|
53
|
+
`config.sessionId ?? getCurrentSessionId() ?? <stable per-instance id>` — so identity binds to the
|
|
54
|
+
session the server ALREADY saw (via `app.session_started`) instead of minting a fresh id the server
|
|
55
|
+
back-fills into a synthetic session. This is what inflated the Morrow/Myelino session counts. An
|
|
56
|
+
explicit `config.sessionId` still freezes the id (opt-out). This is also the kit half of the #205
|
|
57
|
+
"align all app-events to the per-open session" follow-up. **No server change required** for the
|
|
58
|
+
normal flow (session-start fires on mount, identify at login); the only residual is an `identify`
|
|
59
|
+
fired before any open this launch, which falls back to the instance id.
|
|
60
|
+
- `identifyOnboarding` gains a last-resort fallback to `getCurrentSessionId()` when no `contextId`
|
|
61
|
+
is captured or resolvable from storage, so a post-flow identify binds to the live open session
|
|
62
|
+
instead of no-oping. An explicit `contextId` (or a stored one) still wins.
|
|
63
|
+
|
|
64
|
+
### Added: `device.appVersion` auto-detection — populate the `by_app_version` breakdown by default
|
|
65
|
+
|
|
66
|
+
- `collectDeviceContext()` now carries a best-effort `appVersion` so the analytics
|
|
67
|
+
`by_app_version` funnel breakdown works out of the box, instead of staying empty until a host
|
|
68
|
+
remembers to pass `config.appVersion`. New `device/appVersion.ts` reads the version from
|
|
69
|
+
`expo-constants` (`expoConfig.version`, then `nativeAppVersion`) or `expo-application`
|
|
70
|
+
(`nativeApplicationVersion`) when present.
|
|
71
|
+
- ADDS NO DEPENDENCY: the Expo modules are read through a guarded, variable-specifier `require`,
|
|
72
|
+
so a host WITHOUT them (e.g. bare React Native) never fails to bundle — the read simply yields
|
|
73
|
+
`undefined`. Nothing is added to `package.json`; no peer is forced.
|
|
74
|
+
- An explicit host `config.appVersion` still WINS everywhere (the `WireOnboarding` device memo, the
|
|
75
|
+
`useSessionStart` / `useLifecycleEvents` hooks, and the analytics context envelope overwrite the
|
|
76
|
+
auto-detected value with the host's). The context envelope also mirrors the effective version
|
|
77
|
+
onto its outer `appVersion` scalar so `user_context.app_version` populates on facade events too.
|
|
78
|
+
- New public export `detectAppVersion()` (alongside `collectDeviceContext`). Never throws.
|
|
79
|
+
|
|
23
80
|
### Added: session mapping — know when a user opens the app again
|
|
24
81
|
|
|
25
82
|
- New `session-analytics/` module. `reportSessionStart(opts)` posts ONE standard
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { R as ReportAppEventOptions, r as reportAppEvent } from '../transport-BeO_Brcu.mjs';
|
|
2
|
-
import { E as EventQueueOptions } from '../
|
|
3
|
-
export { A as AnalyticsEvent, C as ClientEvent, a as ClientEventTarget, b as ClientEventType, c as ContextEnvelope, d as ContextEnvelopeInput, e as EnvelopeSource, f as EventQueue,
|
|
2
|
+
import { W as WireUserContext, E as EventQueueOptions } from '../currentSession-BJBB7i4-.mjs';
|
|
3
|
+
export { A as AnalyticsEvent, C as ClientEvent, a as ClientEventTarget, b as ClientEventType, c as ContextEnvelope, d as ContextEnvelopeInput, e as EnvelopeSource, f as EventQueue, g as WIRE_ONBOARDING_EVENTS, h as WireOnboardingEventName, i as buildContextEnvelope, j as createEventQueue, k as getCurrentSessionId, m as makeSessionId, r as reportClientEvent, l as reportClientEvents, n as resetCurrentSessionId, s as setCurrentSessionId, t as toAnalyticsEvent } from '../currentSession-BJBB7i4-.mjs';
|
|
4
4
|
import '../types-A6pTxIZV.mjs';
|
|
5
5
|
import '../types-BKfpdZzX.mjs';
|
|
6
6
|
import '../types-GL_hQ0TN.mjs';
|
|
@@ -120,6 +120,13 @@ type CreateAnalyticsConfig = {
|
|
|
120
120
|
appBuild?: string;
|
|
121
121
|
/** Host connectivity signal, e.g. "wifi" | "cellular" — read fresh per event via the provider. */
|
|
122
122
|
networkType?: string;
|
|
123
|
+
/**
|
|
124
|
+
* The rich {@link WireUserContext} to stamp onto every event's `user_context` (device key, opaque
|
|
125
|
+
* user id, opt-in email, arbitrary `extra`). Passed ONCE here at init; updatable post-mount via
|
|
126
|
+
* {@link Analytics.setUserContext} (e.g. attach `userId`/`userEmail` at login). Optional — omit it
|
|
127
|
+
* and events carry only the device context, exactly as before.
|
|
128
|
+
*/
|
|
129
|
+
userContext?: WireUserContext;
|
|
123
130
|
};
|
|
124
131
|
/** Optional queue tuning knobs, forwarded verbatim to {@link createEventQueue}. */
|
|
125
132
|
type AnalyticsOptions = Partial<Pick<EventQueueOptions, "maxSize" | "batchSize" | "baseBackoffMs" | "maxBackoffMs" | "maxRetries">>;
|
|
@@ -135,6 +142,12 @@ type Analytics = {
|
|
|
135
142
|
screen(name: string, props?: AnalyticsProps): void;
|
|
136
143
|
/** Bind the host's opaque user id (per-session, in-memory) and emit an `identify` event. */
|
|
137
144
|
identify(userId: string, traits?: AnalyticsProps): void;
|
|
145
|
+
/**
|
|
146
|
+
* Update the {@link WireUserContext} after init (e.g. attach `userId`/`userEmail` at login). Shallow
|
|
147
|
+
* merges the partial over the current context (`extra` is deep-merged); a supplied `userId` also
|
|
148
|
+
* binds like {@link identify}. Takes effect on subsequent events. Fire-and-forget.
|
|
149
|
+
*/
|
|
150
|
+
setUserContext(partial: Partial<WireUserContext>): void;
|
|
138
151
|
/** Attempt an immediate drain of the pending buffer. Fire-and-forget. */
|
|
139
152
|
flush(): void;
|
|
140
153
|
/** Host reconnect signal: reset backoff and drain now. Fire-and-forget. */
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { R as ReportAppEventOptions, r as reportAppEvent } from '../transport-DLpd1v5_.js';
|
|
2
|
-
import { E as EventQueueOptions } from '../
|
|
3
|
-
export { A as AnalyticsEvent, C as ClientEvent, a as ClientEventTarget, b as ClientEventType, c as ContextEnvelope, d as ContextEnvelopeInput, e as EnvelopeSource, f as EventQueue,
|
|
2
|
+
import { W as WireUserContext, E as EventQueueOptions } from '../currentSession-CxnP7gAa.js';
|
|
3
|
+
export { A as AnalyticsEvent, C as ClientEvent, a as ClientEventTarget, b as ClientEventType, c as ContextEnvelope, d as ContextEnvelopeInput, e as EnvelopeSource, f as EventQueue, g as WIRE_ONBOARDING_EVENTS, h as WireOnboardingEventName, i as buildContextEnvelope, j as createEventQueue, k as getCurrentSessionId, m as makeSessionId, r as reportClientEvent, l as reportClientEvents, n as resetCurrentSessionId, s as setCurrentSessionId, t as toAnalyticsEvent } from '../currentSession-CxnP7gAa.js';
|
|
4
4
|
import '../types-BhpXJGlg.js';
|
|
5
5
|
import '../types-BKfpdZzX.js';
|
|
6
6
|
import '../types-GL_hQ0TN.js';
|
|
@@ -120,6 +120,13 @@ type CreateAnalyticsConfig = {
|
|
|
120
120
|
appBuild?: string;
|
|
121
121
|
/** Host connectivity signal, e.g. "wifi" | "cellular" — read fresh per event via the provider. */
|
|
122
122
|
networkType?: string;
|
|
123
|
+
/**
|
|
124
|
+
* The rich {@link WireUserContext} to stamp onto every event's `user_context` (device key, opaque
|
|
125
|
+
* user id, opt-in email, arbitrary `extra`). Passed ONCE here at init; updatable post-mount via
|
|
126
|
+
* {@link Analytics.setUserContext} (e.g. attach `userId`/`userEmail` at login). Optional — omit it
|
|
127
|
+
* and events carry only the device context, exactly as before.
|
|
128
|
+
*/
|
|
129
|
+
userContext?: WireUserContext;
|
|
123
130
|
};
|
|
124
131
|
/** Optional queue tuning knobs, forwarded verbatim to {@link createEventQueue}. */
|
|
125
132
|
type AnalyticsOptions = Partial<Pick<EventQueueOptions, "maxSize" | "batchSize" | "baseBackoffMs" | "maxBackoffMs" | "maxRetries">>;
|
|
@@ -135,6 +142,12 @@ type Analytics = {
|
|
|
135
142
|
screen(name: string, props?: AnalyticsProps): void;
|
|
136
143
|
/** Bind the host's opaque user id (per-session, in-memory) and emit an `identify` event. */
|
|
137
144
|
identify(userId: string, traits?: AnalyticsProps): void;
|
|
145
|
+
/**
|
|
146
|
+
* Update the {@link WireUserContext} after init (e.g. attach `userId`/`userEmail` at login). Shallow
|
|
147
|
+
* merges the partial over the current context (`extra` is deep-merged); a supplied `userId` also
|
|
148
|
+
* binds like {@link identify}. Takes effect on subsequent events. Fire-and-forget.
|
|
149
|
+
*/
|
|
150
|
+
setUserContext(partial: Partial<WireUserContext>): void;
|
|
138
151
|
/** Attempt an immediate drain of the pending buffer. Fire-and-forget. */
|
|
139
152
|
flush(): void;
|
|
140
153
|
/** Host reconnect signal: reset backoff and drain now. Fire-and-forget. */
|
package/dist/analytics/index.js
CHANGED
|
@@ -3,6 +3,13 @@
|
|
|
3
3
|
var react = require('react');
|
|
4
4
|
var reactNative = require('react-native');
|
|
5
5
|
|
|
6
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
7
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
8
|
+
}) : x)(function(x) {
|
|
9
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
10
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
11
|
+
});
|
|
12
|
+
|
|
6
13
|
// src/reviews/transport.ts
|
|
7
14
|
var reportAppEvent = (target, name, options = {}) => {
|
|
8
15
|
if (!(target == null ? void 0 : target.serverUrl) || !name) return;
|
|
@@ -64,8 +71,29 @@ var createScreenTracker = (options = {}) => {
|
|
|
64
71
|
};
|
|
65
72
|
var screenTrackingHandler = (tracker) => (state) => tracker.track(getActiveRouteName(state));
|
|
66
73
|
var useScreenTracking = (navigationRef, options = {}) => {
|
|
74
|
+
const optionsRef = react.useRef(options);
|
|
75
|
+
optionsRef.current = options;
|
|
67
76
|
const trackerRef = react.useRef(void 0);
|
|
68
|
-
if (!trackerRef.current)
|
|
77
|
+
if (!trackerRef.current) {
|
|
78
|
+
const dynamicOptions = {
|
|
79
|
+
get target() {
|
|
80
|
+
return optionsRef.current.target;
|
|
81
|
+
},
|
|
82
|
+
get sessionId() {
|
|
83
|
+
return optionsRef.current.sessionId;
|
|
84
|
+
},
|
|
85
|
+
get deviceKey() {
|
|
86
|
+
return optionsRef.current.deviceKey;
|
|
87
|
+
},
|
|
88
|
+
get onScreen() {
|
|
89
|
+
return optionsRef.current.onScreen;
|
|
90
|
+
},
|
|
91
|
+
get shouldTrack() {
|
|
92
|
+
return optionsRef.current.shouldTrack;
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
trackerRef.current = createScreenTracker(dynamicOptions);
|
|
96
|
+
}
|
|
69
97
|
react.useEffect(() => {
|
|
70
98
|
const tracker = trackerRef.current;
|
|
71
99
|
if (!tracker || !(navigationRef == null ? void 0 : navigationRef.addListener)) return;
|
|
@@ -136,6 +164,57 @@ var toAnalyticsEvent = (event) => {
|
|
|
136
164
|
}
|
|
137
165
|
}
|
|
138
166
|
};
|
|
167
|
+
|
|
168
|
+
// src/device/appVersion.ts
|
|
169
|
+
var coerceVersion = (value) => {
|
|
170
|
+
if (typeof value !== "string") return void 0;
|
|
171
|
+
const trimmed = value.trim();
|
|
172
|
+
return trimmed.length > 0 ? trimmed : void 0;
|
|
173
|
+
};
|
|
174
|
+
var runtimeRequire = (moduleName) => {
|
|
175
|
+
try {
|
|
176
|
+
if (typeof __require !== "function") return void 0;
|
|
177
|
+
return __require(moduleName);
|
|
178
|
+
} catch {
|
|
179
|
+
return void 0;
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
var interop = (mod) => {
|
|
183
|
+
if (!mod || typeof mod !== "object") return void 0;
|
|
184
|
+
const def = mod.default;
|
|
185
|
+
if (def && typeof def === "object") return def;
|
|
186
|
+
return mod;
|
|
187
|
+
};
|
|
188
|
+
var safeInterop = (requireModule, moduleName) => {
|
|
189
|
+
try {
|
|
190
|
+
return interop(requireModule(moduleName));
|
|
191
|
+
} catch {
|
|
192
|
+
return void 0;
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
var detectAppVersion = (requireModule = runtimeRequire) => {
|
|
196
|
+
try {
|
|
197
|
+
const constants = safeInterop(requireModule, "expo-constants");
|
|
198
|
+
if (constants) {
|
|
199
|
+
const expoConfig = constants.expoConfig;
|
|
200
|
+
if (expoConfig && typeof expoConfig === "object") {
|
|
201
|
+
const fromExpoConfig = coerceVersion(expoConfig.version);
|
|
202
|
+
if (fromExpoConfig) return fromExpoConfig;
|
|
203
|
+
}
|
|
204
|
+
const fromNative = coerceVersion(constants.nativeAppVersion);
|
|
205
|
+
if (fromNative) return fromNative;
|
|
206
|
+
}
|
|
207
|
+
const application = safeInterop(requireModule, "expo-application");
|
|
208
|
+
if (application) {
|
|
209
|
+
const fromApplication = coerceVersion(application.nativeApplicationVersion);
|
|
210
|
+
if (fromApplication) return fromApplication;
|
|
211
|
+
}
|
|
212
|
+
} catch {
|
|
213
|
+
}
|
|
214
|
+
return void 0;
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
// src/device/deviceContext.ts
|
|
139
218
|
var deriveFormFactor = (iosIdiom, width, height) => {
|
|
140
219
|
if (iosIdiom === "pad") return "tablet";
|
|
141
220
|
if (iosIdiom === "phone") return "phone";
|
|
@@ -205,16 +284,20 @@ var collectDeviceContext = () => {
|
|
|
205
284
|
if (resolved.timeZone) ctx.timeZone = resolved.timeZone;
|
|
206
285
|
} catch {
|
|
207
286
|
}
|
|
287
|
+
const appVersion = detectAppVersion();
|
|
288
|
+
if (appVersion) ctx.appVersion = appVersion;
|
|
208
289
|
return ctx;
|
|
209
290
|
};
|
|
210
291
|
|
|
211
292
|
// src/analytics/contextEnvelope.ts
|
|
212
293
|
var buildContextEnvelope = (input = {}) => {
|
|
294
|
+
var _a;
|
|
213
295
|
const device = { ...collectDeviceContext() };
|
|
214
|
-
if (input.appVersion
|
|
296
|
+
if (input.appVersion) device.appVersion = input.appVersion;
|
|
297
|
+
const effectiveAppVersion = (_a = input.appVersion) != null ? _a : device.appVersion;
|
|
215
298
|
const envelope = { device };
|
|
216
299
|
if (input.sessionId) envelope.sessionId = input.sessionId;
|
|
217
|
-
if (
|
|
300
|
+
if (effectiveAppVersion) envelope.appVersion = effectiveAppVersion;
|
|
218
301
|
if (input.appBuild) envelope.appBuild = input.appBuild;
|
|
219
302
|
if (input.networkType) envelope.networkType = input.networkType;
|
|
220
303
|
return envelope;
|
|
@@ -338,6 +421,8 @@ var createEventQueue = (options) => {
|
|
|
338
421
|
})();
|
|
339
422
|
const postBatch = async (events) => {
|
|
340
423
|
if (!(target == null ? void 0 : target.serverUrl) || events.length === 0) return false;
|
|
424
|
+
const controller = typeof AbortController !== "undefined" ? new AbortController() : void 0;
|
|
425
|
+
const timer = setTimeout(() => controller == null ? void 0 : controller.abort(), 15e3);
|
|
341
426
|
try {
|
|
342
427
|
const url = `${target.serverUrl.replace(/\/$/, "")}/v1/events`;
|
|
343
428
|
const headers = { "Content-Type": "application/json" };
|
|
@@ -345,11 +430,14 @@ var createEventQueue = (options) => {
|
|
|
345
430
|
const res = await fetch(url, {
|
|
346
431
|
method: "POST",
|
|
347
432
|
headers,
|
|
348
|
-
body: JSON.stringify({ events })
|
|
433
|
+
body: JSON.stringify({ events }),
|
|
434
|
+
signal: controller == null ? void 0 : controller.signal
|
|
349
435
|
});
|
|
350
436
|
return !!(res && res.ok);
|
|
351
437
|
} catch {
|
|
352
438
|
return false;
|
|
439
|
+
} finally {
|
|
440
|
+
clearTimeout(timer);
|
|
353
441
|
}
|
|
354
442
|
};
|
|
355
443
|
const clearRetry = () => {
|
|
@@ -423,6 +511,16 @@ var createEventQueue = (options) => {
|
|
|
423
511
|
return { enqueue, flush, notifyOnline, size };
|
|
424
512
|
};
|
|
425
513
|
|
|
514
|
+
// src/analytics/currentSession.ts
|
|
515
|
+
var _currentSessionId;
|
|
516
|
+
var setCurrentSessionId = (id) => {
|
|
517
|
+
if (typeof id === "string" && id.length > 0) _currentSessionId = id;
|
|
518
|
+
};
|
|
519
|
+
var getCurrentSessionId = () => _currentSessionId;
|
|
520
|
+
var resetCurrentSessionId = () => {
|
|
521
|
+
_currentSessionId = void 0;
|
|
522
|
+
};
|
|
523
|
+
|
|
426
524
|
// src/identity/userIdentity.ts
|
|
427
525
|
var USER_ID_MAX_LENGTH = 128;
|
|
428
526
|
var sanitizeUserId = (raw) => {
|
|
@@ -432,12 +530,79 @@ var sanitizeUserId = (raw) => {
|
|
|
432
530
|
return trimmed.length > USER_ID_MAX_LENGTH ? trimmed.slice(0, USER_ID_MAX_LENGTH) : trimmed;
|
|
433
531
|
};
|
|
434
532
|
|
|
533
|
+
// src/context/userContext.ts
|
|
534
|
+
var EXTRA_KEY_PREFIX = "custom.";
|
|
535
|
+
var isWireScalar = (value) => {
|
|
536
|
+
const t = typeof value;
|
|
537
|
+
if (t === "string" || t === "boolean") return true;
|
|
538
|
+
if (t === "number") return Number.isFinite(value);
|
|
539
|
+
return false;
|
|
540
|
+
};
|
|
541
|
+
var hashEmailFnv1a = (email) => {
|
|
542
|
+
const normalized = email.trim().toLowerCase();
|
|
543
|
+
let hash = 2166136261;
|
|
544
|
+
for (let i = 0; i < normalized.length; i++) {
|
|
545
|
+
hash ^= normalized.charCodeAt(i);
|
|
546
|
+
hash = Math.imul(hash, 16777619);
|
|
547
|
+
}
|
|
548
|
+
return (hash >>> 0).toString(16).padStart(8, "0");
|
|
549
|
+
};
|
|
550
|
+
var cleanString = (value) => {
|
|
551
|
+
if (typeof value !== "string") return void 0;
|
|
552
|
+
const trimmed = value.trim();
|
|
553
|
+
return trimmed.length > 0 ? trimmed : void 0;
|
|
554
|
+
};
|
|
555
|
+
var namespaceExtra = (extra) => {
|
|
556
|
+
const out = {};
|
|
557
|
+
if (!extra || typeof extra !== "object") return out;
|
|
558
|
+
for (const [key, value] of Object.entries(extra)) {
|
|
559
|
+
const cleanKey = cleanString(key);
|
|
560
|
+
if (!cleanKey) continue;
|
|
561
|
+
if (!isWireScalar(value)) continue;
|
|
562
|
+
out[`${EXTRA_KEY_PREFIX}${cleanKey}`] = value;
|
|
563
|
+
}
|
|
564
|
+
return out;
|
|
565
|
+
};
|
|
566
|
+
var resolveUserContext = (ctx = {}, opts = {}) => {
|
|
567
|
+
var _a;
|
|
568
|
+
const result = {};
|
|
569
|
+
const bucket = {};
|
|
570
|
+
const userId = sanitizeUserId(ctx.userId);
|
|
571
|
+
if (userId) result.userId = userId;
|
|
572
|
+
const deviceKey = cleanString(ctx.deviceKey);
|
|
573
|
+
if (deviceKey) {
|
|
574
|
+
result.deviceKey = deviceKey;
|
|
575
|
+
bucket.device_key = deviceKey;
|
|
576
|
+
}
|
|
577
|
+
const appVersion = (_a = cleanString(ctx.appVersion)) != null ? _a : cleanString(opts.autoAppVersion);
|
|
578
|
+
if (appVersion) {
|
|
579
|
+
result.appVersion = appVersion;
|
|
580
|
+
bucket.app_version = appVersion;
|
|
581
|
+
}
|
|
582
|
+
const email = cleanString(ctx.userEmail);
|
|
583
|
+
if (email) {
|
|
584
|
+
if (ctx.hashEmail) {
|
|
585
|
+
bucket.user_email = hashEmailFnv1a(email);
|
|
586
|
+
bucket.user_email_hashed = true;
|
|
587
|
+
} else {
|
|
588
|
+
bucket.user_email = email;
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
Object.assign(bucket, namespaceExtra(ctx.extra));
|
|
592
|
+
if (Object.keys(bucket).length > 0) result.userContext = bucket;
|
|
593
|
+
return result;
|
|
594
|
+
};
|
|
595
|
+
|
|
435
596
|
// src/analytics/analyticsFacade.ts
|
|
436
597
|
var createAnalytics = (config, options = {}) => {
|
|
437
|
-
var _a;
|
|
438
|
-
const
|
|
598
|
+
var _a, _b, _c, _d;
|
|
599
|
+
const instanceSessionId = (_a = config.sessionId) != null ? _a : makeSessionId();
|
|
600
|
+
const resolveSessionId = () => {
|
|
601
|
+
var _a2, _b2;
|
|
602
|
+
return (_b2 = (_a2 = config.sessionId) != null ? _a2 : getCurrentSessionId()) != null ? _b2 : instanceSessionId;
|
|
603
|
+
};
|
|
439
604
|
const envelope = () => buildContextEnvelope({
|
|
440
|
-
sessionId,
|
|
605
|
+
sessionId: resolveSessionId(),
|
|
441
606
|
appVersion: config.appVersion,
|
|
442
607
|
appBuild: config.appBuild,
|
|
443
608
|
networkType: config.networkType
|
|
@@ -449,16 +614,45 @@ var createAnalytics = (config, options = {}) => {
|
|
|
449
614
|
envelope,
|
|
450
615
|
...options
|
|
451
616
|
});
|
|
452
|
-
let
|
|
617
|
+
let userContext = { ...(_b = config.userContext) != null ? _b : {} };
|
|
618
|
+
let boundUserId = sanitizeUserId((_c = config.userContext) == null ? void 0 : _c.userId);
|
|
619
|
+
const storageKey = `wireai:analytics:userId:${(_d = config.appId) != null ? _d : "default"}`;
|
|
620
|
+
if (config.storage) {
|
|
621
|
+
void config.storage.getItem(storageKey).then((saved) => {
|
|
622
|
+
if (saved && !boundUserId) boundUserId = saved;
|
|
623
|
+
}).catch(() => {
|
|
624
|
+
});
|
|
625
|
+
}
|
|
626
|
+
const applyContext = (event) => {
|
|
627
|
+
var _a2;
|
|
628
|
+
const resolved = resolveUserContext(userContext, { autoAppVersion: config.appVersion });
|
|
629
|
+
if (resolved.userContext) {
|
|
630
|
+
event.user_context = { ...resolved.userContext, ...(_a2 = event.user_context) != null ? _a2 : {} };
|
|
631
|
+
}
|
|
632
|
+
if (boundUserId && !event.user_id) event.user_id = boundUserId;
|
|
633
|
+
};
|
|
634
|
+
const setUserContext = (partial) => {
|
|
635
|
+
var _a2, _b2;
|
|
636
|
+
if (!partial || typeof partial !== "object") return;
|
|
637
|
+
const mergedExtra = partial.extra || userContext.extra ? { ...(_a2 = userContext.extra) != null ? _a2 : {}, ...(_b2 = partial.extra) != null ? _b2 : {} } : void 0;
|
|
638
|
+
userContext = { ...userContext, ...partial };
|
|
639
|
+
if (mergedExtra) userContext.extra = mergedExtra;
|
|
640
|
+
const uid = sanitizeUserId(partial.userId);
|
|
641
|
+
if (uid) {
|
|
642
|
+
boundUserId = uid;
|
|
643
|
+
if (config.storage) void config.storage.setItem(storageKey, uid).catch(() => {
|
|
644
|
+
});
|
|
645
|
+
}
|
|
646
|
+
};
|
|
453
647
|
const track = (event, props) => {
|
|
454
648
|
if (!event) return;
|
|
455
649
|
const clientEvent = {
|
|
456
650
|
event_type: "app_event",
|
|
457
|
-
session_id:
|
|
651
|
+
session_id: resolveSessionId(),
|
|
458
652
|
question_key: event
|
|
459
653
|
};
|
|
460
654
|
if (props && Object.keys(props).length > 0) clientEvent.meta = JSON.stringify(props);
|
|
461
|
-
|
|
655
|
+
applyContext(clientEvent);
|
|
462
656
|
queue.enqueue(clientEvent);
|
|
463
657
|
};
|
|
464
658
|
const screen = (name, props) => {
|
|
@@ -466,29 +660,37 @@ var createAnalytics = (config, options = {}) => {
|
|
|
466
660
|
const meta = { screen: name, ...props != null ? props : {} };
|
|
467
661
|
const clientEvent = {
|
|
468
662
|
event_type: "app_event",
|
|
469
|
-
session_id:
|
|
663
|
+
session_id: resolveSessionId(),
|
|
470
664
|
question_key: "screen",
|
|
471
665
|
meta: JSON.stringify(meta)
|
|
472
666
|
};
|
|
473
|
-
|
|
667
|
+
applyContext(clientEvent);
|
|
474
668
|
queue.enqueue(clientEvent);
|
|
475
669
|
};
|
|
476
670
|
const identify = (userId, traits) => {
|
|
477
671
|
const clean = sanitizeUserId(userId);
|
|
478
672
|
if (!clean) return;
|
|
479
673
|
boundUserId = clean;
|
|
674
|
+
if (config.storage) {
|
|
675
|
+
void config.storage.setItem(storageKey, clean).catch(() => {
|
|
676
|
+
});
|
|
677
|
+
}
|
|
480
678
|
const clientEvent = {
|
|
481
679
|
event_type: "identify",
|
|
482
|
-
|
|
680
|
+
// Reuse the LIVE per-open session id (see `resolveSessionId`) so the server binds identity to
|
|
681
|
+
// the session it already saw instead of back-filling a phantom `session_started`.
|
|
682
|
+
session_id: resolveSessionId(),
|
|
483
683
|
user_id: clean
|
|
484
684
|
};
|
|
485
685
|
if (traits && Object.keys(traits).length > 0) clientEvent.meta = JSON.stringify(traits);
|
|
686
|
+
applyContext(clientEvent);
|
|
486
687
|
queue.enqueue(clientEvent);
|
|
487
688
|
};
|
|
488
689
|
return {
|
|
489
690
|
track,
|
|
490
691
|
screen,
|
|
491
692
|
identify,
|
|
693
|
+
setUserContext,
|
|
492
694
|
flush: queue.flush,
|
|
493
695
|
notifyOnline: queue.notifyOnline,
|
|
494
696
|
size: queue.size
|
|
@@ -496,7 +698,12 @@ var createAnalytics = (config, options = {}) => {
|
|
|
496
698
|
};
|
|
497
699
|
var useAnalytics = (config, options = {}) => {
|
|
498
700
|
const ref = react.useRef(void 0);
|
|
499
|
-
|
|
701
|
+
const prevKeys = react.useRef("");
|
|
702
|
+
const currentKeys = `${config.serverUrl}|${config.apiKey}|${config.appId}`;
|
|
703
|
+
if (!ref.current || prevKeys.current !== currentKeys) {
|
|
704
|
+
prevKeys.current = currentKeys;
|
|
705
|
+
ref.current = createAnalytics(config, options);
|
|
706
|
+
}
|
|
500
707
|
return ref.current;
|
|
501
708
|
};
|
|
502
709
|
|
|
@@ -506,11 +713,14 @@ exports.createAnalytics = createAnalytics;
|
|
|
506
713
|
exports.createEventQueue = createEventQueue;
|
|
507
714
|
exports.createScreenTracker = createScreenTracker;
|
|
508
715
|
exports.getActiveRouteName = getActiveRouteName;
|
|
716
|
+
exports.getCurrentSessionId = getCurrentSessionId;
|
|
509
717
|
exports.makeSessionId = makeSessionId;
|
|
510
718
|
exports.reportAppEvent = reportAppEvent;
|
|
511
719
|
exports.reportClientEvent = reportClientEvent;
|
|
512
720
|
exports.reportClientEvents = reportClientEvents;
|
|
721
|
+
exports.resetCurrentSessionId = resetCurrentSessionId;
|
|
513
722
|
exports.screenTrackingHandler = screenTrackingHandler;
|
|
723
|
+
exports.setCurrentSessionId = setCurrentSessionId;
|
|
514
724
|
exports.toAnalyticsEvent = toAnalyticsEvent;
|
|
515
725
|
exports.useAnalytics = useAnalytics;
|
|
516
726
|
exports.useScreenTracking = useScreenTracking;
|