@ak--47/dungeon-master 1.5.4 → 1.6.1
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/.claude/skills/analyze-soup/SKILL.md +21 -12
- package/.claude/skills/create-dungeon/SKILL.md +65 -5
- package/.claude/skills/create-project/SKILL.md +1 -1
- package/.claude/skills/create-project/provision.mjs +57 -10
- package/.claude/skills/powertools/SKILL.md +75 -0
- package/.claude/skills/powertools/pt.mjs +91 -0
- package/.claude/skills/powertools/snapshot-project.mjs +124 -0
- package/.claude/skills/verify-dungeon/SKILL.md +56 -25
- package/.claude/skills/verify-dungeon/references/counting-semantics.md +26 -6
- package/.claude/skills/verify-dungeon/references/report-format.md +14 -8
- package/.claude/skills/write-hooks/SKILL.md +156 -18
- package/CHANGELOG.md +224 -0
- package/HOOKS.md +566 -112
- package/README.md +30 -9
- package/docs/guides/1.3.0-upgrade-guide.md +262 -0
- package/docs/guides/1.3.1-upgrade-guide.md +328 -0
- package/docs/guides/1.4.0-upgrade-guide.md +130 -0
- package/docs/guides/1.4.1-upgrade-guide.md +125 -0
- package/docs/guides/1.4.3-upgrade-guide.md +200 -0
- package/docs/guides/1.4.4-upgrade-guide.md +95 -0
- package/docs/guides/1.5.0-upgrade-guide.md +426 -0
- package/docs/guides/1.5.1-upgrade-guide.md +178 -0
- package/docs/guides/1.5.3-upgrade-guide.md +117 -0
- package/docs/guides/1.6.0-upgrade-guide.md +204 -0
- package/dungeons/technical/stories-verify.js +152 -0
- package/dungeons/vertical/README.md +154 -0
- package/dungeons/vertical/ai-platform/ai-platform.js +1424 -0
- package/dungeons/vertical/ai-platform/ai-platform.sql +184 -0
- package/dungeons/vertical/ai-platform/ai-platform.verify.mjs +71 -0
- package/dungeons/vertical/community/community.js +1589 -0
- package/dungeons/vertical/community/community.sql +216 -0
- package/dungeons/vertical/community/community.verify.mjs +72 -0
- package/dungeons/vertical/crypto/crypto.js +1483 -0
- package/dungeons/vertical/crypto/crypto.sql +194 -0
- package/dungeons/vertical/crypto/crypto.verify.mjs +78 -0
- package/dungeons/vertical/dating/dating.js +1540 -0
- package/dungeons/vertical/dating/dating.sql +288 -0
- package/dungeons/vertical/dating/dating.verify.mjs +72 -0
- package/dungeons/vertical/devtools/devtools.js +1524 -0
- package/dungeons/vertical/devtools/devtools.sql +161 -0
- package/dungeons/vertical/devtools/devtools.verify.mjs +73 -0
- package/dungeons/vertical/ecommerce/ecommerce.js +1480 -0
- package/dungeons/vertical/ecommerce/ecommerce.sql +218 -0
- package/dungeons/vertical/ecommerce/ecommerce.verify.mjs +70 -0
- package/dungeons/vertical/education/education.js +1763 -0
- package/dungeons/vertical/education/education.sql +243 -0
- package/dungeons/vertical/education/education.verify.mjs +75 -0
- package/dungeons/vertical/fintech/fintech.js +1410 -0
- package/dungeons/vertical/fintech/fintech.sql +175 -0
- package/dungeons/vertical/fintech/fintech.verify.mjs +70 -0
- package/dungeons/vertical/fitness/fitness.js +1495 -0
- package/dungeons/vertical/fitness/fitness.sql +206 -0
- package/dungeons/vertical/fitness/fitness.verify.mjs +71 -0
- package/dungeons/vertical/food-delivery/food-delivery.js +1641 -0
- package/dungeons/vertical/food-delivery/food-delivery.sql +183 -0
- package/dungeons/vertical/food-delivery/food-delivery.verify.mjs +75 -0
- package/dungeons/vertical/gaming/gaming.js +1741 -0
- package/dungeons/vertical/gaming/gaming.sql +249 -0
- package/dungeons/vertical/gaming/gaming.verify.mjs +70 -0
- package/dungeons/vertical/healthcare/healthcare.js +1488 -0
- package/dungeons/vertical/healthcare/healthcare.sql +193 -0
- package/dungeons/vertical/healthcare/healthcare.verify.mjs +72 -0
- package/dungeons/vertical/insurance-application/insurance-application.js +1852 -0
- package/dungeons/vertical/insurance-application/insurance-application.sql +174 -0
- package/dungeons/vertical/insurance-application/insurance-application.verify.mjs +74 -0
- package/dungeons/vertical/logistics/logistics.js +1541 -0
- package/dungeons/vertical/logistics/logistics.sql +205 -0
- package/dungeons/vertical/logistics/logistics.verify.mjs +73 -0
- package/dungeons/vertical/marketplace/marketplace.js +1438 -0
- package/dungeons/vertical/marketplace/marketplace.sql +148 -0
- package/dungeons/vertical/marketplace/marketplace.verify.mjs +74 -0
- package/dungeons/vertical/{media.js → media/media.js} +557 -58
- package/dungeons/vertical/media/media.sql +173 -0
- package/dungeons/vertical/media/media.verify.mjs +74 -0
- package/dungeons/vertical/real-estate/real-estate.js +1629 -0
- package/dungeons/vertical/real-estate/real-estate.sql +167 -0
- package/dungeons/vertical/real-estate/real-estate.verify.mjs +75 -0
- package/dungeons/vertical/sass/sass.js +1733 -0
- package/dungeons/vertical/sass/sass.sql +173 -0
- package/dungeons/vertical/sass/sass.verify.mjs +74 -0
- package/dungeons/vertical/social/social.js +1456 -0
- package/dungeons/vertical/social/social.sql +160 -0
- package/dungeons/vertical/social/social.verify.mjs +76 -0
- package/dungeons/vertical/streaming/streaming.js +677 -0
- package/dungeons/vertical/streaming/streaming.sql +101 -0
- package/dungeons/vertical/streaming/streaming.verify.mjs +75 -0
- package/dungeons/vertical/support-desk/support-desk.js +741 -0
- package/dungeons/vertical/support-desk/support-desk.sql +137 -0
- package/dungeons/vertical/support-desk/support-desk.verify.mjs +75 -0
- package/dungeons/vertical/travel/travel.js +1603 -0
- package/dungeons/vertical/travel/travel.sql +236 -0
- package/dungeons/vertical/travel/travel.verify.mjs +72 -0
- package/index.js +7 -3
- package/lib/core/config-validator.js +37 -2
- package/lib/core/context.js +4 -2
- package/lib/core/dungeon-loader.js +20 -15
- package/lib/core/dungeon-to-json.js +22 -4
- package/lib/core/storage.js +3 -3
- package/lib/generators/funnels.js +36 -9
- package/lib/hook-helpers/cohort.js +53 -0
- package/lib/hook-helpers/index.js +8 -0
- package/lib/hook-helpers/mutate.js +1 -1
- package/lib/hook-helpers/shape.js +319 -0
- package/lib/hook-patterns/aggregate-per-user-by-bin.js +13 -5
- package/lib/hook-patterns/attributed-by-source.js +60 -58
- package/lib/hook-patterns/frequency-by-frequency.js +14 -4
- package/lib/hook-patterns/funnel-frequency-breakdown.js +24 -12
- package/lib/hook-patterns/index.js +1 -1
- package/lib/hook-patterns/time-to-convert-by-segment.js +75 -13
- package/lib/orchestrators/user-loop.js +41 -0
- package/lib/templates/story-spec.schema.json +175 -0
- package/lib/utils/json-evaluator.js +1 -1
- package/lib/utils/utils.js +151 -59
- package/lib/verify/coerce.js +186 -0
- package/lib/verify/counting.js +270 -32
- package/lib/verify/emulate-breakdown.js +1285 -203
- package/lib/verify/first-time.js +89 -0
- package/lib/verify/flows.js +679 -0
- package/lib/verify/formula.js +259 -0
- package/lib/verify/funnel-engine.js +697 -113
- package/lib/verify/index.js +32 -4
- package/lib/verify/sessionize.js +275 -0
- package/lib/verify/story-runner.js +483 -0
- package/lib/verify/verify-dungeon.js +59 -37
- package/package.json +2 -1
- package/scripts/extract-dungeon-schema.mjs +2 -2
- package/scripts/run-many.mjs +2 -2
- package/scripts/verify-runner.mjs +2 -2
- package/scripts/verify-stories.mjs +244 -0
- package/types.d.ts +298 -30
- package/dungeons/vertical/ai-platform.js +0 -818
- package/dungeons/vertical/community.js +0 -966
- package/dungeons/vertical/crypto.js +0 -895
- package/dungeons/vertical/dating.js +0 -830
- package/dungeons/vertical/devtools.js +0 -1050
- package/dungeons/vertical/ecommerce.js +0 -811
- package/dungeons/vertical/education.js +0 -964
- package/dungeons/vertical/fintech.js +0 -946
- package/dungeons/vertical/fitness.js +0 -935
- package/dungeons/vertical/food-delivery.js +0 -934
- package/dungeons/vertical/gaming.js +0 -1120
- package/dungeons/vertical/healthcare.js +0 -945
- package/dungeons/vertical/insurance-application.js +0 -1039
- package/dungeons/vertical/logistics.js +0 -986
- package/dungeons/vertical/marketplace.js +0 -1019
- package/dungeons/vertical/real-estate.js +0 -872
- package/dungeons/vertical/sass.js +0 -995
- package/dungeons/vertical/social.js +0 -904
- package/dungeons/vertical/travel.js +0 -930
|
@@ -1,1039 +0,0 @@
|
|
|
1
|
-
// ── IMPORTS ──
|
|
2
|
-
import dayjs from "dayjs";
|
|
3
|
-
import utc from "dayjs/plugin/utc.js";
|
|
4
|
-
dayjs.extend(utc);
|
|
5
|
-
import "dotenv/config";
|
|
6
|
-
import * as u from "../../lib/utils/utils.js";
|
|
7
|
-
/** @typedef {import("../../types").Dungeon} Config */
|
|
8
|
-
|
|
9
|
-
// ── OVERVIEW ──
|
|
10
|
-
/*
|
|
11
|
-
* NAME: SafeHaven Insurance
|
|
12
|
-
* APP: Modern insurance web application. Users browse coverage options,
|
|
13
|
-
* request quotes, complete multi-step applications, manage policies,
|
|
14
|
-
* file claims, and make premium payments. Spans auto / home / life /
|
|
15
|
-
* health / renters insurance types across web, iOS, and Android.
|
|
16
|
-
* SCALE: 15,000 users, ~1.8M events, 121 days (2026-01-01 → 2026-05-01)
|
|
17
|
-
* CORE LOOP: account created → quote requested → application submitted → policy activated → payment made
|
|
18
|
-
*
|
|
19
|
-
* EVENTS (18):
|
|
20
|
-
* page viewed (10) > application step completed (6) > quote requested (5)
|
|
21
|
-
* > payment made (5) > quote received (4) > application started (4)
|
|
22
|
-
* > claim status checked (4) > support ticket created (4) > coverage reviewed (4)
|
|
23
|
-
* > document uploaded (3) > application submitted (3) > support ticket resolved (3)
|
|
24
|
-
* > application approved (2) > policy activated (2) > claim filed (2)
|
|
25
|
-
* > profile updated (2) > renewal completed (2) > account created (1)
|
|
26
|
-
*
|
|
27
|
-
* FUNNELS (5):
|
|
28
|
-
* - Onboarding: account created → page viewed → quote requested (85%)
|
|
29
|
-
* - Application Completion: application started → step completed → document uploaded → application submitted (60%)
|
|
30
|
-
* - Application Approval: application submitted → application approved → policy activated (70%)
|
|
31
|
-
* - Claims Process: claim filed → claim status checked → support ticket created (50%, A/B experiment)
|
|
32
|
-
* - Policy Renewal: coverage reviewed → payment made → renewal completed (65%)
|
|
33
|
-
*
|
|
34
|
-
* USER PROPS: Platform, insurance_type, app_version, age_range, risk_profile, policy_count, lifetime_premium, preferred_contact
|
|
35
|
-
* SUPER PROPS: Platform, insurance_type, app_version
|
|
36
|
-
* SCD PROPS: policy_status (pending/active/lapsed/renewed, monthly fuzzy, max 8)
|
|
37
|
-
* GROUPS: none
|
|
38
|
-
*/
|
|
39
|
-
|
|
40
|
-
// ── HOOK STORIES ──
|
|
41
|
-
/*
|
|
42
|
-
* NOTE: All cohort effects are HIDDEN — no flag stamping. Discoverable
|
|
43
|
-
* via raw-prop breakdowns (date, app_version, issue_category) or
|
|
44
|
-
* behavioral cohorts.
|
|
45
|
-
*
|
|
46
|
-
* -------------------------------------------------------------------
|
|
47
|
-
* 1. VERSION STAMPING (everything)
|
|
48
|
-
* -------------------------------------------------------------------
|
|
49
|
-
*
|
|
50
|
-
* PATTERN: Every event gets a deterministic app_version based on its
|
|
51
|
-
* final timestamp. All users shift simultaneously on release dates:
|
|
52
|
-
* - Days 0-30: v2.10
|
|
53
|
-
* - Days 30-60: v2.11
|
|
54
|
-
* - Days 60-90: v2.12
|
|
55
|
-
* - Last 10 days: v2.13
|
|
56
|
-
* app_version is a real product property already in superProps.
|
|
57
|
-
*
|
|
58
|
-
* HOW TO FIND IT IN MIXPANEL:
|
|
59
|
-
*
|
|
60
|
-
* Report 1: Event Volume by App Version
|
|
61
|
-
* - Report type: Insights
|
|
62
|
-
* - Event: "page viewed"
|
|
63
|
-
* - Measure: Total
|
|
64
|
-
* - Breakdown: "app_version"
|
|
65
|
-
* - Line chart by day
|
|
66
|
-
* - Expected: clean cutovers between versions, no overlap
|
|
67
|
-
*
|
|
68
|
-
* REAL-WORLD ANALOGUE: Forced auto-update SaaS apps cut all users over
|
|
69
|
-
* on release day, producing crisp version bands.
|
|
70
|
-
*
|
|
71
|
-
* -------------------------------------------------------------------
|
|
72
|
-
* 2. SUPPORT TICKET VOLUME DROP (everything)
|
|
73
|
-
* -------------------------------------------------------------------
|
|
74
|
-
*
|
|
75
|
-
* PATTERN: Pre-v2.13: each user gets 2-3 extra cloned support-ticket
|
|
76
|
-
* created events with bug-related issue_category values (form_crash,
|
|
77
|
-
* login_error, page_timeout, payment_failure — added to event config).
|
|
78
|
-
* Post-v2.13: tickets progressively removed (30% day 1 → 85% day 10).
|
|
79
|
-
* No flag — discover via issue_category breakdown over time.
|
|
80
|
-
*
|
|
81
|
-
* HOW TO FIND IT IN MIXPANEL:
|
|
82
|
-
*
|
|
83
|
-
* Report 1: Ticket Volume Over Time
|
|
84
|
-
* - Report type: Insights
|
|
85
|
-
* - Event: "support ticket created"
|
|
86
|
-
* - Measure: Total
|
|
87
|
-
* - Breakdown: "app_version"
|
|
88
|
-
* - Line chart by day
|
|
89
|
-
* - Expected: high volume on v2.12, sharp drop on v2.13
|
|
90
|
-
*
|
|
91
|
-
* Report 2: Bug Category Share Pre vs Post v2.13
|
|
92
|
-
* - Report type: Insights
|
|
93
|
-
* - Event: "support ticket created"
|
|
94
|
-
* - Measure: Total
|
|
95
|
-
* - Breakdown: "issue_category"
|
|
96
|
-
* - Compare pre-v2.13 vs v2.13 date ranges
|
|
97
|
-
* - Expected: form_crash / login_error / page_timeout / payment_failure
|
|
98
|
-
* dominate pre-v2.13, vanish post-v2.13
|
|
99
|
-
*
|
|
100
|
-
* REAL-WORLD ANALOGUE: A UX-fix release reduces ticket volume overnight.
|
|
101
|
-
*
|
|
102
|
-
* -------------------------------------------------------------------
|
|
103
|
-
* 3. APPLICATION CONVERSION BOOST (everything)
|
|
104
|
-
* -------------------------------------------------------------------
|
|
105
|
-
*
|
|
106
|
-
* PATTERN: Pre-v2.13: ~95% of users have ALL their application approved +
|
|
107
|
-
* policy activated events dropped (per-user gating). Post-v2.13: kept.
|
|
108
|
-
* No flag — discover via funnel by app_version or volume line chart over time.
|
|
109
|
-
*
|
|
110
|
-
* HOW TO FIND IT IN MIXPANEL:
|
|
111
|
-
*
|
|
112
|
-
* Report 1: Application Funnel by Version
|
|
113
|
-
* - Report type: Funnels
|
|
114
|
-
* - Steps: "application submitted" -> "application approved" -> "policy activated"
|
|
115
|
-
* - Breakdown: "app_version"
|
|
116
|
-
* - Expected: post-v2.13 ~ 1.3x pre-v2.13 conversion rate
|
|
117
|
-
* (~58% v2.13 vs ~44% v2.12; some dilution because users span versions)
|
|
118
|
-
*
|
|
119
|
-
* REAL-WORLD ANALOGUE: A buggy multi-step form fixed by release.
|
|
120
|
-
*
|
|
121
|
-
* -------------------------------------------------------------------
|
|
122
|
-
* 4. APPLICATION-STEP MAGIC NUMBER (everything)
|
|
123
|
-
* -------------------------------------------------------------------
|
|
124
|
-
*
|
|
125
|
-
* PATTERN: Users with 8-14 application step completed events sit in the
|
|
126
|
-
* sweet spot — approved_premium boosted +35%. Users with 15+ steps
|
|
127
|
-
* are over-engaged (likely fraud or signal review); 40% of their
|
|
128
|
-
* application approved events drop. No flag.
|
|
129
|
-
*
|
|
130
|
-
* HOW TO FIND IT IN MIXPANEL:
|
|
131
|
-
*
|
|
132
|
-
* Report 1: Avg Approved Premium by Step Bucket
|
|
133
|
-
* - Report type: Insights (with cohort)
|
|
134
|
-
* - Cohort A: users with 8-14 "application step completed"
|
|
135
|
-
* - Cohort B: users with 0-7
|
|
136
|
-
* - Event: "application approved"
|
|
137
|
-
* - Measure: Average of "approved_premium"
|
|
138
|
-
* - Expected: A ~ 1.35x B
|
|
139
|
-
*
|
|
140
|
-
* Report 2: Approvals per User on Heavy Step-Completers
|
|
141
|
-
* - Report type: Insights (with cohort)
|
|
142
|
-
* - Cohort C: users with >= 15 "application step completed"
|
|
143
|
-
* - Cohort A: users with 8-14
|
|
144
|
-
* - Event: "application approved"
|
|
145
|
-
* - Measure: Total per user
|
|
146
|
-
* - Expected: C ~ 40% fewer approvals per user vs A
|
|
147
|
-
*
|
|
148
|
-
* REAL-WORLD ANALOGUE: Engaged applicants get higher premiums approved;
|
|
149
|
-
* over-engaged ones look like fraud and get flagged.
|
|
150
|
-
*
|
|
151
|
-
* -------------------------------------------------------------------
|
|
152
|
-
* 5. APPLICATION COMPLETION TIME-TO-CONVERT (everything)
|
|
153
|
-
* -------------------------------------------------------------------
|
|
154
|
-
*
|
|
155
|
-
* PATTERN: In the everything hook, place each "application approved"
|
|
156
|
-
* event at a fixed target offset from the first "application started"
|
|
157
|
-
* event, determined by account_type (assigned via deterministic hash):
|
|
158
|
-
* - business: ~36h target offset (~0.75x baseline)
|
|
159
|
-
* - individual: ~48h target offset (baseline)
|
|
160
|
-
* - family: ~63h target offset (~1.31x baseline)
|
|
161
|
-
*
|
|
162
|
-
* HOW TO FIND IT IN MIXPANEL:
|
|
163
|
-
*
|
|
164
|
-
* Report 1: Application Funnel TTC by Account Type
|
|
165
|
-
* - Report type: Funnels
|
|
166
|
-
* - Steps: "application started" → "application approved"
|
|
167
|
-
* - Breakdown: account_type (from account created event)
|
|
168
|
-
* - Measure: Median time to convert
|
|
169
|
-
* - Expected: business < individual < family
|
|
170
|
-
*
|
|
171
|
-
* REAL-WORLD ANALOGUE: Business applicants have streamlined processes
|
|
172
|
-
* and pre-filled forms; family policies require more documentation.
|
|
173
|
-
*
|
|
174
|
-
* -------------------------------------------------------------------
|
|
175
|
-
* 6. CLAIMS PROCESS EXPERIMENT (funnel config — no hook code)
|
|
176
|
-
* -------------------------------------------------------------------
|
|
177
|
-
*
|
|
178
|
-
* PATTERN: A/B experiment on the Claims Process funnel. The engine
|
|
179
|
-
* fires $experiment_started events and applies conversion/TTC
|
|
180
|
-
* multipliers automatically. "Simplified Claims" variant gets 1.3x
|
|
181
|
-
* conversion and 0.8x time-to-convert. Experiment runs in the last
|
|
182
|
-
* 35 days of the dataset.
|
|
183
|
-
*
|
|
184
|
-
* HOW TO FIND IT IN MIXPANEL:
|
|
185
|
-
*
|
|
186
|
-
* Report 1: Experiment Results
|
|
187
|
-
* - Report type: Funnels
|
|
188
|
-
* - Steps: "claim filed" -> "claim status checked" -> "support ticket created"
|
|
189
|
-
* - Breakdown: "Variant name"
|
|
190
|
-
* - Date range: last 35 days of dataset
|
|
191
|
-
* - Expected: "Simplified Claims" has ~1.3x conversion vs "Control"
|
|
192
|
-
*
|
|
193
|
-
* Report 2: Experiment Started Events
|
|
194
|
-
* - Report type: Insights
|
|
195
|
-
* - Event: "$experiment_started"
|
|
196
|
-
* - Breakdown: "Variant name"
|
|
197
|
-
* - Expected: roughly equal distribution between Control and Simplified Claims
|
|
198
|
-
*
|
|
199
|
-
* REAL-WORLD ANALOGUE: A/B testing a simplified claims flow to reduce
|
|
200
|
-
* friction and improve completion rates.
|
|
201
|
-
*
|
|
202
|
-
* -------------------------------------------------------------------
|
|
203
|
-
* 7. RISK PROFILE AFFECTS APPROVAL (funnel-pre)
|
|
204
|
-
* -------------------------------------------------------------------
|
|
205
|
-
*
|
|
206
|
-
* PATTERN: In the funnel-pre hook, low-risk users get 1.8x approval
|
|
207
|
-
* funnel conversion (capped at 95%), high-risk users get 0.3x.
|
|
208
|
-
* Medium-risk users are unchanged.
|
|
209
|
-
*
|
|
210
|
-
* HOW TO FIND IT IN MIXPANEL:
|
|
211
|
-
*
|
|
212
|
-
* Report 1: Application Approval Funnel by Risk Profile
|
|
213
|
-
* - Report type: Funnels
|
|
214
|
-
* - Steps: "application submitted" -> "application approved" -> "policy activated"
|
|
215
|
-
* - Breakdown: "risk_profile" (user property)
|
|
216
|
-
* - Expected: low >> medium >> high conversion rates
|
|
217
|
-
*
|
|
218
|
-
* REAL-WORLD ANALOGUE: Underwriting engines auto-approve low-risk
|
|
219
|
-
* applicants and require manual review for high-risk ones.
|
|
220
|
-
*
|
|
221
|
-
* -------------------------------------------------------------------
|
|
222
|
-
* 8. DOCUMENT UPLOAD RETENTION (everything — retention magic number)
|
|
223
|
-
* -------------------------------------------------------------------
|
|
224
|
-
*
|
|
225
|
-
* PATTERN: Users who upload 3+ documents in their first 14 days
|
|
226
|
-
* retain normally. Users with fewer uploads lose 75% of events after
|
|
227
|
-
* day 30. Only affects born-in-dataset users.
|
|
228
|
-
*
|
|
229
|
-
* HOW TO FIND IT IN MIXPANEL:
|
|
230
|
-
*
|
|
231
|
-
* Report 1: Retention by Document Upload Cohort
|
|
232
|
-
* - Report type: Retention
|
|
233
|
-
* - Starting event: "account created"
|
|
234
|
-
* - Return event: any event
|
|
235
|
-
* - Cohort A: users with >= 3 "document uploaded" in first 14 days
|
|
236
|
-
* - Cohort B: users with < 3 "document uploaded" in first 14 days
|
|
237
|
-
* - Expected: Cohort A retains well past day 30; Cohort B drops off sharply
|
|
238
|
-
*
|
|
239
|
-
* Report 2: Post-Day-30 Event Volume
|
|
240
|
-
* - Report type: Insights
|
|
241
|
-
* - Event: all events
|
|
242
|
-
* - Compare doc-uploader cohort vs non-uploader
|
|
243
|
-
* - Expected: non-uploaders have ~75% fewer events after day 30
|
|
244
|
-
*
|
|
245
|
-
* REAL-WORLD ANALOGUE: Users who complete onboarding paperwork early
|
|
246
|
-
* are invested in the product and retain; those who don't churn.
|
|
247
|
-
*
|
|
248
|
-
* -------------------------------------------------------------------
|
|
249
|
-
* 9. END-OF-QUARTER RENEWAL SPIKE (everything — temporal)
|
|
250
|
-
* -------------------------------------------------------------------
|
|
251
|
-
*
|
|
252
|
-
* PATTERN: Days 85-95 of the dataset get 3x renewal completed events
|
|
253
|
-
* and 2x coverage reviewed events via cloning. Simulates end-of-quarter
|
|
254
|
-
* policy renewal batch processing.
|
|
255
|
-
*
|
|
256
|
-
* HOW TO FIND IT IN MIXPANEL:
|
|
257
|
-
*
|
|
258
|
-
* Report 1: Renewal Volume Over Time
|
|
259
|
-
* - Report type: Insights
|
|
260
|
-
* - Event: "renewal completed"
|
|
261
|
-
* - Measure: Total
|
|
262
|
-
* - Line chart by day
|
|
263
|
-
* - Expected: visible spike at days 85-95, ~3x baseline volume
|
|
264
|
-
*
|
|
265
|
-
* Report 2: Coverage Review Spike
|
|
266
|
-
* - Report type: Insights
|
|
267
|
-
* - Event: "coverage reviewed"
|
|
268
|
-
* - Line chart by day
|
|
269
|
-
* - Expected: ~2x volume during days 85-95
|
|
270
|
-
*
|
|
271
|
-
* REAL-WORLD ANALOGUE: Insurance companies batch-process renewals at
|
|
272
|
-
* quarter-end, producing predictable volume spikes.
|
|
273
|
-
*
|
|
274
|
-
* -------------------------------------------------------------------
|
|
275
|
-
* 10. CLAIM-TO-PREMIUM INCREASE (event — closure Map)
|
|
276
|
-
* -------------------------------------------------------------------
|
|
277
|
-
*
|
|
278
|
-
* PATTERN: After a user files a claim, their next "payment made" event
|
|
279
|
-
* gets premium_amount doubled (2.0x). Uses a module-level Map to
|
|
280
|
-
* track state across events. The Map entry is consumed (deleted) on
|
|
281
|
-
* the first payment after the claim, so the effect is one-shot.
|
|
282
|
-
*
|
|
283
|
-
* HOW TO FIND IT IN MIXPANEL:
|
|
284
|
-
*
|
|
285
|
-
* Report 1: Average Premium After Claim
|
|
286
|
-
* - Report type: Insights
|
|
287
|
-
* - Event: "payment made"
|
|
288
|
-
* - Measure: Average of "premium_amount"
|
|
289
|
-
* - Cohort A: users who did "claim filed" before
|
|
290
|
-
* - Cohort B: users who never filed a claim
|
|
291
|
-
* - Expected: Cohort A premium_amount ~ 2.0x Cohort B
|
|
292
|
-
*
|
|
293
|
-
* REAL-WORLD ANALOGUE: Insurance premiums increase after filing a claim.
|
|
294
|
-
*
|
|
295
|
-
* ===================================================================
|
|
296
|
-
* EXPECTED METRICS SUMMARY
|
|
297
|
-
* ===================================================================
|
|
298
|
-
*
|
|
299
|
-
* Hook | Metric | Baseline | Effect | Ratio
|
|
300
|
-
* ------------------------|-------------------------|----------|---------|------
|
|
301
|
-
* Version Stamping | Events per version | n/a | clean | bands
|
|
302
|
-
* Support Ticket Volume | tickets v2.12 -> v2.13 | 1x | ~ 0.3x | -70%
|
|
303
|
-
* Application Conversion | approval rate pre/post | 1x | ~ 1.3x | step-up
|
|
304
|
-
* Step-Count Magic Number | sweet approved_premium | 1x | 1.35x | 1.35x
|
|
305
|
-
* Step-Count Magic Number | over approvals/user | 1x | 0.6x | -40%
|
|
306
|
-
* Application TTC | business vs individual | 1x | 0.74x | faster
|
|
307
|
-
* Application TTC | family vs individual | 1x | 1.3x | slower
|
|
308
|
-
* Claims Experiment | simplified vs control | 1x | 1.3x | +30%
|
|
309
|
-
* Risk Profile Approval | low vs medium conv rate | 1x | 1.8x | +80%
|
|
310
|
-
* Risk Profile Approval | high vs medium conv rate| 1x | 0.3x | -70%
|
|
311
|
-
* Document Upload Ret. | post-d30 non-uploaders | 1x | 0.25x | -75%
|
|
312
|
-
* Renewal Spike | renewals d85-95 vs base | 1x | 3x | +200%
|
|
313
|
-
* Renewal Spike | reviews d85-95 vs base | 1x | 2x | +100%
|
|
314
|
-
* Claim-to-Premium | premium after claim | 1x | 2.0x | +100%
|
|
315
|
-
*/
|
|
316
|
-
|
|
317
|
-
// ── SCALE ──
|
|
318
|
-
const SEED = "dm4-insurance";
|
|
319
|
-
const NUM_USERS = 15_000;
|
|
320
|
-
const DATASET_START = "2026-01-01T00:00:00Z";
|
|
321
|
-
const DATASET_END = "2026-05-01T23:59:59Z";
|
|
322
|
-
const EVENTS_PER_DAY = 1.2;
|
|
323
|
-
const token = process.env.MP_TOKEN || "your-mixpanel-token";
|
|
324
|
-
|
|
325
|
-
const chance = u.initChance(SEED);
|
|
326
|
-
|
|
327
|
-
// ── KNOBS (tweak these to reshape stories) ──
|
|
328
|
-
// H1: Version stamping cutoffs (days from datasetStart / before datasetEnd)
|
|
329
|
-
const V211_DAY = 30;
|
|
330
|
-
const V212_DAY = 60;
|
|
331
|
-
const V213_DAYS_BEFORE_END = 10;
|
|
332
|
-
|
|
333
|
-
// H2: Support ticket volume
|
|
334
|
-
const TICKET_INJECT_MIN = 2;
|
|
335
|
-
const TICKET_INJECT_MAX = 3;
|
|
336
|
-
const TICKET_REMOVAL_BASE_PCT = 30;
|
|
337
|
-
const TICKET_REMOVAL_PER_DAY_PCT = 5.5;
|
|
338
|
-
const TICKET_REMOVAL_CAP_PCT = 85;
|
|
339
|
-
|
|
340
|
-
// H3: Application conversion boost
|
|
341
|
-
const PRE_V213_APPROVAL_DROP_LIKELIHOOD = 95;
|
|
342
|
-
|
|
343
|
-
// H4: Application-step magic number
|
|
344
|
-
const STEP_SWEET_MIN = 8;
|
|
345
|
-
const STEP_SWEET_MAX = 14;
|
|
346
|
-
const STEP_OVER_THRESHOLD = 15;
|
|
347
|
-
const STEP_PREMIUM_BOOST = 1.35;
|
|
348
|
-
const STEP_OVER_DROP_LIKELIHOOD = 40;
|
|
349
|
-
|
|
350
|
-
// H5: Application TTC target offsets (hours from first application started)
|
|
351
|
-
const TTC_BUSINESS_HOURS = 36;
|
|
352
|
-
const TTC_INDIVIDUAL_HOURS = 48;
|
|
353
|
-
const TTC_FAMILY_HOURS = 63;
|
|
354
|
-
|
|
355
|
-
// H7: Risk profile approval multipliers
|
|
356
|
-
const RISK_LOW_CONV_MULT = 1.8;
|
|
357
|
-
const RISK_HIGH_CONV_MULT = 0.3;
|
|
358
|
-
const RISK_CONV_CAP = 95;
|
|
359
|
-
|
|
360
|
-
// H8: Document upload retention
|
|
361
|
-
const DOC_RETENTION_MIN = 3;
|
|
362
|
-
const DOC_RETENTION_WINDOW_DAYS = 14;
|
|
363
|
-
const DOC_RETENTION_CUTOFF_DAYS = 30;
|
|
364
|
-
const DOC_RETENTION_DROP_LIKELIHOOD = 75;
|
|
365
|
-
|
|
366
|
-
// H9: End-of-quarter renewal spike (days from datasetStart)
|
|
367
|
-
const RENEWAL_SPIKE_START_DAY = 85;
|
|
368
|
-
const RENEWAL_SPIKE_END_DAY = 95;
|
|
369
|
-
const RENEWAL_CLONE_COUNT = 2; // 2 extra clones per renewal → 3x volume
|
|
370
|
-
const COVERAGE_CLONE_COUNT = 1; // 1 extra clone per coverage review → 2x volume
|
|
371
|
-
|
|
372
|
-
// H10: Claim-to-premium increase
|
|
373
|
-
const POST_CLAIM_PREMIUM_MULT = 2.0;
|
|
374
|
-
|
|
375
|
-
// ── HOOK STATE ──
|
|
376
|
-
// H10 closure: tracks users who filed a claim (one-shot consumption)
|
|
377
|
-
const claimFilers = new Map();
|
|
378
|
-
|
|
379
|
-
// ── HELPER FUNCTIONS ──
|
|
380
|
-
function handleFunnelPreHooks(record, meta) {
|
|
381
|
-
// H7: RISK PROFILE AFFECTS APPROVAL — low-risk 1.8x, high-risk 0.3x conversion
|
|
382
|
-
const isApprovalFunnel = meta.funnel?.sequence?.includes("application approved");
|
|
383
|
-
if (isApprovalFunnel) {
|
|
384
|
-
const risk = meta.profile?.risk_profile;
|
|
385
|
-
if (risk === "low") record.conversionRate = Math.min(RISK_CONV_CAP, Math.round(record.conversionRate * RISK_LOW_CONV_MULT));
|
|
386
|
-
else if (risk === "high") record.conversionRate = Math.round(record.conversionRate * RISK_HIGH_CONV_MULT);
|
|
387
|
-
}
|
|
388
|
-
return record;
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
function handleEventHooks(record) {
|
|
392
|
-
// H10: CLAIM-TO-PREMIUM INCREASE — premium doubled on first payment after claim
|
|
393
|
-
if (record.event === "claim filed") {
|
|
394
|
-
claimFilers.set(record.user_id, true);
|
|
395
|
-
}
|
|
396
|
-
if (record.event === "payment made" && claimFilers.has(record.user_id)) {
|
|
397
|
-
record.premium_amount = Math.round((record.premium_amount || 500) * POST_CLAIM_PREMIUM_MULT);
|
|
398
|
-
claimFilers.delete(record.user_id);
|
|
399
|
-
}
|
|
400
|
-
return record;
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
function handleEverythingHooks(record, meta) {
|
|
404
|
-
const datasetStart = dayjs.unix(meta.datasetStart);
|
|
405
|
-
const datasetEnd = dayjs.unix(meta.datasetEnd);
|
|
406
|
-
const V211_DATE = datasetStart.add(V211_DAY, "days");
|
|
407
|
-
const V212_DATE = datasetStart.add(V212_DAY, "days");
|
|
408
|
-
const V213_DATE = datasetEnd.subtract(V213_DAYS_BEFORE_END, "days");
|
|
409
|
-
const userEvents = record;
|
|
410
|
-
if (userEvents.length === 0) return record;
|
|
411
|
-
|
|
412
|
-
// Stamp superProps from profile for consistency
|
|
413
|
-
const profile = meta.profile;
|
|
414
|
-
userEvents.forEach(e => {
|
|
415
|
-
e.Platform = profile.Platform;
|
|
416
|
-
e.insurance_type = profile.insurance_type;
|
|
417
|
-
e.app_version = profile.app_version;
|
|
418
|
-
});
|
|
419
|
-
|
|
420
|
-
// Find a user_id from any existing event
|
|
421
|
-
const userId =
|
|
422
|
-
userEvents.find((e) => e.user_id)?.user_id ||
|
|
423
|
-
userEvents[0]?.device_id;
|
|
424
|
-
|
|
425
|
-
// ─── Hook #5: APPLICATION COMPLETION TIME-TO-CONVERT ───
|
|
426
|
-
// Runs FIRST: adjusts timestamps before version stamping.
|
|
427
|
-
// Business accounts complete the application funnel faster (0.74x),
|
|
428
|
-
// family accounts slower (1.3x). Individual stays at baseline.
|
|
429
|
-
// Assigns account_type deterministically per user via djb2 hash
|
|
430
|
-
// (independent of engine RNG). Places each "application approved"
|
|
431
|
-
// event at a fixed offset from the first "application started".
|
|
432
|
-
{
|
|
433
|
-
// djb2 hash of userId → deterministic cohort
|
|
434
|
-
let h = 5381;
|
|
435
|
-
for (let i = 0; i < userId.length; i++) {
|
|
436
|
-
h = ((h << 5) + h + userId.charCodeAt(i)) | 0;
|
|
437
|
-
}
|
|
438
|
-
const acctTypes = ["individual", "family", "business"];
|
|
439
|
-
const userAccountType = acctTypes[((h % 3) + 3) % 3];
|
|
440
|
-
// Stamp account created events with the deterministic type
|
|
441
|
-
for (const evt of userEvents) {
|
|
442
|
-
if (evt.event === "account created") {
|
|
443
|
-
evt.account_type = userAccountType;
|
|
444
|
-
}
|
|
445
|
-
}
|
|
446
|
-
userEvents.sort((a, b) => new Date(a.time) - new Date(b.time));
|
|
447
|
-
// Collect all "application started" times
|
|
448
|
-
const startedTimes = [];
|
|
449
|
-
for (const evt of userEvents) {
|
|
450
|
-
if (evt.event === "application started") {
|
|
451
|
-
startedTimes.push(dayjs(evt.time).valueOf());
|
|
452
|
-
}
|
|
453
|
-
}
|
|
454
|
-
if (startedTimes.length > 0) {
|
|
455
|
-
const firstStartTime = startedTimes[0];
|
|
456
|
-
// Target hours: biz=36, indiv=48, family=63
|
|
457
|
-
// Ratios: biz/indiv≈0.75, family/indiv≈1.31
|
|
458
|
-
const targetHours = (
|
|
459
|
-
userAccountType === "business" ? TTC_BUSINESS_HOURS :
|
|
460
|
-
userAccountType === "family" ? TTC_FAMILY_HOURS :
|
|
461
|
-
TTC_INDIVIDUAL_HOURS
|
|
462
|
-
);
|
|
463
|
-
const targetMs = targetHours * 3600000;
|
|
464
|
-
for (const evt of userEvents) {
|
|
465
|
-
if (evt.event !== "application approved") continue;
|
|
466
|
-
const evtTime = dayjs(evt.time).valueOf();
|
|
467
|
-
const origGap = evtTime - firstStartTime;
|
|
468
|
-
// Small jitter from original gap (mod 4h) for variance
|
|
469
|
-
const jitter = origGap > 0 ? (origGap % (4 * 3600000)) : 0;
|
|
470
|
-
evt.time = dayjs(firstStartTime + targetMs + jitter).toISOString();
|
|
471
|
-
}
|
|
472
|
-
}
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
// ─── Hook #2: SUPPORT TICKET VOLUME ───
|
|
476
|
-
// PRE-V2.13: Inject 2-3 extra support tickets with bug-related categories
|
|
477
|
-
const preV213Tickets = userEvents.filter(
|
|
478
|
-
(e) =>
|
|
479
|
-
e.event === "support ticket created" &&
|
|
480
|
-
dayjs(e.time).isBefore(V213_DATE)
|
|
481
|
-
);
|
|
482
|
-
|
|
483
|
-
if (preV213Tickets.length > 0) {
|
|
484
|
-
const extraCount = chance.integer({ min: TICKET_INJECT_MIN, max: TICKET_INJECT_MAX });
|
|
485
|
-
const bugCategories = [
|
|
486
|
-
"form_crash",
|
|
487
|
-
"login_error",
|
|
488
|
-
"page_timeout",
|
|
489
|
-
"payment_failure",
|
|
490
|
-
];
|
|
491
|
-
|
|
492
|
-
for (let i = 0; i < extraCount; i++) {
|
|
493
|
-
// Pick a random pre-v2.13 ticket to base timing on
|
|
494
|
-
const sourceTicket = chance.pickone(preV213Tickets);
|
|
495
|
-
const sourceTime = dayjs(sourceTicket.time);
|
|
496
|
-
// Offset by a few hours to days
|
|
497
|
-
const offsetHours = chance.integer({ min: 1, max: 72 });
|
|
498
|
-
let newTime = sourceTime.add(offsetHours, "hours");
|
|
499
|
-
// Ensure it stays before v2.13
|
|
500
|
-
if (newTime.isAfter(V213_DATE)) {
|
|
501
|
-
newTime = V213_DATE.subtract(
|
|
502
|
-
chance.integer({ min: 1, max: 48 }),
|
|
503
|
-
"hours"
|
|
504
|
-
);
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
// Compute app_version for injected event
|
|
508
|
-
let injectedVersion;
|
|
509
|
-
if (newTime.isBefore(V211_DATE)) {
|
|
510
|
-
injectedVersion = "2.10";
|
|
511
|
-
} else if (newTime.isBefore(V212_DATE)) {
|
|
512
|
-
injectedVersion = "2.11";
|
|
513
|
-
} else {
|
|
514
|
-
injectedVersion = "2.12"; // always pre-v2.13
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
userEvents.push({
|
|
518
|
-
...sourceTicket,
|
|
519
|
-
time: newTime.toISOString(),
|
|
520
|
-
user_id: userId,
|
|
521
|
-
app_version: injectedVersion,
|
|
522
|
-
issue_category: chance.pickone(bugCategories),
|
|
523
|
-
priority: chance.pickone(["medium", "high", "high"]),
|
|
524
|
-
channel: chance.pickone([
|
|
525
|
-
"chat",
|
|
526
|
-
"phone",
|
|
527
|
-
"email",
|
|
528
|
-
"web_form",
|
|
529
|
-
]),
|
|
530
|
-
});
|
|
531
|
-
}
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
// POST-V2.13: Remove support tickets with increasing probability
|
|
535
|
-
// Day 1 after release: ~30% removal
|
|
536
|
-
// Day 5: ~60% removal
|
|
537
|
-
// Day 10: ~85% removal
|
|
538
|
-
for (let i = userEvents.length - 1; i >= 0; i--) {
|
|
539
|
-
const evt = userEvents[i];
|
|
540
|
-
if (evt.event === "support ticket created") {
|
|
541
|
-
const evtTime = dayjs(evt.time);
|
|
542
|
-
if (evtTime.isAfter(V213_DATE)) {
|
|
543
|
-
const daysSinceRelease = evtTime.diff(
|
|
544
|
-
V213_DATE,
|
|
545
|
-
"days",
|
|
546
|
-
true
|
|
547
|
-
);
|
|
548
|
-
// Linear ramp: 30% base + 5.5% per day → ~85% at day 10
|
|
549
|
-
const removalLikelihood = Math.min(
|
|
550
|
-
TICKET_REMOVAL_CAP_PCT,
|
|
551
|
-
TICKET_REMOVAL_BASE_PCT + daysSinceRelease * TICKET_REMOVAL_PER_DAY_PCT
|
|
552
|
-
);
|
|
553
|
-
if (chance.bool({ likelihood: removalLikelihood })) {
|
|
554
|
-
userEvents.splice(i, 1);
|
|
555
|
-
}
|
|
556
|
-
}
|
|
557
|
-
}
|
|
558
|
-
}
|
|
559
|
-
|
|
560
|
-
// ─── Hook #3: APPLICATION CONVERSION BOOST ───
|
|
561
|
-
// PRE-V2.13: Remove ALL application approved + policy activated events
|
|
562
|
-
// for ~95% of users (per-user gating, not per-event). This produces
|
|
563
|
-
// visible funnel completion gap pre-v2.13 vs post-v2.13.
|
|
564
|
-
if (chance.bool({ likelihood: PRE_V213_APPROVAL_DROP_LIKELIHOOD })) {
|
|
565
|
-
for (let i = userEvents.length - 1; i >= 0; i--) {
|
|
566
|
-
const evt = userEvents[i];
|
|
567
|
-
if (
|
|
568
|
-
(evt.event === "application approved" ||
|
|
569
|
-
evt.event === "policy activated") &&
|
|
570
|
-
dayjs(evt.time).isBefore(V213_DATE)
|
|
571
|
-
) {
|
|
572
|
-
userEvents.splice(i, 1);
|
|
573
|
-
}
|
|
574
|
-
}
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
// Hook 4: APPLICATION-STEP MAGIC NUMBER (no flags)
|
|
578
|
-
// Sweet 8-14 application step completed → +35% on approved_premium
|
|
579
|
-
// for application approved events. Over 15+ → drop 40% of
|
|
580
|
-
// application approved events (fraud filter triggers).
|
|
581
|
-
const stepCount = userEvents.filter(e => e.event === "application step completed").length;
|
|
582
|
-
if (stepCount >= STEP_SWEET_MIN && stepCount <= STEP_SWEET_MAX) {
|
|
583
|
-
userEvents.forEach(e => {
|
|
584
|
-
if (e.event === "application approved" && typeof e.approved_premium === "number") {
|
|
585
|
-
e.approved_premium = Math.round(e.approved_premium * STEP_PREMIUM_BOOST);
|
|
586
|
-
}
|
|
587
|
-
});
|
|
588
|
-
} else if (stepCount >= STEP_OVER_THRESHOLD) {
|
|
589
|
-
for (let i = userEvents.length - 1; i >= 0; i--) {
|
|
590
|
-
if (userEvents[i].event === "application approved" && chance.bool({ likelihood: STEP_OVER_DROP_LIKELIHOOD })) {
|
|
591
|
-
userEvents.splice(i, 1);
|
|
592
|
-
}
|
|
593
|
-
}
|
|
594
|
-
}
|
|
595
|
-
|
|
596
|
-
// ─── Hook #8: DOCUMENT UPLOAD RETENTION ───
|
|
597
|
-
// Users with 3+ "document uploaded" in first 14 days retain;
|
|
598
|
-
// others lose 75% of events post-day-30.
|
|
599
|
-
if (meta.userIsBornInDataset) {
|
|
600
|
-
const firstT = userEvents[0]?.time;
|
|
601
|
-
if (firstT) {
|
|
602
|
-
const window14 = dayjs(firstT).add(DOC_RETENTION_WINDOW_DAYS, "days").toISOString();
|
|
603
|
-
const docUploads = userEvents.filter(
|
|
604
|
-
(e) => e.event === "document uploaded" && e.time <= window14
|
|
605
|
-
).length;
|
|
606
|
-
if (docUploads < DOC_RETENTION_MIN) {
|
|
607
|
-
const cutoff = dayjs(firstT).add(DOC_RETENTION_CUTOFF_DAYS, "days");
|
|
608
|
-
for (let i = userEvents.length - 1; i >= 0; i--) {
|
|
609
|
-
if (
|
|
610
|
-
dayjs(userEvents[i].time).isAfter(cutoff) &&
|
|
611
|
-
chance.bool({ likelihood: DOC_RETENTION_DROP_LIKELIHOOD })
|
|
612
|
-
) {
|
|
613
|
-
userEvents.splice(i, 1);
|
|
614
|
-
}
|
|
615
|
-
}
|
|
616
|
-
}
|
|
617
|
-
}
|
|
618
|
-
}
|
|
619
|
-
|
|
620
|
-
// ─── Hook #9: END-OF-QUARTER RENEWAL SPIKE ───
|
|
621
|
-
// Days 85-95 get 3x renewal clones + 2x coverage reviewed clones
|
|
622
|
-
{
|
|
623
|
-
const spikeStart = datasetStart.add(RENEWAL_SPIKE_START_DAY, "days");
|
|
624
|
-
const spikeEnd = datasetStart.add(RENEWAL_SPIKE_END_DAY, "days");
|
|
625
|
-
const clones = [];
|
|
626
|
-
userEvents.forEach((e) => {
|
|
627
|
-
const t = dayjs(e.time);
|
|
628
|
-
if (t.isAfter(spikeStart) && t.isBefore(spikeEnd)) {
|
|
629
|
-
if (e.event === "renewal completed") {
|
|
630
|
-
for (let c = 0; c < RENEWAL_CLONE_COUNT; c++) {
|
|
631
|
-
clones.push({
|
|
632
|
-
...e,
|
|
633
|
-
time: t
|
|
634
|
-
.add(
|
|
635
|
-
chance.integer({ min: 5, max: 240 }),
|
|
636
|
-
"minutes"
|
|
637
|
-
)
|
|
638
|
-
.toISOString(),
|
|
639
|
-
insert_id: chance.guid(),
|
|
640
|
-
});
|
|
641
|
-
}
|
|
642
|
-
}
|
|
643
|
-
if (e.event === "coverage reviewed") {
|
|
644
|
-
for (let c = 0; c < COVERAGE_CLONE_COUNT; c++) {
|
|
645
|
-
clones.push({
|
|
646
|
-
...e,
|
|
647
|
-
time: t
|
|
648
|
-
.add(
|
|
649
|
-
chance.integer({ min: 5, max: 120 }),
|
|
650
|
-
"minutes"
|
|
651
|
-
)
|
|
652
|
-
.toISOString(),
|
|
653
|
-
insert_id: chance.guid(),
|
|
654
|
-
});
|
|
655
|
-
}
|
|
656
|
-
}
|
|
657
|
-
}
|
|
658
|
-
});
|
|
659
|
-
if (clones.length) userEvents.push(...clones);
|
|
660
|
-
}
|
|
661
|
-
|
|
662
|
-
// ─── Hook #1: VERSION STAMPING ───
|
|
663
|
-
// Runs LAST: stamps app_version based on FINAL timestamps
|
|
664
|
-
// (after Hook #5 TTC scaling has adjusted event times).
|
|
665
|
-
// v2.10 (days 0-30) → v2.11 (30-60) → v2.12 (60-90) → v2.13 (last 10 days)
|
|
666
|
-
for (const evt of userEvents) {
|
|
667
|
-
const eventTime = dayjs(evt.time);
|
|
668
|
-
if (eventTime.isBefore(V211_DATE)) {
|
|
669
|
-
evt.app_version = "2.10";
|
|
670
|
-
} else if (eventTime.isBefore(V212_DATE)) {
|
|
671
|
-
evt.app_version = "2.11";
|
|
672
|
-
} else if (eventTime.isBefore(V213_DATE)) {
|
|
673
|
-
evt.app_version = "2.12";
|
|
674
|
-
} else {
|
|
675
|
-
evt.app_version = "2.13";
|
|
676
|
-
}
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
// Sort events by time after injection/removal/shifting
|
|
680
|
-
userEvents.sort(
|
|
681
|
-
(a, b) => new Date(a.time) - new Date(b.time)
|
|
682
|
-
);
|
|
683
|
-
|
|
684
|
-
return record;
|
|
685
|
-
}
|
|
686
|
-
|
|
687
|
-
// ── CONFIG ──
|
|
688
|
-
/** @type {Config} */
|
|
689
|
-
const config = {
|
|
690
|
-
version: 2,
|
|
691
|
-
seed: SEED,
|
|
692
|
-
datasetStart: DATASET_START,
|
|
693
|
-
datasetEnd: DATASET_END,
|
|
694
|
-
avgEventsPerUserPerDay: EVENTS_PER_DAY,
|
|
695
|
-
numUsers: NUM_USERS,
|
|
696
|
-
format: "json",
|
|
697
|
-
gzip: true,
|
|
698
|
-
credentials: {
|
|
699
|
-
token,
|
|
700
|
-
},
|
|
701
|
-
switches: {
|
|
702
|
-
hasSessionIds: true,
|
|
703
|
-
alsoInferFunnels: false,
|
|
704
|
-
hasLocation: true,
|
|
705
|
-
hasAndroidDevices: true,
|
|
706
|
-
hasIOSDevices: true,
|
|
707
|
-
hasDesktopDevices: true,
|
|
708
|
-
hasBrowser: false,
|
|
709
|
-
hasCampaigns: false,
|
|
710
|
-
isAnonymous: false,
|
|
711
|
-
hasAdSpend: false,
|
|
712
|
-
hasAvatar: true,
|
|
713
|
-
},
|
|
714
|
-
identity: {
|
|
715
|
-
avgDevicePerUser: 2,
|
|
716
|
-
},
|
|
717
|
-
concurrency: 1,
|
|
718
|
-
writeToDisk: false,
|
|
719
|
-
|
|
720
|
-
scdProps: {
|
|
721
|
-
policy_status: {
|
|
722
|
-
values: ["pending", "active", "lapsed", "renewed"],
|
|
723
|
-
frequency: "month",
|
|
724
|
-
timing: "fuzzy",
|
|
725
|
-
max: 8
|
|
726
|
-
}
|
|
727
|
-
},
|
|
728
|
-
mirrorProps: {},
|
|
729
|
-
lookupTables: [],
|
|
730
|
-
|
|
731
|
-
events: [
|
|
732
|
-
{
|
|
733
|
-
event: "account created",
|
|
734
|
-
weight: 1,
|
|
735
|
-
isFirstEvent: true,
|
|
736
|
-
isAuthEvent: true,
|
|
737
|
-
properties: {
|
|
738
|
-
signup_source: ["web", "mobile", "agent_referral", "partner"],
|
|
739
|
-
account_type: ["individual", "family", "business"],
|
|
740
|
-
},
|
|
741
|
-
},
|
|
742
|
-
{
|
|
743
|
-
event: "page viewed",
|
|
744
|
-
weight: 10,
|
|
745
|
-
properties: {
|
|
746
|
-
page_name: [
|
|
747
|
-
"home",
|
|
748
|
-
"quotes",
|
|
749
|
-
"coverage_options",
|
|
750
|
-
"claims",
|
|
751
|
-
"faq",
|
|
752
|
-
"profile",
|
|
753
|
-
"payment",
|
|
754
|
-
"documents",
|
|
755
|
-
],
|
|
756
|
-
referrer: ["direct", "google", "email", "social_media"],
|
|
757
|
-
},
|
|
758
|
-
},
|
|
759
|
-
{
|
|
760
|
-
event: "quote requested",
|
|
761
|
-
weight: 5,
|
|
762
|
-
properties: {
|
|
763
|
-
coverage_level: ["basic", "standard", "premium"],
|
|
764
|
-
deductible: u.weighNumRange(250, 5000, 0.5, 1000),
|
|
765
|
-
},
|
|
766
|
-
},
|
|
767
|
-
{
|
|
768
|
-
event: "quote received",
|
|
769
|
-
weight: 4,
|
|
770
|
-
properties: {
|
|
771
|
-
monthly_premium: u.weighNumRange(30, 800, 0.3, 150),
|
|
772
|
-
coverage_amount: u.weighNumRange(10000, 500000, 0.3, 100000),
|
|
773
|
-
quote_comparison_count: u.weighNumRange(1, 5, 0.5, 2),
|
|
774
|
-
},
|
|
775
|
-
},
|
|
776
|
-
{
|
|
777
|
-
event: "application started",
|
|
778
|
-
weight: 4,
|
|
779
|
-
isStrictEvent: false,
|
|
780
|
-
properties: {
|
|
781
|
-
coverage_level: ["basic", "standard", "premium"],
|
|
782
|
-
estimated_premium: u.weighNumRange(30, 800, 0.3, 150),
|
|
783
|
-
},
|
|
784
|
-
},
|
|
785
|
-
{
|
|
786
|
-
event: "application step completed",
|
|
787
|
-
weight: 6,
|
|
788
|
-
isStrictEvent: false,
|
|
789
|
-
properties: {
|
|
790
|
-
step_name: [
|
|
791
|
-
"personal_info",
|
|
792
|
-
"coverage_selection",
|
|
793
|
-
"medical_history",
|
|
794
|
-
"vehicle_info",
|
|
795
|
-
"beneficiary",
|
|
796
|
-
"review",
|
|
797
|
-
],
|
|
798
|
-
step_number: u.weighNumRange(1, 6),
|
|
799
|
-
time_on_step_sec: u.weighNumRange(15, 600, 0.3, 90),
|
|
800
|
-
},
|
|
801
|
-
},
|
|
802
|
-
{
|
|
803
|
-
event: "document uploaded",
|
|
804
|
-
weight: 3,
|
|
805
|
-
isStrictEvent: false,
|
|
806
|
-
properties: {
|
|
807
|
-
document_type: [
|
|
808
|
-
"drivers_license",
|
|
809
|
-
"proof_of_address",
|
|
810
|
-
"vehicle_registration",
|
|
811
|
-
"medical_records",
|
|
812
|
-
"property_photos",
|
|
813
|
-
],
|
|
814
|
-
file_size_kb: u.weighNumRange(50, 5000, 0.5, 500),
|
|
815
|
-
},
|
|
816
|
-
},
|
|
817
|
-
{
|
|
818
|
-
event: "application submitted",
|
|
819
|
-
weight: 3,
|
|
820
|
-
properties: {
|
|
821
|
-
documents_attached: u.weighNumRange(1, 5, 0.5, 2),
|
|
822
|
-
application_time_min: u.weighNumRange(5, 60, 0.3, 15),
|
|
823
|
-
},
|
|
824
|
-
},
|
|
825
|
-
{
|
|
826
|
-
event: "application approved",
|
|
827
|
-
weight: 2,
|
|
828
|
-
isStrictEvent: false,
|
|
829
|
-
properties: {
|
|
830
|
-
approved_premium: u.weighNumRange(30, 800, 0.3, 150),
|
|
831
|
-
approval_time_hours: u.weighNumRange(1, 72, 0.5, 24),
|
|
832
|
-
},
|
|
833
|
-
},
|
|
834
|
-
{
|
|
835
|
-
event: "policy activated",
|
|
836
|
-
weight: 2,
|
|
837
|
-
isStrictEvent: false,
|
|
838
|
-
properties: {
|
|
839
|
-
policy_term_months: [6, 12, 12, 12, 24],
|
|
840
|
-
effective_date_offset_days: u.weighNumRange(0, 30, 0.5, 7),
|
|
841
|
-
},
|
|
842
|
-
},
|
|
843
|
-
{
|
|
844
|
-
event: "claim filed",
|
|
845
|
-
weight: 2,
|
|
846
|
-
isStrictEvent: false,
|
|
847
|
-
properties: {
|
|
848
|
-
claim_type: [
|
|
849
|
-
"collision",
|
|
850
|
-
"theft",
|
|
851
|
-
"water_damage",
|
|
852
|
-
"fire",
|
|
853
|
-
"medical",
|
|
854
|
-
"liability",
|
|
855
|
-
],
|
|
856
|
-
estimated_amount: u.weighNumRange(100, 50000, 0.3, 3000),
|
|
857
|
-
},
|
|
858
|
-
},
|
|
859
|
-
{
|
|
860
|
-
event: "claim status checked",
|
|
861
|
-
weight: 4,
|
|
862
|
-
properties: {
|
|
863
|
-
claim_status: [
|
|
864
|
-
"submitted",
|
|
865
|
-
"under_review",
|
|
866
|
-
"approved",
|
|
867
|
-
"denied",
|
|
868
|
-
"payment_pending",
|
|
869
|
-
],
|
|
870
|
-
days_since_filed: u.weighNumRange(1, 60, 0.5, 10),
|
|
871
|
-
},
|
|
872
|
-
},
|
|
873
|
-
{
|
|
874
|
-
event: "payment made",
|
|
875
|
-
weight: 5,
|
|
876
|
-
isStrictEvent: false,
|
|
877
|
-
properties: {
|
|
878
|
-
payment_method: ["credit_card", "bank_transfer", "auto_pay", "check"],
|
|
879
|
-
amount: u.weighNumRange(30, 800, 0.3, 150),
|
|
880
|
-
premium_amount: u.weighNumRange(50, 600, 0.3, 150),
|
|
881
|
-
payment_status: ["success", "success", "success", "success", "failed"],
|
|
882
|
-
},
|
|
883
|
-
},
|
|
884
|
-
{
|
|
885
|
-
event: "support ticket created",
|
|
886
|
-
weight: 4,
|
|
887
|
-
isStrictEvent: false,
|
|
888
|
-
properties: {
|
|
889
|
-
issue_category: [
|
|
890
|
-
"billing",
|
|
891
|
-
"claims",
|
|
892
|
-
"coverage",
|
|
893
|
-
"technical",
|
|
894
|
-
"policy_change",
|
|
895
|
-
"form_crash",
|
|
896
|
-
"login_error",
|
|
897
|
-
"page_timeout",
|
|
898
|
-
"payment_failure",
|
|
899
|
-
],
|
|
900
|
-
priority: ["low", "medium", "medium", "high"],
|
|
901
|
-
channel: ["chat", "phone", "email", "web_form"],
|
|
902
|
-
},
|
|
903
|
-
},
|
|
904
|
-
{
|
|
905
|
-
event: "support ticket resolved",
|
|
906
|
-
weight: 3,
|
|
907
|
-
properties: {
|
|
908
|
-
resolution_type: [
|
|
909
|
-
"self_service",
|
|
910
|
-
"agent_assisted",
|
|
911
|
-
"escalated",
|
|
912
|
-
"auto_resolved",
|
|
913
|
-
],
|
|
914
|
-
satisfaction_score: u.weighNumRange(1, 5, 1, 4),
|
|
915
|
-
resolution_time_hours: u.weighNumRange(0.5, 72, 0.3, 8),
|
|
916
|
-
},
|
|
917
|
-
},
|
|
918
|
-
{
|
|
919
|
-
event: "coverage reviewed",
|
|
920
|
-
weight: 4,
|
|
921
|
-
isStrictEvent: false,
|
|
922
|
-
properties: {
|
|
923
|
-
current_premium: u.weighNumRange(30, 800, 0.3, 150),
|
|
924
|
-
coverage_adequate: [true, true, true, false],
|
|
925
|
-
},
|
|
926
|
-
},
|
|
927
|
-
{
|
|
928
|
-
event: "profile updated",
|
|
929
|
-
weight: 2,
|
|
930
|
-
properties: {
|
|
931
|
-
field_updated: [
|
|
932
|
-
"address",
|
|
933
|
-
"phone",
|
|
934
|
-
"email",
|
|
935
|
-
"beneficiary",
|
|
936
|
-
"payment_method",
|
|
937
|
-
"vehicle_info",
|
|
938
|
-
],
|
|
939
|
-
},
|
|
940
|
-
},
|
|
941
|
-
{
|
|
942
|
-
event: "renewal completed",
|
|
943
|
-
weight: 2,
|
|
944
|
-
isStrictEvent: false,
|
|
945
|
-
properties: {
|
|
946
|
-
renewal_premium: u.weighNumRange(30, 800, 0.3, 150),
|
|
947
|
-
premium_change_pct: u.weighNumRange(-15, 20, 1, 3),
|
|
948
|
-
auto_renewed: [true, true, false],
|
|
949
|
-
},
|
|
950
|
-
},
|
|
951
|
-
],
|
|
952
|
-
|
|
953
|
-
funnels: [
|
|
954
|
-
{
|
|
955
|
-
name: "Onboarding",
|
|
956
|
-
sequence: ["account created", "page viewed", "quote requested"],
|
|
957
|
-
isFirstFunnel: true,
|
|
958
|
-
conversionRate: 85,
|
|
959
|
-
timeToConvert: 0.5,
|
|
960
|
-
},
|
|
961
|
-
{
|
|
962
|
-
name: "Application Completion",
|
|
963
|
-
sequence: [
|
|
964
|
-
"application started",
|
|
965
|
-
"application step completed",
|
|
966
|
-
"document uploaded",
|
|
967
|
-
"application submitted",
|
|
968
|
-
],
|
|
969
|
-
conversionRate: 60,
|
|
970
|
-
timeToConvert: 48,
|
|
971
|
-
weight: 4,
|
|
972
|
-
order: "sequential",
|
|
973
|
-
},
|
|
974
|
-
{
|
|
975
|
-
name: "Application Approval",
|
|
976
|
-
sequence: [
|
|
977
|
-
"application submitted",
|
|
978
|
-
"application approved",
|
|
979
|
-
"policy activated",
|
|
980
|
-
],
|
|
981
|
-
conversionRate: 70,
|
|
982
|
-
timeToConvert: 72,
|
|
983
|
-
weight: 3,
|
|
984
|
-
order: "sequential",
|
|
985
|
-
},
|
|
986
|
-
{
|
|
987
|
-
name: "Claims Process",
|
|
988
|
-
sequence: [
|
|
989
|
-
"claim filed",
|
|
990
|
-
"claim status checked",
|
|
991
|
-
"support ticket created",
|
|
992
|
-
],
|
|
993
|
-
conversionRate: 50,
|
|
994
|
-
timeToConvert: 24,
|
|
995
|
-
weight: 2,
|
|
996
|
-
experiment: {
|
|
997
|
-
name: "Simplified Claims Flow",
|
|
998
|
-
variants: [
|
|
999
|
-
{ name: "Control" },
|
|
1000
|
-
{ name: "Simplified Claims", conversionMultiplier: 1.3, ttcMultiplier: 0.8 },
|
|
1001
|
-
],
|
|
1002
|
-
startDaysBeforeEnd: 35,
|
|
1003
|
-
},
|
|
1004
|
-
},
|
|
1005
|
-
{
|
|
1006
|
-
name: "Policy Renewal",
|
|
1007
|
-
sequence: ["coverage reviewed", "payment made", "renewal completed"],
|
|
1008
|
-
conversionRate: 65,
|
|
1009
|
-
timeToConvert: 72,
|
|
1010
|
-
weight: 3,
|
|
1011
|
-
},
|
|
1012
|
-
],
|
|
1013
|
-
|
|
1014
|
-
superProps: {
|
|
1015
|
-
Platform: ["web", "ios", "android"],
|
|
1016
|
-
insurance_type: ["auto", "home", "life", "health", "renters"],
|
|
1017
|
-
app_version: ["2.10"],
|
|
1018
|
-
},
|
|
1019
|
-
|
|
1020
|
-
userProps: {
|
|
1021
|
-
Platform: ["web", "ios", "android"],
|
|
1022
|
-
insurance_type: ["auto", "home", "life", "health", "renters"],
|
|
1023
|
-
app_version: ["2.10"],
|
|
1024
|
-
age_range: ["18-25", "26-35", "36-45", "46-55", "56-65", "65+"],
|
|
1025
|
-
risk_profile: ["low", "medium", "high"],
|
|
1026
|
-
policy_count: u.weighNumRange(0, 5, 0.5, 1),
|
|
1027
|
-
lifetime_premium: u.weighNumRange(0, 50000, 0.3, 5000),
|
|
1028
|
-
preferred_contact: ["email", "phone", "app_notification"],
|
|
1029
|
-
},
|
|
1030
|
-
|
|
1031
|
-
hook(record, type, meta) {
|
|
1032
|
-
if (type === "funnel-pre") return handleFunnelPreHooks(record, meta);
|
|
1033
|
-
if (type === "event") return handleEventHooks(record);
|
|
1034
|
-
if (type === "everything") return handleEverythingHooks(record, meta);
|
|
1035
|
-
return record;
|
|
1036
|
-
},
|
|
1037
|
-
};
|
|
1038
|
-
|
|
1039
|
-
export default config;
|