@wireai/activation 0.4.0 → 0.8.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 +92 -0
- package/dist/analytics/index.d.mts +19 -2
- package/dist/analytics/index.d.ts +19 -2
- package/dist/analytics/index.js +258 -18
- package/dist/analytics/index.js.map +1 -1
- package/dist/analytics/index.mjs +256 -19
- package/dist/analytics/index.mjs.map +1 -1
- package/dist/{eventQueue-CA1d8Fmn.d.mts → currentSession-d9CrBxwe.d.mts} +152 -16
- package/dist/{eventQueue-CrNB9gzH.d.ts → currentSession-f7LWcdWG.d.ts} +152 -16
- package/dist/index.d.mts +96 -32
- package/dist/index.d.ts +96 -32
- package/dist/index.js +214 -3
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +201 -4
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/WireOnboarding.tsx +4 -3
- package/src/analytics/analyticsFacade.ts +120 -12
- package/src/analytics/contextEnvelope.ts +13 -7
- package/src/analytics/currentSession.ts +35 -0
- package/src/analytics/index.ts +3 -0
- package/src/context/deviceId.ts +43 -0
- package/src/context/userContext.ts +210 -0
- package/src/device/appVersion.ts +103 -0
- package/src/device/deviceContext.ts +31 -6
- package/src/device/deviceModel.ts +93 -0
- package/src/identity/userIdentity.ts +5 -0
- package/src/index.ts +25 -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/types.ts +6 -5
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
|
@@ -18,8 +18,100 @@ Historical entries below the rename keep the old package name on purpose.
|
|
|
18
18
|
- Version reset to `0.1.0` to mark the start of the new package line. The GitHub repository
|
|
19
19
|
stays `chohra-med/wireai-onboarding`.
|
|
20
20
|
|
|
21
|
+
## [Unreleased]
|
|
22
|
+
|
|
23
|
+
### Added: "device" is now fully automatic — auto-minted, persisted per-install `device_key`
|
|
24
|
+
|
|
25
|
+
- The analytics façade (`createAnalytics`) now auto-mints a stable, NON-PII per-install `device_key`
|
|
26
|
+
when the host supplies none via `WireUserContext.deviceKey`. It is minted ONCE (`context/deviceId.ts`
|
|
27
|
+
— `mintDeviceId`, dependency-free time+random, no `uuid`), PERSISTED via the host's existing
|
|
28
|
+
`storage` (`WireOnboardingStorage`) under `deviceIdStorageKey(appId)`, and REUSED on every open;
|
|
29
|
+
with no storage it falls back to an in-memory id. Result: `user_context.device_key` is ALWAYS
|
|
30
|
+
present, so the server's review/questionnaire gating + A/B stickiness (both key on `device_key`)
|
|
31
|
+
work out of the box with zero host wiring. A host-supplied `deviceKey` still WINS (and opts out of
|
|
32
|
+
auto-minting). New exports: `mintDeviceId`, `deviceIdStorageKey`, `AUTO_DEVICE_ID_PREFIX`.
|
|
33
|
+
|
|
34
|
+
### Fixed: `WireUserContext.appVersion` now flows into `device.appVersion` (not just `user_context`)
|
|
35
|
+
|
|
36
|
+
- A host that set the app version ONLY inside `userContext` previously got it in
|
|
37
|
+
`user_context.app_version` but left `device.appVersion` on the auto-detected value — and the
|
|
38
|
+
server's `by_app_version` breakdown reads `device.appVersion`. The façade's context-envelope
|
|
39
|
+
provider now feeds `userContext.appVersion ?? config.appVersion` into `buildContextEnvelope`, so an
|
|
40
|
+
explicit `WireUserContext.appVersion` populates BOTH surfaces (explicit still wins over auto-detect).
|
|
41
|
+
|
|
42
|
+
### Added: best-effort iOS `device.model`
|
|
43
|
+
|
|
44
|
+
- `device.model` was Android-only (`Platform.constants.Model`); iOS `Platform.constants` exposes no
|
|
45
|
+
model. `collectDeviceContext` now fills iOS `model` best-effort via `detectNativeModel`
|
|
46
|
+
(`device/deviceModel.ts`), reading `expo-device`'s `modelName`/`modelId` through the SAME guarded,
|
|
47
|
+
variable-specifier require the kit already uses for `appVersion` — so it adds NO dependency and is
|
|
48
|
+
simply omitted when `expo-device` is absent. It reads a device CLASS (not a unique id / not the
|
|
49
|
+
user-set device name), so it changes no App Privacy / Data Safety declaration. ASYMMETRY documented
|
|
50
|
+
in the `DeviceContext.model` TSDoc: without `expo-device`, iOS `model` stays omitted; no native
|
|
51
|
+
dependency is added for it.
|
|
52
|
+
|
|
53
|
+
- Backward compatible: no existing export renamed or removed; zero new dependencies. The one
|
|
54
|
+
behavior change is intentional — events now carry an auto `device_key` when the host omits one.
|
|
55
|
+
|
|
21
56
|
## [0.7.0] — Unreleased
|
|
22
57
|
|
|
58
|
+
### Added: `WireUserContext` — one rich user-context object → every event's `user_context`
|
|
59
|
+
|
|
60
|
+
- New `context/userContext.ts` + public export `WireUserContext`: a single, extensible object a host
|
|
61
|
+
passes ONCE (`appVersion?`, `deviceKey?`, `userId?`, `userEmail?` + `hashEmail?`, `extra?`) that the
|
|
62
|
+
kit flows into every analytics event. Pure `resolveUserContext(ctx, { autoAppVersion })` merges it
|
|
63
|
+
with ONE precedence rule — an explicit field WINS over the auto-detected `device`/`appVersion` —
|
|
64
|
+
and omits missing fields (never sent empty). Also exported: `namespaceExtra`, `hashEmailFnv1a`,
|
|
65
|
+
`isWireScalar`, `RESERVED_USER_CONTEXT_KEYS`, `EXTRA_KEY_PREFIX`.
|
|
66
|
+
- Deliberate bucket separation (nothing leaks): `userId` → the event's TOP-LEVEL opaque `user_id`
|
|
67
|
+
(via `sanitizeUserId`, capped 128) and NEVER the `user_context` bucket; `deviceKey` →
|
|
68
|
+
`user_context.device_key` (NOT `session_id`); `appVersion` → `user_context.app_version`; `extra`
|
|
69
|
+
entries are coerced to `string | number | boolean` (non-scalars/NaN/Infinity dropped) and
|
|
70
|
+
NAMESPACED under a `custom.` key prefix so a host extra can never collide with a reserved key.
|
|
71
|
+
- **`userEmail` is OPT-IN PII in its OWN field** (`user_context.user_email`) — never merged into
|
|
72
|
+
`userId`. The kit NEVER auto-collects it; a host passes it only with the user's consent (EU users:
|
|
73
|
+
personal data). Set `hashEmail: true` to send a dependency-free FNV-1a fold instead of the raw
|
|
74
|
+
address (`user_context.user_email_hashed: true`). NOTE: FNV-1a is a lightweight non-cryptographic
|
|
75
|
+
fold; for a cryptographic digest, pre-hash host-side (e.g. `expo-crypto` SHA-256) and pass that as
|
|
76
|
+
`userEmail` with `hashEmail` falsy. Documented in the field's TSDoc.
|
|
77
|
+
- The analytics façade (`createAnalytics`) now accepts `userContext` at init AND exposes
|
|
78
|
+
`setUserContext(partial)` so a host can attach `userId`/`userEmail` at LOGIN without remounting;
|
|
79
|
+
both flow into subsequent `track`/`screen`/`identify` events' `user_context`. Backward compatible:
|
|
80
|
+
`createAnalytics({ serverUrl, apiKey })` with no `userContext` behaves exactly as before.
|
|
81
|
+
|
|
82
|
+
### Fixed: `identify` + host app-events reuse the live per-open session (kills the phantom-session)
|
|
83
|
+
|
|
84
|
+
- New `analytics/currentSession.ts` (`getCurrentSessionId` / `setCurrentSessionId` /
|
|
85
|
+
`resetCurrentSessionId`): a process-local registry of the CURRENT per-open `session_id`.
|
|
86
|
+
`reportSessionStart` registers each per-open id here on every app-open.
|
|
87
|
+
- The façade's `identify` (and `track`/`screen` app-events) now source their `session_id` from
|
|
88
|
+
`config.sessionId ?? getCurrentSessionId() ?? <stable per-instance id>` — so identity binds to the
|
|
89
|
+
session the server ALREADY saw (via `app.session_started`) instead of minting a fresh id the server
|
|
90
|
+
back-fills into a synthetic session. This is what inflated the Morrow/Myelino session counts. An
|
|
91
|
+
explicit `config.sessionId` still freezes the id (opt-out). This is also the kit half of the #205
|
|
92
|
+
"align all app-events to the per-open session" follow-up. **No server change required** for the
|
|
93
|
+
normal flow (session-start fires on mount, identify at login); the only residual is an `identify`
|
|
94
|
+
fired before any open this launch, which falls back to the instance id.
|
|
95
|
+
- `identifyOnboarding` gains a last-resort fallback to `getCurrentSessionId()` when no `contextId`
|
|
96
|
+
is captured or resolvable from storage, so a post-flow identify binds to the live open session
|
|
97
|
+
instead of no-oping. An explicit `contextId` (or a stored one) still wins.
|
|
98
|
+
|
|
99
|
+
### Added: `device.appVersion` auto-detection — populate the `by_app_version` breakdown by default
|
|
100
|
+
|
|
101
|
+
- `collectDeviceContext()` now carries a best-effort `appVersion` so the analytics
|
|
102
|
+
`by_app_version` funnel breakdown works out of the box, instead of staying empty until a host
|
|
103
|
+
remembers to pass `config.appVersion`. New `device/appVersion.ts` reads the version from
|
|
104
|
+
`expo-constants` (`expoConfig.version`, then `nativeAppVersion`) or `expo-application`
|
|
105
|
+
(`nativeApplicationVersion`) when present.
|
|
106
|
+
- ADDS NO DEPENDENCY: the Expo modules are read through a guarded, variable-specifier `require`,
|
|
107
|
+
so a host WITHOUT them (e.g. bare React Native) never fails to bundle — the read simply yields
|
|
108
|
+
`undefined`. Nothing is added to `package.json`; no peer is forced.
|
|
109
|
+
- An explicit host `config.appVersion` still WINS everywhere (the `WireOnboarding` device memo, the
|
|
110
|
+
`useSessionStart` / `useLifecycleEvents` hooks, and the analytics context envelope overwrite the
|
|
111
|
+
auto-detected value with the host's). The context envelope also mirrors the effective version
|
|
112
|
+
onto its outer `appVersion` scalar so `user_context.app_version` populates on facade events too.
|
|
113
|
+
- New public export `detectAppVersion()` (alongside `collectDeviceContext`). Never throws.
|
|
114
|
+
|
|
23
115
|
### Added: session mapping — know when a user opens the app again
|
|
24
116
|
|
|
25
117
|
- 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-d9CrBxwe.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-d9CrBxwe.mjs';
|
|
4
4
|
import '../types-A6pTxIZV.mjs';
|
|
5
5
|
import '../types-BKfpdZzX.mjs';
|
|
6
6
|
import '../types-GL_hQ0TN.mjs';
|
|
@@ -120,6 +120,17 @@ 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.
|
|
127
|
+
*
|
|
128
|
+
* NOTE on `deviceKey`: you do NOT need to supply one. When omitted, the kit auto-mints a stable,
|
|
129
|
+
* non-PII per-install `device_key`, persists it via `storage`, and reuses it every open (in-memory
|
|
130
|
+
* fallback without storage) — so `user_context.device_key` is ALWAYS present for the server's
|
|
131
|
+
* review/questionnaire gating + A/B stickiness. Supply `deviceKey` only to use your OWN id (it wins).
|
|
132
|
+
*/
|
|
133
|
+
userContext?: WireUserContext;
|
|
123
134
|
};
|
|
124
135
|
/** Optional queue tuning knobs, forwarded verbatim to {@link createEventQueue}. */
|
|
125
136
|
type AnalyticsOptions = Partial<Pick<EventQueueOptions, "maxSize" | "batchSize" | "baseBackoffMs" | "maxBackoffMs" | "maxRetries">>;
|
|
@@ -135,6 +146,12 @@ type Analytics = {
|
|
|
135
146
|
screen(name: string, props?: AnalyticsProps): void;
|
|
136
147
|
/** Bind the host's opaque user id (per-session, in-memory) and emit an `identify` event. */
|
|
137
148
|
identify(userId: string, traits?: AnalyticsProps): void;
|
|
149
|
+
/**
|
|
150
|
+
* Update the {@link WireUserContext} after init (e.g. attach `userId`/`userEmail` at login). Shallow
|
|
151
|
+
* merges the partial over the current context (`extra` is deep-merged); a supplied `userId` also
|
|
152
|
+
* binds like {@link identify}. Takes effect on subsequent events. Fire-and-forget.
|
|
153
|
+
*/
|
|
154
|
+
setUserContext(partial: Partial<WireUserContext>): void;
|
|
138
155
|
/** Attempt an immediate drain of the pending buffer. Fire-and-forget. */
|
|
139
156
|
flush(): void;
|
|
140
157
|
/** 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-f7LWcdWG.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-f7LWcdWG.js';
|
|
4
4
|
import '../types-BhpXJGlg.js';
|
|
5
5
|
import '../types-BKfpdZzX.js';
|
|
6
6
|
import '../types-GL_hQ0TN.js';
|
|
@@ -120,6 +120,17 @@ 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.
|
|
127
|
+
*
|
|
128
|
+
* NOTE on `deviceKey`: you do NOT need to supply one. When omitted, the kit auto-mints a stable,
|
|
129
|
+
* non-PII per-install `device_key`, persists it via `storage`, and reuses it every open (in-memory
|
|
130
|
+
* fallback without storage) — so `user_context.device_key` is ALWAYS present for the server's
|
|
131
|
+
* review/questionnaire gating + A/B stickiness. Supply `deviceKey` only to use your OWN id (it wins).
|
|
132
|
+
*/
|
|
133
|
+
userContext?: WireUserContext;
|
|
123
134
|
};
|
|
124
135
|
/** Optional queue tuning knobs, forwarded verbatim to {@link createEventQueue}. */
|
|
125
136
|
type AnalyticsOptions = Partial<Pick<EventQueueOptions, "maxSize" | "batchSize" | "baseBackoffMs" | "maxBackoffMs" | "maxRetries">>;
|
|
@@ -135,6 +146,12 @@ type Analytics = {
|
|
|
135
146
|
screen(name: string, props?: AnalyticsProps): void;
|
|
136
147
|
/** Bind the host's opaque user id (per-session, in-memory) and emit an `identify` event. */
|
|
137
148
|
identify(userId: string, traits?: AnalyticsProps): void;
|
|
149
|
+
/**
|
|
150
|
+
* Update the {@link WireUserContext} after init (e.g. attach `userId`/`userEmail` at login). Shallow
|
|
151
|
+
* merges the partial over the current context (`extra` is deep-merged); a supplied `userId` also
|
|
152
|
+
* binds like {@link identify}. Takes effect on subsequent events. Fire-and-forget.
|
|
153
|
+
*/
|
|
154
|
+
setUserContext(partial: Partial<WireUserContext>): void;
|
|
138
155
|
/** Attempt an immediate drain of the pending buffer. Fire-and-forget. */
|
|
139
156
|
flush(): void;
|
|
140
157
|
/** 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;
|
|
@@ -157,6 +164,98 @@ var toAnalyticsEvent = (event) => {
|
|
|
157
164
|
}
|
|
158
165
|
}
|
|
159
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/deviceModel.ts
|
|
218
|
+
var coerceModel = (value) => {
|
|
219
|
+
if (typeof value !== "string") return void 0;
|
|
220
|
+
const trimmed = value.trim();
|
|
221
|
+
return trimmed.length > 0 ? trimmed : void 0;
|
|
222
|
+
};
|
|
223
|
+
var runtimeRequire2 = (moduleName) => {
|
|
224
|
+
try {
|
|
225
|
+
if (typeof __require !== "function") return void 0;
|
|
226
|
+
return __require(moduleName);
|
|
227
|
+
} catch {
|
|
228
|
+
return void 0;
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
var interop2 = (mod) => {
|
|
232
|
+
if (!mod || typeof mod !== "object") return void 0;
|
|
233
|
+
const def = mod.default;
|
|
234
|
+
if (def && typeof def === "object") return def;
|
|
235
|
+
return mod;
|
|
236
|
+
};
|
|
237
|
+
var safeInterop2 = (requireModule, moduleName) => {
|
|
238
|
+
try {
|
|
239
|
+
return interop2(requireModule(moduleName));
|
|
240
|
+
} catch {
|
|
241
|
+
return void 0;
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
var detectNativeModel = (requireModule = runtimeRequire2) => {
|
|
245
|
+
try {
|
|
246
|
+
const device = safeInterop2(requireModule, "expo-device");
|
|
247
|
+
if (device) {
|
|
248
|
+
const modelName = coerceModel(device.modelName);
|
|
249
|
+
if (modelName) return modelName;
|
|
250
|
+
const modelId = coerceModel(device.modelId);
|
|
251
|
+
if (modelId) return modelId;
|
|
252
|
+
}
|
|
253
|
+
} catch {
|
|
254
|
+
}
|
|
255
|
+
return void 0;
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
// src/device/deviceContext.ts
|
|
160
259
|
var deriveFormFactor = (iosIdiom, width, height) => {
|
|
161
260
|
if (iosIdiom === "pad") return "tablet";
|
|
162
261
|
if (iosIdiom === "phone") return "phone";
|
|
@@ -202,6 +301,8 @@ var collectDeviceContext = () => {
|
|
|
202
301
|
ctx.interfaceIdiom = idiom;
|
|
203
302
|
iosIdiom = idiom;
|
|
204
303
|
}
|
|
304
|
+
const iosModel = detectNativeModel();
|
|
305
|
+
if (iosModel) ctx.model = iosModel;
|
|
205
306
|
}
|
|
206
307
|
} catch {
|
|
207
308
|
}
|
|
@@ -226,16 +327,20 @@ var collectDeviceContext = () => {
|
|
|
226
327
|
if (resolved.timeZone) ctx.timeZone = resolved.timeZone;
|
|
227
328
|
} catch {
|
|
228
329
|
}
|
|
330
|
+
const appVersion = detectAppVersion();
|
|
331
|
+
if (appVersion) ctx.appVersion = appVersion;
|
|
229
332
|
return ctx;
|
|
230
333
|
};
|
|
231
334
|
|
|
232
335
|
// src/analytics/contextEnvelope.ts
|
|
233
336
|
var buildContextEnvelope = (input = {}) => {
|
|
337
|
+
var _a;
|
|
234
338
|
const device = { ...collectDeviceContext() };
|
|
235
|
-
if (input.appVersion
|
|
339
|
+
if (input.appVersion) device.appVersion = input.appVersion;
|
|
340
|
+
const effectiveAppVersion = (_a = input.appVersion) != null ? _a : device.appVersion;
|
|
236
341
|
const envelope = { device };
|
|
237
342
|
if (input.sessionId) envelope.sessionId = input.sessionId;
|
|
238
|
-
if (
|
|
343
|
+
if (effectiveAppVersion) envelope.appVersion = effectiveAppVersion;
|
|
239
344
|
if (input.appBuild) envelope.appBuild = input.appBuild;
|
|
240
345
|
if (input.networkType) envelope.networkType = input.networkType;
|
|
241
346
|
return envelope;
|
|
@@ -449,6 +554,16 @@ var createEventQueue = (options) => {
|
|
|
449
554
|
return { enqueue, flush, notifyOnline, size };
|
|
450
555
|
};
|
|
451
556
|
|
|
557
|
+
// src/analytics/currentSession.ts
|
|
558
|
+
var _currentSessionId;
|
|
559
|
+
var setCurrentSessionId = (id) => {
|
|
560
|
+
if (typeof id === "string" && id.length > 0) _currentSessionId = id;
|
|
561
|
+
};
|
|
562
|
+
var getCurrentSessionId = () => _currentSessionId;
|
|
563
|
+
var resetCurrentSessionId = () => {
|
|
564
|
+
_currentSessionId = void 0;
|
|
565
|
+
};
|
|
566
|
+
|
|
452
567
|
// src/identity/userIdentity.ts
|
|
453
568
|
var USER_ID_MAX_LENGTH = 128;
|
|
454
569
|
var sanitizeUserId = (raw) => {
|
|
@@ -458,16 +573,109 @@ var sanitizeUserId = (raw) => {
|
|
|
458
573
|
return trimmed.length > USER_ID_MAX_LENGTH ? trimmed.slice(0, USER_ID_MAX_LENGTH) : trimmed;
|
|
459
574
|
};
|
|
460
575
|
|
|
576
|
+
// src/context/userContext.ts
|
|
577
|
+
var EXTRA_KEY_PREFIX = "custom.";
|
|
578
|
+
var isWireScalar = (value) => {
|
|
579
|
+
const t = typeof value;
|
|
580
|
+
if (t === "string" || t === "boolean") return true;
|
|
581
|
+
if (t === "number") return Number.isFinite(value);
|
|
582
|
+
return false;
|
|
583
|
+
};
|
|
584
|
+
var hashEmailFnv1a = (email) => {
|
|
585
|
+
const normalized = email.trim().toLowerCase();
|
|
586
|
+
let hash = 2166136261;
|
|
587
|
+
for (let i = 0; i < normalized.length; i++) {
|
|
588
|
+
hash ^= normalized.charCodeAt(i);
|
|
589
|
+
hash = Math.imul(hash, 16777619);
|
|
590
|
+
}
|
|
591
|
+
return (hash >>> 0).toString(16).padStart(8, "0");
|
|
592
|
+
};
|
|
593
|
+
var cleanString = (value) => {
|
|
594
|
+
if (typeof value !== "string") return void 0;
|
|
595
|
+
const trimmed = value.trim();
|
|
596
|
+
return trimmed.length > 0 ? trimmed : void 0;
|
|
597
|
+
};
|
|
598
|
+
var namespaceExtra = (extra) => {
|
|
599
|
+
const out = {};
|
|
600
|
+
if (!extra || typeof extra !== "object") return out;
|
|
601
|
+
for (const [key, value] of Object.entries(extra)) {
|
|
602
|
+
const cleanKey = cleanString(key);
|
|
603
|
+
if (!cleanKey) continue;
|
|
604
|
+
if (!isWireScalar(value)) continue;
|
|
605
|
+
out[`${EXTRA_KEY_PREFIX}${cleanKey}`] = value;
|
|
606
|
+
}
|
|
607
|
+
return out;
|
|
608
|
+
};
|
|
609
|
+
var resolveUserContext = (ctx = {}, opts = {}) => {
|
|
610
|
+
var _a;
|
|
611
|
+
const result = {};
|
|
612
|
+
const bucket = {};
|
|
613
|
+
const userId = sanitizeUserId(ctx.userId);
|
|
614
|
+
if (userId) result.userId = userId;
|
|
615
|
+
const deviceKey = cleanString(ctx.deviceKey);
|
|
616
|
+
if (deviceKey) {
|
|
617
|
+
result.deviceKey = deviceKey;
|
|
618
|
+
bucket.device_key = deviceKey;
|
|
619
|
+
}
|
|
620
|
+
const appVersion = (_a = cleanString(ctx.appVersion)) != null ? _a : cleanString(opts.autoAppVersion);
|
|
621
|
+
if (appVersion) {
|
|
622
|
+
result.appVersion = appVersion;
|
|
623
|
+
bucket.app_version = appVersion;
|
|
624
|
+
}
|
|
625
|
+
const email = cleanString(ctx.userEmail);
|
|
626
|
+
if (email) {
|
|
627
|
+
if (ctx.hashEmail) {
|
|
628
|
+
bucket.user_email = hashEmailFnv1a(email);
|
|
629
|
+
bucket.user_email_hashed = true;
|
|
630
|
+
} else {
|
|
631
|
+
bucket.user_email = email;
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
Object.assign(bucket, namespaceExtra(ctx.extra));
|
|
635
|
+
if (Object.keys(bucket).length > 0) result.userContext = bucket;
|
|
636
|
+
return result;
|
|
637
|
+
};
|
|
638
|
+
|
|
639
|
+
// src/context/deviceId.ts
|
|
640
|
+
var AUTO_DEVICE_ID_PREFIX = "wdev_";
|
|
641
|
+
var deviceIdStorageKey = (appId) => `wireai:analytics:deviceKey:${appId != null ? appId : "default"}`;
|
|
642
|
+
var randomChunk = () => Math.floor(Math.random() * 4294967296).toString(36).padStart(6, "0");
|
|
643
|
+
var mintDeviceId = () => {
|
|
644
|
+
const time = Date.now().toString(36);
|
|
645
|
+
return `${AUTO_DEVICE_ID_PREFIX}${time}_${randomChunk()}${randomChunk()}`;
|
|
646
|
+
};
|
|
647
|
+
|
|
461
648
|
// src/analytics/analyticsFacade.ts
|
|
462
649
|
var createAnalytics = (config, options = {}) => {
|
|
463
|
-
var _a, _b;
|
|
464
|
-
const
|
|
465
|
-
const
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
650
|
+
var _a, _b, _c, _d, _e;
|
|
651
|
+
const instanceSessionId = (_a = config.sessionId) != null ? _a : makeSessionId();
|
|
652
|
+
const resolveSessionId = () => {
|
|
653
|
+
var _a2, _b2;
|
|
654
|
+
return (_b2 = (_a2 = config.sessionId) != null ? _a2 : getCurrentSessionId()) != null ? _b2 : instanceSessionId;
|
|
655
|
+
};
|
|
656
|
+
let userContext = { ...(_b = config.userContext) != null ? _b : {} };
|
|
657
|
+
const hostDeviceKeyAtInit = typeof ((_c = config.userContext) == null ? void 0 : _c.deviceKey) === "string" && config.userContext.deviceKey.trim() ? config.userContext.deviceKey.trim() : void 0;
|
|
658
|
+
let autoDeviceKey = mintDeviceId();
|
|
659
|
+
if (config.storage && !hostDeviceKeyAtInit) {
|
|
660
|
+
const storage = config.storage;
|
|
661
|
+
const deviceKey = deviceIdStorageKey(config.appId);
|
|
662
|
+
void storage.getItem(deviceKey).then((saved) => {
|
|
663
|
+
const persisted = typeof saved === "string" && saved.trim() ? saved.trim() : void 0;
|
|
664
|
+
if (persisted) autoDeviceKey = persisted;
|
|
665
|
+
else void storage.setItem(deviceKey, autoDeviceKey).catch(() => {
|
|
666
|
+
});
|
|
667
|
+
}).catch(() => {
|
|
668
|
+
});
|
|
669
|
+
}
|
|
670
|
+
const envelope = () => {
|
|
671
|
+
var _a2;
|
|
672
|
+
return buildContextEnvelope({
|
|
673
|
+
sessionId: resolveSessionId(),
|
|
674
|
+
appVersion: (_a2 = userContext.appVersion) != null ? _a2 : config.appVersion,
|
|
675
|
+
appBuild: config.appBuild,
|
|
676
|
+
networkType: config.networkType
|
|
677
|
+
});
|
|
678
|
+
};
|
|
471
679
|
const queue = createEventQueue({
|
|
472
680
|
target: { serverUrl: config.serverUrl, apiKey: config.apiKey },
|
|
473
681
|
storage: config.storage,
|
|
@@ -475,23 +683,48 @@ var createAnalytics = (config, options = {}) => {
|
|
|
475
683
|
envelope,
|
|
476
684
|
...options
|
|
477
685
|
});
|
|
478
|
-
let boundUserId;
|
|
479
|
-
const storageKey = `wireai:analytics:userId:${(
|
|
686
|
+
let boundUserId = sanitizeUserId((_d = config.userContext) == null ? void 0 : _d.userId);
|
|
687
|
+
const storageKey = `wireai:analytics:userId:${(_e = config.appId) != null ? _e : "default"}`;
|
|
480
688
|
if (config.storage) {
|
|
481
689
|
void config.storage.getItem(storageKey).then((saved) => {
|
|
482
|
-
if (saved) boundUserId = saved;
|
|
690
|
+
if (saved && !boundUserId) boundUserId = saved;
|
|
483
691
|
}).catch(() => {
|
|
484
692
|
});
|
|
485
693
|
}
|
|
694
|
+
const applyContext = (event) => {
|
|
695
|
+
var _a2;
|
|
696
|
+
const hostDeviceKey = typeof userContext.deviceKey === "string" && userContext.deviceKey.trim() ? userContext.deviceKey : void 0;
|
|
697
|
+
const resolved = resolveUserContext(
|
|
698
|
+
{ ...userContext, deviceKey: hostDeviceKey != null ? hostDeviceKey : autoDeviceKey },
|
|
699
|
+
{ autoAppVersion: config.appVersion }
|
|
700
|
+
);
|
|
701
|
+
if (resolved.userContext) {
|
|
702
|
+
event.user_context = { ...resolved.userContext, ...(_a2 = event.user_context) != null ? _a2 : {} };
|
|
703
|
+
}
|
|
704
|
+
if (boundUserId && !event.user_id) event.user_id = boundUserId;
|
|
705
|
+
};
|
|
706
|
+
const setUserContext = (partial) => {
|
|
707
|
+
var _a2, _b2;
|
|
708
|
+
if (!partial || typeof partial !== "object") return;
|
|
709
|
+
const mergedExtra = partial.extra || userContext.extra ? { ...(_a2 = userContext.extra) != null ? _a2 : {}, ...(_b2 = partial.extra) != null ? _b2 : {} } : void 0;
|
|
710
|
+
userContext = { ...userContext, ...partial };
|
|
711
|
+
if (mergedExtra) userContext.extra = mergedExtra;
|
|
712
|
+
const uid = sanitizeUserId(partial.userId);
|
|
713
|
+
if (uid) {
|
|
714
|
+
boundUserId = uid;
|
|
715
|
+
if (config.storage) void config.storage.setItem(storageKey, uid).catch(() => {
|
|
716
|
+
});
|
|
717
|
+
}
|
|
718
|
+
};
|
|
486
719
|
const track = (event, props) => {
|
|
487
720
|
if (!event) return;
|
|
488
721
|
const clientEvent = {
|
|
489
722
|
event_type: "app_event",
|
|
490
|
-
session_id:
|
|
723
|
+
session_id: resolveSessionId(),
|
|
491
724
|
question_key: event
|
|
492
725
|
};
|
|
493
726
|
if (props && Object.keys(props).length > 0) clientEvent.meta = JSON.stringify(props);
|
|
494
|
-
|
|
727
|
+
applyContext(clientEvent);
|
|
495
728
|
queue.enqueue(clientEvent);
|
|
496
729
|
};
|
|
497
730
|
const screen = (name, props) => {
|
|
@@ -499,11 +732,11 @@ var createAnalytics = (config, options = {}) => {
|
|
|
499
732
|
const meta = { screen: name, ...props != null ? props : {} };
|
|
500
733
|
const clientEvent = {
|
|
501
734
|
event_type: "app_event",
|
|
502
|
-
session_id:
|
|
735
|
+
session_id: resolveSessionId(),
|
|
503
736
|
question_key: "screen",
|
|
504
737
|
meta: JSON.stringify(meta)
|
|
505
738
|
};
|
|
506
|
-
|
|
739
|
+
applyContext(clientEvent);
|
|
507
740
|
queue.enqueue(clientEvent);
|
|
508
741
|
};
|
|
509
742
|
const identify = (userId, traits) => {
|
|
@@ -516,16 +749,20 @@ var createAnalytics = (config, options = {}) => {
|
|
|
516
749
|
}
|
|
517
750
|
const clientEvent = {
|
|
518
751
|
event_type: "identify",
|
|
519
|
-
|
|
752
|
+
// Reuse the LIVE per-open session id (see `resolveSessionId`) so the server binds identity to
|
|
753
|
+
// the session it already saw instead of back-filling a phantom `session_started`.
|
|
754
|
+
session_id: resolveSessionId(),
|
|
520
755
|
user_id: clean
|
|
521
756
|
};
|
|
522
757
|
if (traits && Object.keys(traits).length > 0) clientEvent.meta = JSON.stringify(traits);
|
|
758
|
+
applyContext(clientEvent);
|
|
523
759
|
queue.enqueue(clientEvent);
|
|
524
760
|
};
|
|
525
761
|
return {
|
|
526
762
|
track,
|
|
527
763
|
screen,
|
|
528
764
|
identify,
|
|
765
|
+
setUserContext,
|
|
529
766
|
flush: queue.flush,
|
|
530
767
|
notifyOnline: queue.notifyOnline,
|
|
531
768
|
size: queue.size
|
|
@@ -548,11 +785,14 @@ exports.createAnalytics = createAnalytics;
|
|
|
548
785
|
exports.createEventQueue = createEventQueue;
|
|
549
786
|
exports.createScreenTracker = createScreenTracker;
|
|
550
787
|
exports.getActiveRouteName = getActiveRouteName;
|
|
788
|
+
exports.getCurrentSessionId = getCurrentSessionId;
|
|
551
789
|
exports.makeSessionId = makeSessionId;
|
|
552
790
|
exports.reportAppEvent = reportAppEvent;
|
|
553
791
|
exports.reportClientEvent = reportClientEvent;
|
|
554
792
|
exports.reportClientEvents = reportClientEvents;
|
|
793
|
+
exports.resetCurrentSessionId = resetCurrentSessionId;
|
|
555
794
|
exports.screenTrackingHandler = screenTrackingHandler;
|
|
795
|
+
exports.setCurrentSessionId = setCurrentSessionId;
|
|
556
796
|
exports.toAnalyticsEvent = toAnalyticsEvent;
|
|
557
797
|
exports.useAnalytics = useAnalytics;
|
|
558
798
|
exports.useScreenTracking = useScreenTracking;
|