@rocapine/rocalytics-sdk 0.0.0-stage → 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/CHANGELOG.md +86 -0
- package/LICENSE +21 -0
- package/README.md +449 -2
- package/client/package.json +1 -0
- package/core/package.json +4 -0
- package/dist/client/client.d.ts +146 -0
- package/dist/client/client.d.ts.map +1 -0
- package/dist/client/client.js +376 -0
- package/dist/client/client.js.map +1 -0
- package/dist/client/index.d.ts +13 -0
- package/dist/client/index.d.ts.map +1 -0
- package/dist/client/index.js +35 -0
- package/dist/client/index.js.map +1 -0
- package/dist/client/native.d.ts +68 -0
- package/dist/client/native.d.ts.map +1 -0
- package/dist/client/native.js +64 -0
- package/dist/client/native.js.map +1 -0
- package/dist/client/onboardingSink.d.ts +63 -0
- package/dist/client/onboardingSink.d.ts.map +1 -0
- package/dist/client/onboardingSink.js +126 -0
- package/dist/client/onboardingSink.js.map +1 -0
- package/dist/client/paywallSink.d.ts +13 -0
- package/dist/client/paywallSink.d.ts.map +1 -0
- package/dist/client/paywallSink.js +26 -0
- package/dist/client/paywallSink.js.map +1 -0
- package/dist/client/requests.d.ts +66 -0
- package/dist/client/requests.d.ts.map +1 -0
- package/dist/client/requests.js +90 -0
- package/dist/client/requests.js.map +1 -0
- package/dist/client/types.d.ts +180 -0
- package/dist/client/types.d.ts.map +1 -0
- package/dist/client/types.js +4 -0
- package/dist/client/types.js.map +1 -0
- package/dist/core/context.d.ts +39 -0
- package/dist/core/context.d.ts.map +1 -0
- package/dist/core/context.js +45 -0
- package/dist/core/context.js.map +1 -0
- package/dist/core/delivery.d.ts +63 -0
- package/dist/core/delivery.d.ts.map +1 -0
- package/dist/core/delivery.js +133 -0
- package/dist/core/delivery.js.map +1 -0
- package/dist/core/diagnostics.d.ts +16 -0
- package/dist/core/diagnostics.d.ts.map +1 -0
- package/dist/core/diagnostics.js +24 -0
- package/dist/core/diagnostics.js.map +1 -0
- package/dist/core/httpSink.d.ts +35 -0
- package/dist/core/httpSink.d.ts.map +1 -0
- package/dist/core/httpSink.js +65 -0
- package/dist/core/httpSink.js.map +1 -0
- package/dist/core/ids.d.ts +16 -0
- package/dist/core/ids.d.ts.map +1 -0
- package/dist/core/ids.js +51 -0
- package/dist/core/ids.js.map +1 -0
- package/dist/core/index.d.ts +17 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/index.js +32 -0
- package/dist/core/index.js.map +1 -0
- package/dist/core/sink.d.ts +39 -0
- package/dist/core/sink.d.ts.map +1 -0
- package/dist/core/sink.js +16 -0
- package/dist/core/sink.js.map +1 -0
- package/dist/core/storage.d.ts +52 -0
- package/dist/core/storage.d.ts.map +1 -0
- package/dist/core/storage.js +79 -0
- package/dist/core/storage.js.map +1 -0
- package/dist/core/text.d.ts +9 -0
- package/dist/core/text.d.ts.map +1 -0
- package/dist/core/text.js +47 -0
- package/dist/core/text.js.map +1 -0
- package/dist/core/time.d.ts +18 -0
- package/dist/core/time.d.ts.map +1 -0
- package/dist/core/time.js +18 -0
- package/dist/core/time.js.map +1 -0
- package/dist/onboarding/contract.d.ts +138 -0
- package/dist/onboarding/contract.d.ts.map +1 -0
- package/dist/onboarding/contract.js +11 -0
- package/dist/onboarding/contract.js.map +1 -0
- package/dist/onboarding/index.d.ts +15 -0
- package/dist/onboarding/index.d.ts.map +1 -0
- package/dist/onboarding/index.js +22 -0
- package/dist/onboarding/index.js.map +1 -0
- package/dist/onboarding/input.d.ts +102 -0
- package/dist/onboarding/input.d.ts.map +1 -0
- package/dist/onboarding/input.js +216 -0
- package/dist/onboarding/input.js.map +1 -0
- package/dist/onboarding/persisted.d.ts +20 -0
- package/dist/onboarding/persisted.d.ts.map +1 -0
- package/dist/onboarding/persisted.js +75 -0
- package/dist/onboarding/persisted.js.map +1 -0
- package/dist/onboarding/state.d.ts +65 -0
- package/dist/onboarding/state.d.ts.map +1 -0
- package/dist/onboarding/state.js +207 -0
- package/dist/onboarding/state.js.map +1 -0
- package/dist/onboarding/tracker.d.ts +135 -0
- package/dist/onboarding/tracker.d.ts.map +1 -0
- package/dist/onboarding/tracker.js +734 -0
- package/dist/onboarding/tracker.js.map +1 -0
- package/dist/paywall/contract.d.ts +67 -0
- package/dist/paywall/contract.d.ts.map +1 -0
- package/dist/paywall/contract.js +3 -0
- package/dist/paywall/contract.js.map +1 -0
- package/dist/paywall/index.d.ts +14 -0
- package/dist/paywall/index.d.ts.map +1 -0
- package/dist/paywall/index.js +19 -0
- package/dist/paywall/index.js.map +1 -0
- package/dist/paywall/observer.d.ts +38 -0
- package/dist/paywall/observer.d.ts.map +1 -0
- package/dist/paywall/observer.js +6 -0
- package/dist/paywall/observer.js.map +1 -0
- package/dist/paywall/state.d.ts +26 -0
- package/dist/paywall/state.d.ts.map +1 -0
- package/dist/paywall/state.js +132 -0
- package/dist/paywall/state.js.map +1 -0
- package/dist/paywall/tracker.d.ts +36 -0
- package/dist/paywall/tracker.d.ts.map +1 -0
- package/dist/paywall/tracker.js +180 -0
- package/dist/paywall/tracker.js.map +1 -0
- package/dist/version.d.ts +3 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +6 -0
- package/dist/version.js.map +1 -0
- package/docs/onboarding-run-contract.md +1013 -0
- package/docs/onboarding-run.examples.ts +306 -0
- package/docs/onboarding-run.schema.json +224 -0
- package/docs/onboarding-run.types.ts +144 -0
- package/docs/paywall-presentation-contract.md +139 -0
- package/docs/paywall-presentation.examples.ts +42 -0
- package/docs/paywall-presentation.schema.json +268 -0
- package/docs/paywall-presentation.types.ts +73 -0
- package/onboarding/package.json +4 -0
- package/package.json +120 -4
- package/paywall/package.json +1 -0
- package/src/client/client.ts +417 -0
- package/src/client/index.ts +54 -0
- package/src/client/native.ts +109 -0
- package/src/client/onboardingSink.ts +121 -0
- package/src/client/paywallSink.ts +25 -0
- package/src/client/requests.ts +126 -0
- package/src/client/types.ts +198 -0
- package/src/core/context.ts +69 -0
- package/src/core/delivery.ts +181 -0
- package/src/core/diagnostics.ts +30 -0
- package/src/core/httpSink.ts +70 -0
- package/src/core/ids.ts +46 -0
- package/src/core/index.ts +20 -0
- package/src/core/sink.ts +46 -0
- package/src/core/storage.ts +112 -0
- package/src/core/text.ts +37 -0
- package/src/core/time.ts +26 -0
- package/src/onboarding/contract.ts +144 -0
- package/src/onboarding/index.ts +39 -0
- package/src/onboarding/input.ts +246 -0
- package/src/onboarding/persisted.ts +93 -0
- package/src/onboarding/state.ts +233 -0
- package/src/onboarding/tracker.ts +855 -0
- package/src/paywall/contract.ts +73 -0
- package/src/paywall/index.ts +36 -0
- package/src/paywall/observer.ts +43 -0
- package/src/paywall/state.ts +125 -0
- package/src/paywall/tracker.ts +200 -0
- package/src/version.ts +2 -0
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
// Onboarding run contract, schema_version 1: the example runs.
|
|
2
|
+
//
|
|
3
|
+
// Every payload shown in onboarding-run-contract.md is produced here, and so is
|
|
4
|
+
// every run behind its worked-example funnel tables. Being TypeScript typed
|
|
5
|
+
// against onboarding-run.types.ts, the examples are checked by the compiler; a
|
|
6
|
+
// test also validates them against onboarding-run.schema.json and recomputes the
|
|
7
|
+
// doc's funnel tables from them. The only import is type-only, so a runtime
|
|
8
|
+
// that strips types can load this file without resolving it.
|
|
9
|
+
import type {
|
|
10
|
+
Answer,
|
|
11
|
+
Manifest,
|
|
12
|
+
OnboardingIdentity,
|
|
13
|
+
OnboardingRunSnapshot,
|
|
14
|
+
Properties,
|
|
15
|
+
RunContext,
|
|
16
|
+
StepEntry,
|
|
17
|
+
StudioLinks,
|
|
18
|
+
} from "./onboarding-run.types";
|
|
19
|
+
|
|
20
|
+
/** The moment the worked-example funnels are computed at. */
|
|
21
|
+
export const AS_OF = "2026-01-12T00:00:00.000Z";
|
|
22
|
+
/** An in_progress run with no snapshot received for this long counts as quit. */
|
|
23
|
+
export const QUIT_AFTER_MS = 24 * 60 * 60 * 1000;
|
|
24
|
+
/** The most entries a snapshot may hold (D8). Past it the tracker stops recording (D26). */
|
|
25
|
+
export const MAX_ENTRIES = 500;
|
|
26
|
+
|
|
27
|
+
export interface ExampleRun {
|
|
28
|
+
/** Server receive time of the run's first snapshot: the run's bucket. */
|
|
29
|
+
first_received_at: string;
|
|
30
|
+
/** Server receive time of the latest snapshot: what the quit threshold reads. */
|
|
31
|
+
last_received_at: string;
|
|
32
|
+
payload: OnboardingRunSnapshot;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const CONTEXT: RunContext = {
|
|
36
|
+
app_version: "2.4.0",
|
|
37
|
+
build: "412",
|
|
38
|
+
platform: "ios",
|
|
39
|
+
os_version: "18.1",
|
|
40
|
+
locale: "en-US",
|
|
41
|
+
timezone: "America/New_York",
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
type PathItem = string | { step: string; answers: Answer[] };
|
|
45
|
+
|
|
46
|
+
const iso = (ms: number) => new Date(ms).toISOString();
|
|
47
|
+
const STEP_MS = 10_000;
|
|
48
|
+
const RECEIVE_LAG_MS = 300;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Builds a run the way a tracker would record it: one entry per screen shown,
|
|
52
|
+
* each lasting ten seconds, one send per entry plus one on completion, and each
|
|
53
|
+
* send arriving 300 ms after it left the device. A step the flow passes over
|
|
54
|
+
* is simply absent from `path`. Past MAX_ENTRIES the tracker stops recording
|
|
55
|
+
* (no new entries, answers or property changes; it still closes the last kept
|
|
56
|
+
* entry and still sends the completion), and marks the snapshot `truncated` (D26).
|
|
57
|
+
*/
|
|
58
|
+
function run(opts: {
|
|
59
|
+
n: number;
|
|
60
|
+
start: string;
|
|
61
|
+
onboarding: OnboardingIdentity;
|
|
62
|
+
manifest: Manifest;
|
|
63
|
+
path: PathItem[];
|
|
64
|
+
completed: boolean;
|
|
65
|
+
studio?: StudioLinks;
|
|
66
|
+
properties?: Properties;
|
|
67
|
+
context?: RunContext;
|
|
68
|
+
}): ExampleRun {
|
|
69
|
+
let t = Date.parse(opts.start);
|
|
70
|
+
const steps: StepEntry[] = [];
|
|
71
|
+
let sends = 0;
|
|
72
|
+
let lastEventAt = t;
|
|
73
|
+
let truncated = false;
|
|
74
|
+
for (const item of opts.path) {
|
|
75
|
+
const step = typeof item === "string" ? item : item.step;
|
|
76
|
+
const previous = steps[steps.length - 1];
|
|
77
|
+
if (previous && previous.exited_at === null) previous.exited_at = iso(t);
|
|
78
|
+
if (steps.length === MAX_ENTRIES) {
|
|
79
|
+
// The user is on a new screen, but recording it would break the limit.
|
|
80
|
+
if (!truncated) {
|
|
81
|
+
truncated = true;
|
|
82
|
+
sends += 1; // one send to report the closed last entry and the flag
|
|
83
|
+
lastEventAt = t;
|
|
84
|
+
}
|
|
85
|
+
} else {
|
|
86
|
+
steps.push({
|
|
87
|
+
step_key: step,
|
|
88
|
+
entered_at: iso(t),
|
|
89
|
+
exited_at: null,
|
|
90
|
+
answers: typeof item === "string" ? [] : item.answers,
|
|
91
|
+
});
|
|
92
|
+
sends += 1;
|
|
93
|
+
lastEventAt = t;
|
|
94
|
+
}
|
|
95
|
+
t += STEP_MS;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (opts.completed) {
|
|
99
|
+
if (!truncated) steps[steps.length - 1].exited_at = iso(t);
|
|
100
|
+
lastEventAt = t;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Spread in this order so the doc's JSON blocks read top-down. No cast: the
|
|
104
|
+
// compiler checks each branch against the snapshot type.
|
|
105
|
+
const head = {
|
|
106
|
+
schema_version: 1 as const,
|
|
107
|
+
run_id: `00000000-0000-4000-8000-${String(opts.n).padStart(12, "0")}`,
|
|
108
|
+
};
|
|
109
|
+
const tail = {
|
|
110
|
+
onboarding: opts.onboarding,
|
|
111
|
+
...(opts.studio ? { studio: opts.studio } : {}),
|
|
112
|
+
context: opts.context ?? CONTEXT,
|
|
113
|
+
manifest: opts.manifest,
|
|
114
|
+
...(opts.properties ? { properties: opts.properties } : {}),
|
|
115
|
+
...(truncated ? { truncated: true as const } : {}),
|
|
116
|
+
steps,
|
|
117
|
+
};
|
|
118
|
+
const started_at = steps[0].entered_at;
|
|
119
|
+
const payload: OnboardingRunSnapshot = opts.completed
|
|
120
|
+
? { ...head, seq: sends + 1, status: "completed", started_at, completed_at: iso(t), sent_at: iso(t), ...tail }
|
|
121
|
+
: { ...head, seq: sends, status: "in_progress", started_at, completed_at: null, sent_at: iso(lastEventAt), ...tail };
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
first_received_at: iso(Date.parse(opts.start) + RECEIVE_LAG_MS),
|
|
125
|
+
last_received_at: iso(lastEventAt + RECEIVE_LAG_MS),
|
|
126
|
+
payload,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const single = (question_key: string, value: string): Answer => ({ question_key, kind: "single", value });
|
|
131
|
+
const minutes = (n: number) => iso(Date.parse("2026-01-10T09:00:00.000Z") + n * 60_000);
|
|
132
|
+
|
|
133
|
+
// ---------------------------------------------------------------------------
|
|
134
|
+
// Kitchen sink: one Studio-served run, two snapshots of it (mid-flow, then
|
|
135
|
+
// completed). Uses every optional part of the shape except `variant_key`, which
|
|
136
|
+
// a Studio-served run does not carry: each Studio arm is its own onboarding key
|
|
137
|
+
// (D25). IN_FLOW_VARIANT below covers `variant_key`.
|
|
138
|
+
// ---------------------------------------------------------------------------
|
|
139
|
+
|
|
140
|
+
const KITCHEN_MANIFEST: Manifest = {
|
|
141
|
+
steps: [
|
|
142
|
+
{ step_key: "welcome" },
|
|
143
|
+
{ step_key: "goal" },
|
|
144
|
+
{ step_key: "experience" },
|
|
145
|
+
{ step_key: "plan_quick", slot: "plan" },
|
|
146
|
+
{ step_key: "plan_detailed", slot: "plan" },
|
|
147
|
+
{ step_key: "permissions" },
|
|
148
|
+
{ step_key: "summary" },
|
|
149
|
+
],
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
const kitchenGoal: PathItem = {
|
|
153
|
+
step: "goal",
|
|
154
|
+
answers: [
|
|
155
|
+
single("goal", "practice"),
|
|
156
|
+
{ question_key: "topics", kind: "multi", value: ["vocabulary", "listening"] },
|
|
157
|
+
],
|
|
158
|
+
};
|
|
159
|
+
const kitchenExperience: PathItem = {
|
|
160
|
+
step: "experience",
|
|
161
|
+
answers: [{ question_key: "daily_time", kind: "numeric", value: 15, unit: "minute" }],
|
|
162
|
+
};
|
|
163
|
+
const kitchenPath: PathItem[] = [
|
|
164
|
+
"welcome",
|
|
165
|
+
kitchenGoal,
|
|
166
|
+
kitchenExperience,
|
|
167
|
+
kitchenGoal, // back navigation: a repeated entry
|
|
168
|
+
kitchenExperience,
|
|
169
|
+
"plan_detailed",
|
|
170
|
+
// `permissions` is not shown (already granted): no entry, derived as passed over.
|
|
171
|
+
{ step: "summary", answers: [{ question_key: "feedback", kind: "text", value: "Looking forward to it" }] },
|
|
172
|
+
];
|
|
173
|
+
const kitchen = (path: PathItem[], completed: boolean) =>
|
|
174
|
+
run({
|
|
175
|
+
n: 1,
|
|
176
|
+
start: "2026-01-10T08:00:00.000Z",
|
|
177
|
+
// Studio-served: the key is the Studio onboarding id, the version the deployment id.
|
|
178
|
+
// A Studio-served draft or preview has no deployment id and sends version "draft".
|
|
179
|
+
onboarding: { key: "6f1c2b0e-3d4a-4f8e-9b7c-2a1d0e9f8c7b", version: "412" },
|
|
180
|
+
studio: {
|
|
181
|
+
onboarding_id: "6f1c2b0e-3d4a-4f8e-9b7c-2a1d0e9f8c7b",
|
|
182
|
+
deployment_id: "412",
|
|
183
|
+
audience_id: "7",
|
|
184
|
+
},
|
|
185
|
+
context: { ...CONTEXT, platform: "android", os_version: "15", build: "2040", locale: "fr-FR", timezone: "Europe/Paris", library_version: "0.1.0" },
|
|
186
|
+
manifest: KITCHEN_MANIFEST,
|
|
187
|
+
properties: { signup_source: "email", returning_user: false, cohort_week: 2 },
|
|
188
|
+
path,
|
|
189
|
+
completed,
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
export const KITCHEN_SINK: ExampleRun[] = [kitchen(kitchenPath.slice(0, 3), false), kitchen(kitchenPath, true)];
|
|
193
|
+
|
|
194
|
+
// ---------------------------------------------------------------------------
|
|
195
|
+
// A hand-coded flow that runs its own in-flow variants under one key (D25).
|
|
196
|
+
// ---------------------------------------------------------------------------
|
|
197
|
+
|
|
198
|
+
export const IN_FLOW_VARIANT: ExampleRun = run({
|
|
199
|
+
n: 2,
|
|
200
|
+
start: "2026-01-10T08:30:00.000Z",
|
|
201
|
+
onboarding: { key: "main", version: "7", variant_key: "short-intro" },
|
|
202
|
+
manifest: { steps: [{ step_key: "welcome" }, { step_key: "goal" }, { step_key: "summary" }] },
|
|
203
|
+
path: ["welcome"],
|
|
204
|
+
completed: false,
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
// ---------------------------------------------------------------------------
|
|
208
|
+
// Overflow (D26): a run that loops between two screens far past 500 entries.
|
|
209
|
+
// The tracker keeps the first 500, stops recording (entries, answers and
|
|
210
|
+
// property changes), and still completes.
|
|
211
|
+
// ---------------------------------------------------------------------------
|
|
212
|
+
|
|
213
|
+
const loop: PathItem[] = [];
|
|
214
|
+
for (let i = 0; i < 300; i++) loop.push("question", "review");
|
|
215
|
+
|
|
216
|
+
export const OVERFLOW: ExampleRun = run({
|
|
217
|
+
n: 401,
|
|
218
|
+
start: "2026-01-10T07:00:00.000Z",
|
|
219
|
+
onboarding: { key: "example-long", version: "1" },
|
|
220
|
+
manifest: {
|
|
221
|
+
steps: [{ step_key: "welcome" }, { step_key: "question" }, { step_key: "review" }, { step_key: "summary" }],
|
|
222
|
+
},
|
|
223
|
+
path: ["welcome", ...loop, "summary"],
|
|
224
|
+
completed: true,
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
// ---------------------------------------------------------------------------
|
|
228
|
+
// Worked examples for the funnel measures.
|
|
229
|
+
// ---------------------------------------------------------------------------
|
|
230
|
+
|
|
231
|
+
const SKIP_MANIFEST: Manifest = {
|
|
232
|
+
steps: [
|
|
233
|
+
{ step_key: "welcome" },
|
|
234
|
+
{ step_key: "goal" },
|
|
235
|
+
{ step_key: "experience" },
|
|
236
|
+
{ step_key: "permissions" },
|
|
237
|
+
{ step_key: "summary" },
|
|
238
|
+
],
|
|
239
|
+
};
|
|
240
|
+
const skip = { onboarding: { key: "example-skip", version: "1" }, manifest: SKIP_MANIFEST };
|
|
241
|
+
const learn: PathItem = { step: "goal", answers: [single("goal", "learn")] };
|
|
242
|
+
const practice: PathItem = { step: "goal", answers: [single("goal", "practice")] };
|
|
243
|
+
|
|
244
|
+
const SLOT_MANIFEST: Manifest = {
|
|
245
|
+
steps: [
|
|
246
|
+
{ step_key: "welcome" },
|
|
247
|
+
{ step_key: "goal" },
|
|
248
|
+
{ step_key: "plan_quick", slot: "plan" },
|
|
249
|
+
{ step_key: "plan_detailed", slot: "plan" },
|
|
250
|
+
{ step_key: "summary" },
|
|
251
|
+
],
|
|
252
|
+
};
|
|
253
|
+
const slot = { onboarding: { key: "example-slot", version: "1" }, manifest: SLOT_MANIFEST };
|
|
254
|
+
const quick: PathItem = { step: "goal", answers: [single("pace", "quick")] };
|
|
255
|
+
const detailed: PathItem = { step: "goal", answers: [single("pace", "detailed")] };
|
|
256
|
+
|
|
257
|
+
const MERGE_MANIFEST: Manifest = {
|
|
258
|
+
steps: [
|
|
259
|
+
{ step_key: "welcome" },
|
|
260
|
+
{ step_key: "goal" },
|
|
261
|
+
{ step_key: "routine_a1", slot: "routine" },
|
|
262
|
+
{ step_key: "routine_b1", slot: "routine" },
|
|
263
|
+
{ step_key: "routine_a2" },
|
|
264
|
+
{ step_key: "summary" },
|
|
265
|
+
],
|
|
266
|
+
};
|
|
267
|
+
const merge = { onboarding: { key: "example-merge", version: "1" }, manifest: MERGE_MANIFEST };
|
|
268
|
+
const pathA: PathItem = { step: "goal", answers: [single("path", "a")] };
|
|
269
|
+
const pathB: PathItem = { step: "goal", answers: [single("path", "b")] };
|
|
270
|
+
|
|
271
|
+
export const WORKED_EXAMPLES: Record<string, { runs: ExampleRun[] }> = {
|
|
272
|
+
derived_skip: {
|
|
273
|
+
runs: [
|
|
274
|
+
run({ ...skip, n: 101, start: minutes(0), completed: true, path: ["welcome", learn, "permissions", "summary"] }),
|
|
275
|
+
run({
|
|
276
|
+
...skip,
|
|
277
|
+
n: 102,
|
|
278
|
+
start: minutes(15),
|
|
279
|
+
completed: true,
|
|
280
|
+
path: ["welcome", practice, "experience", practice, "experience", "summary"],
|
|
281
|
+
}),
|
|
282
|
+
run({ ...skip, n: 103, start: minutes(30), completed: false, path: ["welcome", practice, "experience"] }),
|
|
283
|
+
run({ ...skip, n: 104, start: minutes(45), completed: false, path: ["welcome", learn, "permissions"] }),
|
|
284
|
+
run({ ...skip, n: 105, start: "2026-01-11T23:00:00.000Z", completed: false, path: ["welcome"] }),
|
|
285
|
+
run({ ...skip, n: 106, start: minutes(60), completed: false, path: ["welcome"] }),
|
|
286
|
+
],
|
|
287
|
+
},
|
|
288
|
+
alternative_screens: {
|
|
289
|
+
runs: [
|
|
290
|
+
run({ ...slot, n: 201, start: minutes(0), completed: true, path: ["welcome", quick, "plan_quick", "summary"] }),
|
|
291
|
+
run({ ...slot, n: 202, start: minutes(15), completed: true, path: ["welcome", detailed, "plan_detailed", "summary"] }),
|
|
292
|
+
run({ ...slot, n: 203, start: minutes(30), completed: false, path: ["welcome", detailed, "plan_detailed"] }),
|
|
293
|
+
run({ ...slot, n: 204, start: minutes(45), completed: true, path: ["welcome", quick, "plan_quick", "summary"] }),
|
|
294
|
+
run({ ...slot, n: 205, start: minutes(60), completed: false, path: ["welcome", quick] }),
|
|
295
|
+
],
|
|
296
|
+
},
|
|
297
|
+
merge_back: {
|
|
298
|
+
runs: [
|
|
299
|
+
run({ ...merge, n: 301, start: minutes(0), completed: true, path: ["welcome", pathA, "routine_a1", "routine_a2", "summary"] }),
|
|
300
|
+
run({ ...merge, n: 302, start: minutes(15), completed: false, path: ["welcome", pathA, "routine_a1"] }),
|
|
301
|
+
run({ ...merge, n: 303, start: minutes(30), completed: true, path: ["welcome", pathB, "routine_b1", "summary"] }),
|
|
302
|
+
run({ ...merge, n: 304, start: minutes(45), completed: false, path: ["welcome", pathB, "routine_b1"] }),
|
|
303
|
+
run({ ...merge, n: 305, start: minutes(60), completed: true, path: ["welcome", pathB, "routine_b1", "summary"] }),
|
|
304
|
+
],
|
|
305
|
+
},
|
|
306
|
+
};
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "urn:rocalytics-sdk:onboarding-run:1",
|
|
4
|
+
"title": "Onboarding run snapshot, schema_version 1",
|
|
5
|
+
"description": "One send of an onboarding run. Each send replaces the run's previous snapshot. Normative text: onboarding-run-contract.md. The schema is closed: an unknown field is an error.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"required": [
|
|
8
|
+
"schema_version",
|
|
9
|
+
"run_id",
|
|
10
|
+
"seq",
|
|
11
|
+
"status",
|
|
12
|
+
"started_at",
|
|
13
|
+
"completed_at",
|
|
14
|
+
"sent_at",
|
|
15
|
+
"onboarding",
|
|
16
|
+
"context",
|
|
17
|
+
"manifest",
|
|
18
|
+
"steps"
|
|
19
|
+
],
|
|
20
|
+
"additionalProperties": false,
|
|
21
|
+
"properties": {
|
|
22
|
+
"schema_version": { "const": 1 },
|
|
23
|
+
"run_id": {
|
|
24
|
+
"description": "Client-minted, lowercase UUID. One per run.",
|
|
25
|
+
"type": "string",
|
|
26
|
+
"pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
|
|
27
|
+
},
|
|
28
|
+
"seq": {
|
|
29
|
+
"description": "Starts at 1 and increases with every send of this run. The server keeps the snapshot with the highest seq.",
|
|
30
|
+
"type": "integer",
|
|
31
|
+
"minimum": 1
|
|
32
|
+
},
|
|
33
|
+
"status": { "enum": ["in_progress", "completed"] },
|
|
34
|
+
"started_at": { "$ref": "#/$defs/timestamp" },
|
|
35
|
+
"completed_at": { "type": ["string", "null"] },
|
|
36
|
+
"sent_at": { "$ref": "#/$defs/timestamp" },
|
|
37
|
+
"onboarding": {
|
|
38
|
+
"type": "object",
|
|
39
|
+
"required": ["key", "version"],
|
|
40
|
+
"additionalProperties": false,
|
|
41
|
+
"properties": {
|
|
42
|
+
"key": { "$ref": "#/$defs/key" },
|
|
43
|
+
"version": {
|
|
44
|
+
"type": "string",
|
|
45
|
+
"pattern": "^[A-Za-z0-9][A-Za-z0-9_.:+-]{0,63}$"
|
|
46
|
+
},
|
|
47
|
+
"variant_key": {
|
|
48
|
+
"anyOf": [{ "$ref": "#/$defs/key" }, { "type": "null" }]
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
"studio": {
|
|
53
|
+
"description": "Optional links to Studio records. Opaque strings.",
|
|
54
|
+
"type": "object",
|
|
55
|
+
"additionalProperties": false,
|
|
56
|
+
"properties": {
|
|
57
|
+
"onboarding_id": { "$ref": "#/$defs/link" },
|
|
58
|
+
"deployment_id": { "$ref": "#/$defs/link" },
|
|
59
|
+
"audience_id": { "$ref": "#/$defs/link" }
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
"context": {
|
|
63
|
+
"description": "Captured once, when the run starts. Country is not sent: the server derives it.",
|
|
64
|
+
"type": "object",
|
|
65
|
+
"required": ["app_version", "build", "platform", "os_version", "locale", "timezone"],
|
|
66
|
+
"additionalProperties": false,
|
|
67
|
+
"properties": {
|
|
68
|
+
"app_version": { "$ref": "#/$defs/shortText" },
|
|
69
|
+
"build": { "anyOf": [{ "$ref": "#/$defs/shortText" }, { "type": "null" }] },
|
|
70
|
+
"platform": { "enum": ["ios", "android", "web"] },
|
|
71
|
+
"os_version": { "anyOf": [{ "$ref": "#/$defs/shortText" }, { "type": "null" }] },
|
|
72
|
+
"locale": {
|
|
73
|
+
"description": "BCP 47 language tag.",
|
|
74
|
+
"type": "string",
|
|
75
|
+
"pattern": "^[A-Za-z]{2,3}(-[A-Za-z0-9]{1,8})*$"
|
|
76
|
+
},
|
|
77
|
+
"timezone": {
|
|
78
|
+
"description": "IANA time zone name.",
|
|
79
|
+
"type": "string",
|
|
80
|
+
"pattern": "^[A-Za-z][A-Za-z0-9_+-]*(/[A-Za-z0-9_+-]+)*$",
|
|
81
|
+
"maxLength": 64
|
|
82
|
+
},
|
|
83
|
+
"library_version": { "$ref": "#/$defs/shortText" }
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
"manifest": {
|
|
87
|
+
"type": "object",
|
|
88
|
+
"required": ["steps"],
|
|
89
|
+
"additionalProperties": false,
|
|
90
|
+
"properties": {
|
|
91
|
+
"steps": {
|
|
92
|
+
"type": "array",
|
|
93
|
+
"minItems": 1,
|
|
94
|
+
"maxItems": 200,
|
|
95
|
+
"items": {
|
|
96
|
+
"type": "object",
|
|
97
|
+
"required": ["step_key"],
|
|
98
|
+
"additionalProperties": false,
|
|
99
|
+
"properties": {
|
|
100
|
+
"step_key": { "$ref": "#/$defs/key" },
|
|
101
|
+
"slot": { "$ref": "#/$defs/key" }
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
"properties": {
|
|
108
|
+
"type": "object",
|
|
109
|
+
"maxProperties": 20,
|
|
110
|
+
"propertyNames": { "pattern": "^[a-z][a-z0-9_]{0,39}$" },
|
|
111
|
+
"additionalProperties": {
|
|
112
|
+
"anyOf": [
|
|
113
|
+
{ "type": "string", "maxLength": 256 },
|
|
114
|
+
{ "type": "number" },
|
|
115
|
+
{ "type": "boolean" },
|
|
116
|
+
{ "type": "null" }
|
|
117
|
+
]
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
"truncated": {
|
|
121
|
+
"description": "Present, and true, once the tracker has stopped recording (no new entries, answers or property changes) because recording more would break the 500-entry or 256 KiB limit. What was recorded is kept. Absent means not truncated.",
|
|
122
|
+
"const": true
|
|
123
|
+
},
|
|
124
|
+
"steps": {
|
|
125
|
+
"description": "Every screen shown so far, in the order shown. A step that was not shown has no entry: whether it was passed over is derived from the manifest at read time.",
|
|
126
|
+
"type": "array",
|
|
127
|
+
"minItems": 1,
|
|
128
|
+
"maxItems": 500,
|
|
129
|
+
"items": { "$ref": "#/$defs/entry" }
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
"allOf": [
|
|
133
|
+
{
|
|
134
|
+
"if": { "properties": { "status": { "const": "completed" } } },
|
|
135
|
+
"then": { "properties": { "completed_at": { "$ref": "#/$defs/timestamp" } } },
|
|
136
|
+
"else": { "properties": { "completed_at": { "type": "null" } } }
|
|
137
|
+
}
|
|
138
|
+
],
|
|
139
|
+
"$defs": {
|
|
140
|
+
"timestamp": {
|
|
141
|
+
"description": "RFC 3339 in UTC with exactly three fractional digits and a Z suffix, e.g. 2026-01-10T09:00:00.000Z. A pattern rather than format: date-time, because format is annotation-only in draft 2020-12.",
|
|
142
|
+
"type": "string",
|
|
143
|
+
"pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}\\.[0-9]{3}Z$"
|
|
144
|
+
},
|
|
145
|
+
"key": {
|
|
146
|
+
"type": "string",
|
|
147
|
+
"pattern": "^[A-Za-z0-9][A-Za-z0-9_.:-]{0,127}$"
|
|
148
|
+
},
|
|
149
|
+
"link": {
|
|
150
|
+
"anyOf": [
|
|
151
|
+
{ "type": "string", "minLength": 1, "maxLength": 128 },
|
|
152
|
+
{ "type": "null" }
|
|
153
|
+
]
|
|
154
|
+
},
|
|
155
|
+
"shortText": { "type": "string", "minLength": 1, "maxLength": 32 },
|
|
156
|
+
"entry": {
|
|
157
|
+
"type": "object",
|
|
158
|
+
"required": ["step_key", "entered_at", "exited_at", "answers"],
|
|
159
|
+
"additionalProperties": false,
|
|
160
|
+
"properties": {
|
|
161
|
+
"step_key": { "$ref": "#/$defs/key" },
|
|
162
|
+
"entered_at": { "$ref": "#/$defs/timestamp" },
|
|
163
|
+
"exited_at": {
|
|
164
|
+
"anyOf": [{ "$ref": "#/$defs/timestamp" }, { "type": "null" }]
|
|
165
|
+
},
|
|
166
|
+
"answers": {
|
|
167
|
+
"type": "array",
|
|
168
|
+
"maxItems": 50,
|
|
169
|
+
"items": { "$ref": "#/$defs/answer" }
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
},
|
|
173
|
+
"answer": {
|
|
174
|
+
"oneOf": [
|
|
175
|
+
{
|
|
176
|
+
"type": "object",
|
|
177
|
+
"required": ["question_key", "kind", "value"],
|
|
178
|
+
"additionalProperties": false,
|
|
179
|
+
"properties": {
|
|
180
|
+
"question_key": { "$ref": "#/$defs/key" },
|
|
181
|
+
"kind": { "const": "single" },
|
|
182
|
+
"value": { "$ref": "#/$defs/key" }
|
|
183
|
+
}
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
"type": "object",
|
|
187
|
+
"required": ["question_key", "kind", "value"],
|
|
188
|
+
"additionalProperties": false,
|
|
189
|
+
"properties": {
|
|
190
|
+
"question_key": { "$ref": "#/$defs/key" },
|
|
191
|
+
"kind": { "const": "multi" },
|
|
192
|
+
"value": {
|
|
193
|
+
"type": "array",
|
|
194
|
+
"maxItems": 50,
|
|
195
|
+
"uniqueItems": true,
|
|
196
|
+
"items": { "$ref": "#/$defs/key" }
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
"type": "object",
|
|
202
|
+
"required": ["question_key", "kind", "value"],
|
|
203
|
+
"additionalProperties": false,
|
|
204
|
+
"properties": {
|
|
205
|
+
"question_key": { "$ref": "#/$defs/key" },
|
|
206
|
+
"kind": { "const": "numeric" },
|
|
207
|
+
"value": { "type": "number" },
|
|
208
|
+
"unit": { "$ref": "#/$defs/key" }
|
|
209
|
+
}
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
"type": "object",
|
|
213
|
+
"required": ["question_key", "kind", "value"],
|
|
214
|
+
"additionalProperties": false,
|
|
215
|
+
"properties": {
|
|
216
|
+
"question_key": { "$ref": "#/$defs/key" },
|
|
217
|
+
"kind": { "const": "text" },
|
|
218
|
+
"value": { "type": "string", "maxLength": 1000 }
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
]
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
// Onboarding run contract, schema_version 1: TypeScript types.
|
|
2
|
+
//
|
|
3
|
+
// The normative text is onboarding-run-contract.md; the machine-checkable shape
|
|
4
|
+
// is onboarding-run.schema.json. These types mirror the schema. Where a rule is
|
|
5
|
+
// a pattern or a limit (key characters, list lengths), the type says `string`
|
|
6
|
+
// or `T[]` and the comment names the rule; the schema enforces it.
|
|
7
|
+
//
|
|
8
|
+
// This file has no imports, so it can be copied into any TypeScript project.
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* RFC 3339 timestamp in UTC with exactly three fractional digits and a `Z`
|
|
12
|
+
* suffix, e.g. `2026-01-10T09:00:00.000Z`.
|
|
13
|
+
*/
|
|
14
|
+
export type Timestamp = string;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* An identifier chosen by the app: 1 to 128 characters from `A-Z a-z 0-9 _ . : -`,
|
|
18
|
+
* starting with a letter or digit. Case-sensitive. Used for onboarding, step,
|
|
19
|
+
* question, option, slot and variant keys.
|
|
20
|
+
*/
|
|
21
|
+
export type Key = string;
|
|
22
|
+
|
|
23
|
+
export interface OnboardingIdentity {
|
|
24
|
+
/**
|
|
25
|
+
* Stable key for one flow. Never reused for a different flow. Each
|
|
26
|
+
* Studio-served A/B arm is its own onboarding, so it has its own key.
|
|
27
|
+
*/
|
|
28
|
+
key: Key;
|
|
29
|
+
/**
|
|
30
|
+
* Revision of the flow. 1 to 64 characters from `A-Z a-z 0-9 _ . : + -`,
|
|
31
|
+
* starting with a letter or digit. Must change whenever the manifest changes,
|
|
32
|
+
* except for the reserved version `draft`, which a Studio-served draft or
|
|
33
|
+
* preview must send because it has no deployment id.
|
|
34
|
+
*/
|
|
35
|
+
version: string;
|
|
36
|
+
/**
|
|
37
|
+
* In-flow variant, for an app that runs one flow (one key) with its own
|
|
38
|
+
* variants. Not set for a Studio-served run. Absent or null otherwise.
|
|
39
|
+
*/
|
|
40
|
+
variant_key?: Key | null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Optional links to Studio records. Opaque strings of 1 to 128 characters; numeric ids are sent in decimal. */
|
|
44
|
+
export interface StudioLinks {
|
|
45
|
+
onboarding_id?: string | null;
|
|
46
|
+
deployment_id?: string | null;
|
|
47
|
+
audience_id?: string | null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Captured once, at run start, and repeated unchanged in every snapshot. */
|
|
51
|
+
export interface RunContext {
|
|
52
|
+
/** Marketing version, e.g. `2.4.0`. 1 to 32 characters. */
|
|
53
|
+
app_version: string;
|
|
54
|
+
/** Build number, 1 to 32 characters, or null on a platform with no such concept. */
|
|
55
|
+
build: string | null;
|
|
56
|
+
platform: "ios" | "android" | "web";
|
|
57
|
+
/** OS version, 1 to 32 characters, or null when the platform cannot report one. */
|
|
58
|
+
os_version: string | null;
|
|
59
|
+
/** BCP 47 language tag the onboarding is shown in, e.g. `fr-FR`. */
|
|
60
|
+
locale: string;
|
|
61
|
+
/** IANA time zone, e.g. `Europe/Paris`. At most 64 characters. */
|
|
62
|
+
timezone: string;
|
|
63
|
+
/** Version of the tracking library that produced the payload. 1 to 32 characters. */
|
|
64
|
+
library_version?: string;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface ManifestStep {
|
|
68
|
+
step_key: Key;
|
|
69
|
+
/** Steps sharing a slot are alternatives shown at one position. Must be contiguous. */
|
|
70
|
+
slot?: Key;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Declared once, at run start, and repeated unchanged in every snapshot. */
|
|
74
|
+
export interface Manifest {
|
|
75
|
+
/** Every step the flow can show, in flow order. 1 to 200 entries. */
|
|
76
|
+
steps: ManifestStep[];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Run-level custom property value. Strings are at most 256 characters; numbers are finite. */
|
|
80
|
+
export type PropertyValue = string | number | boolean | null;
|
|
81
|
+
|
|
82
|
+
/** At most 20 keys, each matching `^[a-z][a-z0-9_]{0,39}$`. */
|
|
83
|
+
export type Properties = Record<string, PropertyValue>;
|
|
84
|
+
|
|
85
|
+
export type Answer =
|
|
86
|
+
| { question_key: Key; kind: "single"; value: Key }
|
|
87
|
+
/** At most 50 option keys, unique. */
|
|
88
|
+
| { question_key: Key; kind: "multi"; value: Key[] }
|
|
89
|
+
| { question_key: Key; kind: "numeric"; value: number; unit?: Key }
|
|
90
|
+
/** At most 1,000 characters. Never aggregated. */
|
|
91
|
+
| { question_key: Key; kind: "text"; value: string };
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* One visit to a screen the user was shown. A step that was not shown has no
|
|
95
|
+
* entry; whether the run passed over it is derived from the manifest.
|
|
96
|
+
*/
|
|
97
|
+
export interface StepEntry {
|
|
98
|
+
step_key: Key;
|
|
99
|
+
entered_at: Timestamp;
|
|
100
|
+
/**
|
|
101
|
+
* Null only on the last entry of an in_progress run. In a truncated run the
|
|
102
|
+
* last kept entry may already be closed, and keeps its own exit on completion.
|
|
103
|
+
*/
|
|
104
|
+
exited_at: Timestamp | null;
|
|
105
|
+
/** At most 50. `question_key` is unique within an entry. */
|
|
106
|
+
answers: Answer[];
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
interface RunSnapshotBase {
|
|
110
|
+
schema_version: 1;
|
|
111
|
+
/** Client-minted, lowercase UUID. One per run. */
|
|
112
|
+
run_id: string;
|
|
113
|
+
/** Starts at 1 and increases with every send of this run. A retry reuses it. */
|
|
114
|
+
seq: number;
|
|
115
|
+
started_at: Timestamp;
|
|
116
|
+
/** Client clock when this snapshot was sent. Advisory only. */
|
|
117
|
+
sent_at: Timestamp;
|
|
118
|
+
onboarding: OnboardingIdentity;
|
|
119
|
+
studio?: StudioLinks;
|
|
120
|
+
context: RunContext;
|
|
121
|
+
manifest: Manifest;
|
|
122
|
+
properties?: Properties;
|
|
123
|
+
/**
|
|
124
|
+
* Present once the tracker has stopped recording (no new entries, answers or
|
|
125
|
+
* property changes) because recording more would break the 500-entry or
|
|
126
|
+
* 256 KiB limit. What was recorded is kept.
|
|
127
|
+
*/
|
|
128
|
+
truncated?: true;
|
|
129
|
+
/** Every entry so far, in the order they happened. 1 to 500 entries. */
|
|
130
|
+
steps: StepEntry[];
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export interface InProgressRunSnapshot extends RunSnapshotBase {
|
|
134
|
+
status: "in_progress";
|
|
135
|
+
completed_at: null;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export interface CompletedRunSnapshot extends RunSnapshotBase {
|
|
139
|
+
status: "completed";
|
|
140
|
+
completed_at: Timestamp;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** One send. Each send replaces the run's previous snapshot. */
|
|
144
|
+
export type OnboardingRunSnapshot = InProgressRunSnapshot | CompletedRunSnapshot;
|