@wireai/activation 0.9.1 → 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-Bs2JfTJ8.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-Bs2JfTJ8.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-61dcm3V-.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-61dcm3V-.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 = {
@@ -953,13 +969,18 @@ var createEventQueue = (options) => {
953
969
  };
954
970
 
955
971
  // src/analytics/currentSession.ts
956
- var _currentSessionId;
972
+ var CURRENT_SESSION_ID_SLOT = /* @__PURE__ */ Symbol.for(
973
+ "@wireai/activation:currentSessionId"
974
+ );
975
+ var globalSlot = globalThis;
957
976
  var setCurrentSessionId = (id) => {
958
- 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
+ }
959
980
  };
960
- var getCurrentSessionId = () => _currentSessionId;
981
+ var getCurrentSessionId = () => globalSlot[CURRENT_SESSION_ID_SLOT];
961
982
  var resetCurrentSessionId = () => {
962
- _currentSessionId = void 0;
983
+ globalSlot[CURRENT_SESSION_ID_SLOT] = void 0;
963
984
  };
964
985
 
965
986
  // src/identity/userIdentity.ts
@@ -1187,7 +1208,9 @@ exports.getCurrentSessionId = getCurrentSessionId;
1187
1208
  exports.makeSessionId = makeSessionId;
1188
1209
  exports.reportAppEvent = reportAppEvent;
1189
1210
  exports.reportClientEvent = reportClientEvent;
1211
+ exports.reportClientEventAwait = reportClientEventAwait;
1190
1212
  exports.reportClientEvents = reportClientEvents;
1213
+ exports.reportClientEventsAwait = reportClientEventsAwait;
1191
1214
  exports.resetCurrentSessionId = resetCurrentSessionId;
1192
1215
  exports.screenTrackingHandler = screenTrackingHandler;
1193
1216
  exports.setCurrentSessionId = setCurrentSessionId;