@wireai/activation 0.2.0 → 0.3.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/dist/analytics/index.d.mts +69 -2
- package/dist/analytics/index.d.ts +69 -2
- package/dist/analytics/index.js +79 -0
- package/dist/analytics/index.js.map +1 -1
- package/dist/analytics/index.mjs +78 -1
- package/dist/analytics/index.mjs.map +1 -1
- package/dist/{eventQueue-CxKi7Qd5.d.mts → eventQueue-CA1d8Fmn.d.mts} +7 -3
- package/dist/{eventQueue-rV1dtJJR.d.ts → eventQueue-CrNB9gzH.d.ts} +7 -3
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js.map +1 -1
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/analytics/analyticsFacade.ts +171 -0
- package/src/analytics/index.ts +12 -0
- package/src/analytics/reportClientEvent.ts +11 -2
- package/src/analytics/useAnalytics.ts +36 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wireai/activation",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Premium, fully-themable drop-in AI onboarding kit for React Native / Expo, on top of the open-source wireai-rn SDK.",
|
|
6
6
|
"author": "Malik Chohra <malik@getwireai.com>",
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* analyticsFacade — a Segment/PostHog-shaped developer API (`track` / `screen` / `identify`)
|
|
3
|
+
* over the kit's OWN offline-first event queue. One package, one key, one line:
|
|
4
|
+
*
|
|
5
|
+
* const analytics = createAnalytics({ serverUrl, apiKey, storage });
|
|
6
|
+
* analytics.track("content_share", { source: "feed" });
|
|
7
|
+
* analytics.screen("Home", { tab: "explore" });
|
|
8
|
+
* analytics.identify("u_123", { plan: "pro" });
|
|
9
|
+
*
|
|
10
|
+
* WHY a façade: the primitives already exist (`createEventQueue` for durable offline-first
|
|
11
|
+
* transport, `buildContextEnvelope` for the non-PII device context, the `identify` client-event
|
|
12
|
+
* contract for user binding), but a host had to wire them together by hand. This composes them
|
|
13
|
+
* into the familiar analytics-SDK surface so a consumer gets the ergonomics with NO second
|
|
14
|
+
* install and NO second key — the same `{ serverUrl, apiKey }` creds as onboarding.
|
|
15
|
+
*
|
|
16
|
+
* ROUTING: every call builds a `ClientEvent` and `enqueue`s it. The queue stamps the context
|
|
17
|
+
* envelope (device + host scalars), persists offline, batches, retries, and dequeues on ack —
|
|
18
|
+
* so `track`/`screen`/`identify` inherit offline-first durability for free. `screen` routes
|
|
19
|
+
* through the SAME queue rather than the direct `reportAppEvent` POST (a deliberate upgrade:
|
|
20
|
+
* screen views survive being offline too).
|
|
21
|
+
*
|
|
22
|
+
* DEPENDENCY-FREE + TREE-SHAKEABLE: this module imports ONLY the queue, the envelope builder,
|
|
23
|
+
* the client-event types + session-id seed, and the identity sanitizer. It reaches NO
|
|
24
|
+
* onboarding / showcase / review UI, so an analytics-only consumer bundles none of it. React is
|
|
25
|
+
* absent here on purpose — the optional React glue is the thin `useAnalytics` hook.
|
|
26
|
+
*
|
|
27
|
+
* FIRE-AND-FORGET: no method throws into the UI or blocks — the queue already guarantees that.
|
|
28
|
+
*/
|
|
29
|
+
import { buildContextEnvelope, type ContextEnvelope } from "./contextEnvelope";
|
|
30
|
+
import { createEventQueue, type EventQueue, type EventQueueOptions } from "./eventQueue";
|
|
31
|
+
import { makeSessionId, type ClientEvent } from "./reportClientEvent";
|
|
32
|
+
import { sanitizeUserId } from "../identity/userIdentity";
|
|
33
|
+
|
|
34
|
+
/** Arbitrary non-PII event properties. Serialized to the event's `meta` (a JSON string) on the wire. */
|
|
35
|
+
export type AnalyticsProps = Record<string, unknown>;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Tenant transport + context inputs for {@link createAnalytics}. `serverUrl`/`apiKey` are the
|
|
39
|
+
* SAME creds as onboarding (never a second key). The rest feed the context envelope + the queue's
|
|
40
|
+
* offline persistence — all optional.
|
|
41
|
+
*/
|
|
42
|
+
export type CreateAnalyticsConfig = {
|
|
43
|
+
/** Base server URL (same as `WireOnboardingConfig.serverUrl`); `/v1/events` is appended. */
|
|
44
|
+
serverUrl: string;
|
|
45
|
+
/** Tenant API key; sent as `Authorization: Bearer`. */
|
|
46
|
+
apiKey: string;
|
|
47
|
+
/**
|
|
48
|
+
* Correlation id shared by every event from this instance (and the `identify` event). Defaults
|
|
49
|
+
* to a fresh `makeSessionId()` at creation so all events agree on one id per analytics instance.
|
|
50
|
+
*/
|
|
51
|
+
sessionId?: string;
|
|
52
|
+
/** Tenant/app id used to namespace the queue's default storage key (`wireai:evtq:<appId>`). */
|
|
53
|
+
appId?: string;
|
|
54
|
+
/**
|
|
55
|
+
* Host persistence (AsyncStorage-compatible subset) for offline-first durability. When omitted,
|
|
56
|
+
* the queue runs in the documented in-memory mode (survives re-renders, not app kills).
|
|
57
|
+
*/
|
|
58
|
+
storage?: EventQueueOptions["storage"];
|
|
59
|
+
/** Host app version, e.g. "1.4.2" (host-injected; stamped onto every event's context). */
|
|
60
|
+
appVersion?: string;
|
|
61
|
+
/** Host native build number, e.g. "412" (host-injected). */
|
|
62
|
+
appBuild?: string;
|
|
63
|
+
/** Host connectivity signal, e.g. "wifi" | "cellular" — read fresh per event via the provider. */
|
|
64
|
+
networkType?: string;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
/** Optional queue tuning knobs, forwarded verbatim to {@link createEventQueue}. */
|
|
68
|
+
export type AnalyticsOptions = Partial<
|
|
69
|
+
Pick<EventQueueOptions, "maxSize" | "batchSize" | "baseBackoffMs" | "maxBackoffMs" | "maxRetries">
|
|
70
|
+
>;
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* The developer-facing analytics surface. `track`/`screen`/`identify` are the Segment/PostHog-shaped
|
|
74
|
+
* API; `flush`/`notifyOnline`/`size` expose the underlying queue so a host can drive reconnect
|
|
75
|
+
* draining (load-bearing for offline-first) and inspect the pending buffer.
|
|
76
|
+
*/
|
|
77
|
+
export type Analytics = {
|
|
78
|
+
/** Record an in-app event: `event_type='app_event'`, `question_key=<event>`, `props`→`meta`. */
|
|
79
|
+
track(event: string, props?: AnalyticsProps): void;
|
|
80
|
+
/** Record a screen view: `event_type='app_event'`, `question_key='screen'`, `meta={ screen, ...props }`. */
|
|
81
|
+
screen(name: string, props?: AnalyticsProps): void;
|
|
82
|
+
/** Bind the host's opaque user id (per-session, in-memory) and emit an `identify` event. */
|
|
83
|
+
identify(userId: string, traits?: AnalyticsProps): void;
|
|
84
|
+
/** Attempt an immediate drain of the pending buffer. Fire-and-forget. */
|
|
85
|
+
flush(): void;
|
|
86
|
+
/** Host reconnect signal: reset backoff and drain now. Fire-and-forget. */
|
|
87
|
+
notifyOnline(): void;
|
|
88
|
+
/** Current pending (in-memory) count. */
|
|
89
|
+
size(): number;
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Create a bound analytics instance. Seeds one correlation `sessionId`, builds an offline-first
|
|
94
|
+
* queue over the tenant transport, and passes a fresh-per-event context envelope PROVIDER so the
|
|
95
|
+
* connectivity type is read at enqueue time. The bound user id starts unset (see `identify`).
|
|
96
|
+
*/
|
|
97
|
+
export const createAnalytics = (
|
|
98
|
+
config: CreateAnalyticsConfig,
|
|
99
|
+
options: AnalyticsOptions = {},
|
|
100
|
+
): Analytics => {
|
|
101
|
+
const sessionId = config.sessionId ?? makeSessionId();
|
|
102
|
+
|
|
103
|
+
// A provider (not a fixed value) so `networkType` is evaluated fresh on every enqueue.
|
|
104
|
+
const envelope = (): ContextEnvelope =>
|
|
105
|
+
buildContextEnvelope({
|
|
106
|
+
sessionId,
|
|
107
|
+
appVersion: config.appVersion,
|
|
108
|
+
appBuild: config.appBuild,
|
|
109
|
+
networkType: config.networkType,
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
const queue: EventQueue = createEventQueue({
|
|
113
|
+
target: { serverUrl: config.serverUrl, apiKey: config.apiKey },
|
|
114
|
+
storage: config.storage,
|
|
115
|
+
appId: config.appId,
|
|
116
|
+
envelope,
|
|
117
|
+
...options,
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// Per-session, in-memory user binding. Not persisted across launches (that is a follow-up).
|
|
121
|
+
let boundUserId: string | undefined;
|
|
122
|
+
|
|
123
|
+
const track = (event: string, props?: AnalyticsProps): void => {
|
|
124
|
+
if (!event) return;
|
|
125
|
+
const clientEvent: ClientEvent = {
|
|
126
|
+
event_type: "app_event",
|
|
127
|
+
session_id: sessionId,
|
|
128
|
+
question_key: event,
|
|
129
|
+
};
|
|
130
|
+
if (props && Object.keys(props).length > 0) clientEvent.meta = JSON.stringify(props);
|
|
131
|
+
if (boundUserId) clientEvent.user_id = boundUserId;
|
|
132
|
+
queue.enqueue(clientEvent);
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const screen = (name: string, props?: AnalyticsProps): void => {
|
|
136
|
+
if (!name) return;
|
|
137
|
+
// Reuse the K6 screen event shape (question_key='screen'); route through the queue for offline-first.
|
|
138
|
+
const meta = { screen: name, ...(props ?? {}) };
|
|
139
|
+
const clientEvent: ClientEvent = {
|
|
140
|
+
event_type: "app_event",
|
|
141
|
+
session_id: sessionId,
|
|
142
|
+
question_key: "screen",
|
|
143
|
+
meta: JSON.stringify(meta),
|
|
144
|
+
};
|
|
145
|
+
if (boundUserId) clientEvent.user_id = boundUserId;
|
|
146
|
+
queue.enqueue(clientEvent);
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
const identify = (userId: string, traits?: AnalyticsProps): void => {
|
|
150
|
+
const clean = sanitizeUserId(userId);
|
|
151
|
+
// Blank / non-string → no binding, no event (sanitizeUserId returns undefined). >128 → truncated.
|
|
152
|
+
if (!clean) return;
|
|
153
|
+
boundUserId = clean;
|
|
154
|
+
const clientEvent: ClientEvent = {
|
|
155
|
+
event_type: "identify",
|
|
156
|
+
session_id: sessionId,
|
|
157
|
+
user_id: clean,
|
|
158
|
+
};
|
|
159
|
+
if (traits && Object.keys(traits).length > 0) clientEvent.meta = JSON.stringify(traits);
|
|
160
|
+
queue.enqueue(clientEvent);
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
return {
|
|
164
|
+
track,
|
|
165
|
+
screen,
|
|
166
|
+
identify,
|
|
167
|
+
flush: queue.flush,
|
|
168
|
+
notifyOnline: queue.notifyOnline,
|
|
169
|
+
size: queue.size,
|
|
170
|
+
};
|
|
171
|
+
};
|
package/src/analytics/index.ts
CHANGED
|
@@ -45,3 +45,15 @@ export type { ContextEnvelope, ContextEnvelopeInput } from "./contextEnvelope";
|
|
|
45
45
|
// ─── Offline-first, persistent, batched + retried event queue (dependency-free) ─
|
|
46
46
|
export { createEventQueue } from "./eventQueue";
|
|
47
47
|
export type { EventQueue, EventQueueOptions, EnvelopeSource } from "./eventQueue";
|
|
48
|
+
|
|
49
|
+
// ─── Developer-facing analytics façade (track / screen / identify) over the queue ─
|
|
50
|
+
export { createAnalytics } from "./analyticsFacade";
|
|
51
|
+
export type {
|
|
52
|
+
Analytics,
|
|
53
|
+
AnalyticsOptions,
|
|
54
|
+
AnalyticsProps,
|
|
55
|
+
CreateAnalyticsConfig,
|
|
56
|
+
} from "./analyticsFacade";
|
|
57
|
+
|
|
58
|
+
// ─── The thin optional React hook over the façade ─────────────────────────────
|
|
59
|
+
export { useAnalytics } from "./useAnalytics";
|
|
@@ -35,8 +35,17 @@ import type { DeviceContext } from "../device/deviceContext";
|
|
|
35
35
|
* This is the SINGLE fallback signal — hosts must NOT also report their own.
|
|
36
36
|
* `identify` binds the host's opaque `user_id` to this `session_id` (late binding — the user
|
|
37
37
|
* registered during/after onboarding). It carries no funnel weight; the server maps the
|
|
38
|
-
* session to the user and back-fills a `session_started` if it never saw the session.
|
|
39
|
-
|
|
38
|
+
* session to the user and back-fills a `session_started` if it never saw the session.
|
|
39
|
+
* `app_event` is the host's own in-app event (the `createAnalytics` façade's `track`/`screen`
|
|
40
|
+
* route through the offline queue as first-class `ClientEvent`s). The server already ingests
|
|
41
|
+
* it — `reportAppEvent` (reviews/transport) has fired `event_type='app_event'` all along; this
|
|
42
|
+
* widening just lets the same shape flow through the durable queue instead of a blind POST. */
|
|
43
|
+
export type ClientEventType =
|
|
44
|
+
| "screen_skipped"
|
|
45
|
+
| "dropped"
|
|
46
|
+
| "client_fallback"
|
|
47
|
+
| "identify"
|
|
48
|
+
| "app_event";
|
|
40
49
|
|
|
41
50
|
/** One client-reported event. Mirrors the server's `OnboardingEvent` (client-settable fields). */
|
|
42
51
|
export type ClientEvent = {
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useAnalytics — a THIN optional React hook over the pure {@link createAnalytics} factory.
|
|
3
|
+
*
|
|
4
|
+
* It builds ONE analytics instance for the component's lifetime and returns it, so re-renders
|
|
5
|
+
* never rebuild the queue or lose the in-memory user binding. React is a REQUIRED peer of the
|
|
6
|
+
* kit, so importing it here is allowed; the hook adds NO other dependency. Mirrors the existing
|
|
7
|
+
* `createScreenTracker` / `useScreenTracking` split — the factory stays React-free, this is the
|
|
8
|
+
* glue.
|
|
9
|
+
*
|
|
10
|
+
* const analytics = useAnalytics({ serverUrl, apiKey, storage, appId });
|
|
11
|
+
* analytics.track("content_share", { source: "feed" });
|
|
12
|
+
* // ...on reconnect: analytics.notifyOnline();
|
|
13
|
+
*/
|
|
14
|
+
import { useRef } from "react";
|
|
15
|
+
|
|
16
|
+
import {
|
|
17
|
+
createAnalytics,
|
|
18
|
+
type Analytics,
|
|
19
|
+
type AnalyticsOptions,
|
|
20
|
+
type CreateAnalyticsConfig,
|
|
21
|
+
} from "./analyticsFacade";
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Build a per-mount analytics instance. `config`/`options` are read once at first render (the
|
|
25
|
+
* instance is stable for the component's lifetime, held in a ref). Returns the {@link Analytics}
|
|
26
|
+
* surface so the component can `track` / `screen` / `identify` and drive `notifyOnline` on reconnect.
|
|
27
|
+
*/
|
|
28
|
+
export const useAnalytics = (
|
|
29
|
+
config: CreateAnalyticsConfig,
|
|
30
|
+
options: AnalyticsOptions = {},
|
|
31
|
+
): Analytics => {
|
|
32
|
+
// One instance per mount; kept in a ref so re-renders never rebuild the queue or drop the binding.
|
|
33
|
+
const ref = useRef<Analytics | undefined>(undefined);
|
|
34
|
+
if (!ref.current) ref.current = createAnalytics(config, options);
|
|
35
|
+
return ref.current;
|
|
36
|
+
};
|