@wireai/activation 0.9.2 → 0.10.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 CHANGED
@@ -123,6 +123,13 @@ write your own fetch.
123
123
  session. `deviceKey` is the identity that matters. Pass `sessionId` only when you have one.
124
124
  - Running a firing experiment? Echo the returned `arm` back as `meta.firing_arm` on the review
125
125
  submission so per-arm attribution survives a reweighting.
126
+ - **Fire the gate off an in-app action.** `useWireActivation({ serverUrl, apiKey, deviceKey })`
127
+ (ROOT export) returns `{ track, sessionId, revalidation }`. `await track(name, meta?)` POSTs an
128
+ `app_event` (`question_key=name`) under the current session, resolves `true` on 2xx, and bumps
129
+ `revalidation`; list `revalidation` in the `fetchReviewDecision` / `fetchQuestionnaireDecision`
130
+ effect's deps so the gate re-fetches after the action and can fire. React-free factory:
131
+ `createWireActivation(config)`. This is the kit-owned replacement for hand-rolling
132
+ session-id + await-POST + revalidate.
126
133
 
127
134
  ## Gotchas (do not miss)
128
135
 
package/CHANGELOG.md CHANGED
@@ -3,6 +3,30 @@
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
+ ## [Unreleased]
7
+
8
+ ### Added: `wire.track` + `useWireActivation` (fire a gate off an in-app action)
9
+
10
+ A kit-owned event-trigger surface so a consumer stops hand-rolling the session-id + await-POST +
11
+ revalidate dance to make a review or questionnaire fire off an arbitrary in-app action (the
12
+ workaround Morrow shipped). All additive; no existing API changed.
13
+
14
+ - **`useWireActivation({ serverUrl, apiKey, deviceKey?, ... })`** (root export) returns
15
+ `{ track, sessionId, revalidation }`. `createWireActivation(config)` is the React-free factory
16
+ it wraps (returns `{ track, sessionId, subscribeRevalidation, getRevalidationVersion }`).
17
+ - **`await track(name, meta?)`** POSTs `event_type='app_event'`, `question_key=name` under the
18
+ current `getCurrentSessionId()` plus `user_context.device_key` (via `reportClientEventAwait` to
19
+ `/v1/events`). Resolves `true` on a 2xx and only then bumps decision revalidation; resolves
20
+ `false` (no bump) on no current session, a blank name, or a failed POST. Never throws.
21
+ - **Revalidation pub/sub** (`useActivationRevalidation`, `bumpActivationRevalidation`,
22
+ `subscribeActivationRevalidation`, `getActivationRevalidationVersion`) is a `globalThis`
23
+ `Symbol.for` singleton, so a `track()` on the root bundle reaches a gate subscribed from the
24
+ `./reviews` / `./questionnaire` subpath under `dist`. List `revalidation` in a
25
+ `fetchReviewDecision` / `fetchQuestionnaireDecision` effect's deps and the gate re-fetches after
26
+ the action, then fires.
27
+ - Docs: new README section "Fire a review or questionnaire from an in-app action", an event-trigger
28
+ step in `INTEGRATION_PROMPT.md`, and entries in `llms.txt` / `AGENTS.md`.
29
+
6
30
  ## [0.9.1] — 2026-07-17
7
31
 
8
32
  ### Fixed: the review-decision wire is now documented as it actually is
@@ -43,7 +43,17 @@ STEPS (do them in order, stop and ask if a convention is ambiguous):
43
43
  - Analytics: log via `toAnalyticsEvent(e)` → my logger, using the canonical `wire_onboarding_*` names.
44
44
  7. Navigation: mount the screen in my first-run/signup flow; branch to the static flow when the gate
45
45
  is off or config is missing.
46
- 8. Verify: type-check (and lint the changed files); test the flag-off + backend-error paths.
46
+ 8. Event triggers (only if I asked to fire a review/questionnaire off an in-app action): pick the
47
+ action that should trigger it (e.g. finishing a task, journaling a win) and its key, e.g.
48
+ `journal_done`. On my home/gate screen call
49
+ `const { track, sessionId, revalidation } = useWireActivation({ serverUrl: WIREAI_SERVER_URL, apiKey: WIREAI_API_KEY, appId: APP_ID, deviceKey })`
50
+ from `@wireai/activation`, fetch the gate decision in a `useEffect` keyed on `[revalidation]`
51
+ (`fetchReviewDecision({ serverUrl, apiKey }, { sessionId, deviceKey })` → set it into
52
+ `useReviewGate({ config, decision })`; questionnaires use `fetchQuestionnaireDecision` +
53
+ `useQuestionnaireGate` the same way), and at the action site call `await track("journal_done")`.
54
+ Do NOT hand-roll the session id or the POST; `wire.track` owns both and revalidates the gate on
55
+ a successful 2xx.
56
+ 9. Verify: type-check (and lint the changed files); test the flag-off + backend-error paths.
47
57
 
48
58
  Report back: the files you changed (path:line), the typecheck result, and anything you couldn't
49
59
  infer about my conventions.
package/README.md CHANGED
@@ -874,6 +874,68 @@ The next retention module reuses this exact server-directed shape:
874
874
  with the client once-gating per announcement id through the same storage. Same seam, no code
875
875
  yet.
876
876
 
877
+ ## Fire a review or questionnaire from an in-app action (`wire.track`)
878
+
879
+ The decision seam above re-fetches on mount. But the gate usually sits on the home feed while
880
+ the user goes *elsewhere* to do the thing that should trigger it (journals a win, finishes a
881
+ task). The mount fetch ran before that event existed and never re-runs, so the trigger never
882
+ lands. `wire.track` closes that loop. It posts the action, waits for the server to store it,
883
+ then signals every gate to re-fetch. It is the kit-owned replacement for the session-id,
884
+ await-POST, revalidate dance a host would otherwise hand-roll (`reportAppEvent` is the older
885
+ fire-and-forget path; `wire.track` is the awaitable one that also revalidates).
886
+
887
+ `useWireActivation` is the one import that gives you the whole surface. It takes the SAME
888
+ `serverUrl` / `apiKey` as onboarding (never a second key) and returns `{ track, sessionId,
889
+ revalidation }`:
890
+
891
+ - **`track(name, meta?)`** is awaitable. `name` is the exact string a server firing trigger
892
+ matches on (`question_key`). It POSTs `event_type='app_event'` under the current
893
+ `getCurrentSessionId()` plus a `user_context.device_key`, resolves `true` once the server
894
+ has stored it (2xx) and only then bumps revalidation, and resolves `false` (no bump) when
895
+ there is no current session, a blank name, or the POST fails. It never throws.
896
+ - **`sessionId`** is the current per-open session id (read fresh each render), or `undefined`.
897
+ - **`revalidation`** is a counter that ticks on every successful `track()` from anywhere. List
898
+ it in a decision-fetch effect's deps and the fetch re-runs when it changes.
899
+
900
+ ### The `track` then re-fetch then fire wiring
901
+
902
+ No single component closes the loop for you, because the decision fetch lives in your gate's
903
+ host effect. Wire it once: fetch the decision on mount AND whenever `revalidation` ticks, then
904
+ pass the result straight to the gate's `decision` prop. Pass the same `deviceKey` to both the
905
+ hook and the fetch so the server reasons about one device.
906
+
907
+ ```tsx
908
+ import { useEffect, useState } from "react";
909
+ import { useWireActivation } from "@wireai/activation";
910
+ import {
911
+ fetchReviewDecision,
912
+ useReviewGate,
913
+ type ReviewDecisionResponse,
914
+ } from "@wireai/activation/reviews";
915
+
916
+ const target = { serverUrl: SERVER_URL, apiKey: TENANT_KEY };
917
+ const { track, sessionId, revalidation } = useWireActivation({ ...target, deviceKey });
918
+ const [decision, setDecision] = useState<ReviewDecisionResponse>();
919
+
920
+ // Re-fetch the server decision on mount AND every time a track() succeeds (revalidation ticks).
921
+ useEffect(() => {
922
+ fetchReviewDecision(target, { sessionId, deviceKey }).then((d) => setDecision(d ?? undefined));
923
+ }, [revalidation]);
924
+
925
+ const gate = useReviewGate({ config: { id: "home", minSessions: 2 }, decision, storage });
926
+
927
+ // …elsewhere, on the triggering action. Awaiting it means the re-fetch sees the event:
928
+ await track("journal_done"); // POST stored → revalidation bumps → effect re-fetches → gate can fire
929
+ ```
930
+
931
+ The questionnaire gate uses the identical shape: swap `fetchReviewDecision` / `useReviewGate`
932
+ for `fetchQuestionnaireDecision` / `useQuestionnaireGate` from `@wireai/activation/questionnaire`
933
+ (both take a `decision` prop). One `wire.track` call feeds every subscribed gate.
934
+
935
+ If you are outside React (a service, a saga), `createWireActivation(config)` is the React-free
936
+ factory. It returns `{ track, sessionId, subscribeRevalidation, getRevalidationVersion }`: the
937
+ same `track`, plus the raw pub/sub the hook wraps for you.
938
+
877
939
  ## Feature controls (per-module kill switches)
878
940
 
879
941
  Every activation surface has a switch you can flip from the dashboard "in case of something":
@@ -1,6 +1,6 @@
1
1
  export { R as ReportAppEventOptions, r as reportAppEvent } from '../transport-Bzb-bcB2.mjs';
2
- import { W as WireUserContext, E as EventQueueOptions } from '../currentSession-CRRQLbAq.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-CRRQLbAq.mjs';
2
+ import { W as WireUserContext, E as EventQueueOptions } from '../currentSession-DdDkprpM.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 reportClientEventAwait, n as reportClientEvents, o as reportClientEventsAwait, p as resetCurrentSessionId, s as setCurrentSessionId, t as toAnalyticsEvent } from '../currentSession-DdDkprpM.mjs';
4
4
  import '../types-CNUqMK0D.mjs';
5
5
  import '../types-BKfpdZzX.mjs';
6
6
  import '../types-BcmagF6K.mjs';
@@ -1,6 +1,6 @@
1
1
  export { R as ReportAppEventOptions, r as reportAppEvent } from '../transport-B31G0Cib.js';
2
- import { W as WireUserContext, E as EventQueueOptions } from '../currentSession-Dj0cN3Rg.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-Dj0cN3Rg.js';
2
+ import { W as WireUserContext, E as EventQueueOptions } from '../currentSession-D0Vq7_VE.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 reportClientEventAwait, n as reportClientEvents, o as reportClientEventsAwait, p as resetCurrentSessionId, s as setCurrentSessionId, t as toAnalyticsEvent } from '../currentSession-D0Vq7_VE.js';
4
4
  import '../types-Buj9Lw9t.js';
5
5
  import '../types-BKfpdZzX.js';
6
6
  import '../types-BcmagF6K.js';
@@ -499,22 +499,38 @@ var useScreenTracking = (navigationRef, options = {}) => {
499
499
 
500
500
  // src/analytics/reportClientEvent.ts
501
501
  var makeSessionId = () => `wire_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
502
- var reportClientEvents = (target, events) => {
503
- if (!(target == null ? void 0 : target.serverUrl) || events.length === 0) return;
502
+ var buildEventsRequest = (target, events) => {
503
+ if (!(target == null ? void 0 : target.serverUrl) || events.length === 0) return null;
504
504
  try {
505
505
  const url = `${target.serverUrl.replace(/\/$/, "")}/v1/events`;
506
506
  const headers = { "Content-Type": "application/json" };
507
507
  if (target.apiKey) headers.Authorization = `Bearer ${target.apiKey}`;
508
- void fetch(url, {
509
- method: "POST",
510
- headers,
511
- body: JSON.stringify({ events })
512
- }).catch(() => {
508
+ return { url, init: { method: "POST", headers, body: JSON.stringify({ events }) } };
509
+ } catch {
510
+ return null;
511
+ }
512
+ };
513
+ var reportClientEvents = (target, events) => {
514
+ try {
515
+ const req = buildEventsRequest(target, events);
516
+ if (!req) return;
517
+ void fetch(req.url, req.init).catch(() => {
513
518
  });
514
519
  } catch {
515
520
  }
516
521
  };
517
522
  var reportClientEvent = (target, event) => reportClientEvents(target, [event]);
523
+ var reportClientEventsAwait = async (target, events) => {
524
+ try {
525
+ const req = buildEventsRequest(target, events);
526
+ if (!req) return false;
527
+ const res = await fetch(req.url, req.init);
528
+ return Boolean(res && res.ok);
529
+ } catch {
530
+ return false;
531
+ }
532
+ };
533
+ var reportClientEventAwait = (target, event) => reportClientEventsAwait(target, [event]);
518
534
 
519
535
  // src/analytics/analyticsEvent.ts
520
536
  var WIRE_ONBOARDING_EVENTS = {
@@ -860,9 +876,8 @@ var createEventQueue = (options) => {
860
876
  } catch {
861
877
  }
862
878
  })();
863
- const DROP_STATUSES = /* @__PURE__ */ new Set([400, 404, 413, 422]);
864
879
  const postBatch = async (events) => {
865
- if (!(target == null ? void 0 : target.serverUrl) || events.length === 0) return "retry";
880
+ if (!(target == null ? void 0 : target.serverUrl) || events.length === 0) return false;
866
881
  const controller = typeof AbortController !== "undefined" ? new AbortController() : void 0;
867
882
  const timer = setTimeout(() => controller == null ? void 0 : controller.abort(), 15e3);
868
883
  try {
@@ -875,12 +890,9 @@ var createEventQueue = (options) => {
875
890
  body: JSON.stringify({ events }),
876
891
  signal: controller == null ? void 0 : controller.signal
877
892
  });
878
- if (res && res.ok) return "ok";
879
- const status = res == null ? void 0 : res.status;
880
- if (typeof status === "number" && DROP_STATUSES.has(status)) return "drop";
881
- return "retry";
893
+ return !!(res && res.ok);
882
894
  } catch {
883
- return "retry";
895
+ return false;
884
896
  } finally {
885
897
  clearTimeout(timer);
886
898
  }
@@ -912,8 +924,8 @@ var createEventQueue = (options) => {
912
924
  try {
913
925
  while (pending.length > 0) {
914
926
  const batch = pending.slice(0, batchSize);
915
- const result = await postBatch(batch.map((item) => item.event));
916
- if (result === "retry") {
927
+ const ok = await postBatch(batch.map((item) => item.event));
928
+ if (!ok) {
917
929
  scheduleRetry();
918
930
  return;
919
931
  }
@@ -957,13 +969,18 @@ var createEventQueue = (options) => {
957
969
  };
958
970
 
959
971
  // src/analytics/currentSession.ts
960
- var _currentSessionId;
972
+ var CURRENT_SESSION_ID_SLOT = /* @__PURE__ */ Symbol.for(
973
+ "@wireai/activation:currentSessionId"
974
+ );
975
+ var globalSlot = globalThis;
961
976
  var setCurrentSessionId = (id) => {
962
- if (typeof id === "string" && id.length > 0) _currentSessionId = id;
977
+ if (typeof id === "string" && id.length > 0) {
978
+ globalSlot[CURRENT_SESSION_ID_SLOT] = id;
979
+ }
963
980
  };
964
- var getCurrentSessionId = () => _currentSessionId;
981
+ var getCurrentSessionId = () => globalSlot[CURRENT_SESSION_ID_SLOT];
965
982
  var resetCurrentSessionId = () => {
966
- _currentSessionId = void 0;
983
+ globalSlot[CURRENT_SESSION_ID_SLOT] = void 0;
967
984
  };
968
985
 
969
986
  // src/identity/userIdentity.ts
@@ -1191,7 +1208,9 @@ exports.getCurrentSessionId = getCurrentSessionId;
1191
1208
  exports.makeSessionId = makeSessionId;
1192
1209
  exports.reportAppEvent = reportAppEvent;
1193
1210
  exports.reportClientEvent = reportClientEvent;
1211
+ exports.reportClientEventAwait = reportClientEventAwait;
1194
1212
  exports.reportClientEvents = reportClientEvents;
1213
+ exports.reportClientEventsAwait = reportClientEventsAwait;
1195
1214
  exports.resetCurrentSessionId = resetCurrentSessionId;
1196
1215
  exports.screenTrackingHandler = screenTrackingHandler;
1197
1216
  exports.setCurrentSessionId = setCurrentSessionId;