@ak--47/dungeon-master 1.5.3 → 1.6.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/.claude/skills/analyze-soup/SKILL.md +21 -12
- package/.claude/skills/create-dungeon/SKILL.md +65 -5
- package/.claude/skills/create-project/SKILL.md +96 -0
- package/.claude/skills/create-project/provision.mjs +336 -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 +211 -0
- package/HOOKS.md +566 -112
- package/README.md +28 -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 +2 -2
- 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 +5 -4
- 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 +3 -2
- 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 +296 -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,946 +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
|
-
import { findFirstSequence, scaleFunnelTTC } from "../../lib/hook-helpers/timing.js";
|
|
8
|
-
/** @typedef {import("../../types").Dungeon} Config */
|
|
9
|
-
|
|
10
|
-
// ── OVERVIEW ──
|
|
11
|
-
/*
|
|
12
|
-
* NAME: NexBank
|
|
13
|
-
* APP: Chime/Revolut-style neobank app. Users open accounts (personal or
|
|
14
|
-
* business), transact across 7 merchant categories, send transfers,
|
|
15
|
-
* pay bills, set budgets, invest, apply for loans, and earn
|
|
16
|
-
* tier-scaled rewards. Core loop runs from onboarding through daily
|
|
17
|
-
* banking, financial planning, investments, and rewards.
|
|
18
|
-
* SCALE: 10,000 users, ~1.4M events, 121 days (2026-01-01 → 2026-05-01)
|
|
19
|
-
* CORE LOOP: account opened → app session → balance checked → transaction completed
|
|
20
|
-
*
|
|
21
|
-
* EVENTS (19):
|
|
22
|
-
* app session (20) > transaction completed (18) > balance checked (15)
|
|
23
|
-
* > notification opened (10) > transfer sent (8) > bill paid (6)
|
|
24
|
-
* > investment made (4) > reward redeemed (4) > budget alert (4)
|
|
25
|
-
* > budget created (3) > savings goal set (3) > support contacted (3)
|
|
26
|
-
* > card locked (2) > dispute filed (2) > loan applied (2) > premium upgraded (2)
|
|
27
|
-
* > account opened (1) > loan approved (1) > bill payment missed (1)
|
|
28
|
-
*
|
|
29
|
-
* FUNNELS (8):
|
|
30
|
-
* - Onboarding: account opened → app session → balance checked (85%)
|
|
31
|
-
* - Daily Banking: app session → balance checked → transaction completed (80%)
|
|
32
|
-
* - Transfers: app session → transfer sent → notification opened (50%)
|
|
33
|
-
* - Bill Payment: app session → bill paid → notification opened (60%)
|
|
34
|
-
* - Financial Planning: budget created → budget alert → savings goal set (40%)
|
|
35
|
-
* - Investment: balance checked → investment made → reward redeemed (30%)
|
|
36
|
-
* - Support: support contacted → card locked → dispute filed (35%)
|
|
37
|
-
* - Lending: loan applied → loan approved → premium upgraded (25%)
|
|
38
|
-
*
|
|
39
|
-
* USER PROPS: account_tier, Platform, credit_score_range, income_bracket, account_age_months, total_balance, has_direct_deposit, account_segment, employee_count, annual_revenue, industry, age_range, life_stage
|
|
40
|
-
* SUPER PROPS: account_tier, Platform
|
|
41
|
-
* SCD PROPS: account_tier (basic/plus/premium, monthly fuzzy, max 6), risk_category (low/medium/high/critical, household_id-scoped, monthly fixed, max 8)
|
|
42
|
-
* GROUPS: household_id (500 households)
|
|
43
|
-
*/
|
|
44
|
-
|
|
45
|
-
// ── HOOK STORIES ──
|
|
46
|
-
/*
|
|
47
|
-
* NOTE: All cohort effects are HIDDEN — no flag stamping. Discoverable
|
|
48
|
-
* via behavioral cohorts, raw-prop breakdowns (date, account_tier),
|
|
49
|
-
* or funnel time-to-convert.
|
|
50
|
-
*
|
|
51
|
-
* ---------------------------------------------------------------
|
|
52
|
-
* 1. PERSONAL VS BUSINESS ACCOUNTS (user)
|
|
53
|
-
*
|
|
54
|
-
* PATTERN: 20% of accounts are business (employee_count, revenue,
|
|
55
|
-
* industry attached) and 80% are personal (age_range, life_stage).
|
|
56
|
-
* Account segment shapes downstream transaction sizes.
|
|
57
|
-
*
|
|
58
|
-
* HOW TO FIND IT IN MIXPANEL:
|
|
59
|
-
*
|
|
60
|
-
* Report 1: Account Segment Mix
|
|
61
|
-
* - Report type: Insights
|
|
62
|
-
* - Event: any event
|
|
63
|
-
* - Measure: Unique users
|
|
64
|
-
* - Breakdown: "account_segment"
|
|
65
|
-
* - Expected: ~80% personal, ~20% business
|
|
66
|
-
*
|
|
67
|
-
* Report 2: Transaction Size by Segment
|
|
68
|
-
* - Report type: Insights
|
|
69
|
-
* - Event: "transaction completed"
|
|
70
|
-
* - Measure: Average of "amount"
|
|
71
|
-
* - Breakdown: "account_segment"
|
|
72
|
-
* - Expected: business ~ $200+, personal ~ $50 (~4x larger)
|
|
73
|
-
*
|
|
74
|
-
* REAL-WORLD ANALOGUE: Neobanks serve both consumers and small
|
|
75
|
-
* businesses with the same core product, but business activity is
|
|
76
|
-
* meaningfully higher value per transaction.
|
|
77
|
-
*
|
|
78
|
-
* ---------------------------------------------------------------
|
|
79
|
-
* 2. PAYDAY PATTERNS (everything)
|
|
80
|
-
*
|
|
81
|
-
* PATTERN: Direct deposit transactions are 3x larger on the 1st and
|
|
82
|
-
* 15th of the month. Transfers are ~1.6x larger on the 1st-3rd and
|
|
83
|
-
* 15th-17th (60% likelihood × 2x boost). No flag — discover via day-of-month
|
|
84
|
-
* breakdown on raw amount.
|
|
85
|
-
*
|
|
86
|
-
* HOW TO FIND IT IN MIXPANEL:
|
|
87
|
-
*
|
|
88
|
-
* Report 1: Direct Deposit Size by Day of Month
|
|
89
|
-
* - Report type: Insights
|
|
90
|
-
* - Event: "transaction completed"
|
|
91
|
-
* - Measure: Average of "amount"
|
|
92
|
-
* - Filter: "transaction_type" = "direct_deposit"
|
|
93
|
-
* - Breakdown: day of month
|
|
94
|
-
* - Expected: 1st and 15th avg ~ 3x other days
|
|
95
|
-
*
|
|
96
|
-
* Report 2: Post-Payday Transfer Spending by Day of Month
|
|
97
|
-
* - Report type: Insights
|
|
98
|
-
* - Event: "transfer sent"
|
|
99
|
-
* - Measure: Average of "amount"
|
|
100
|
-
* - Breakdown: day of month
|
|
101
|
-
* - Expected: 1-3 and 15-17 avg ~ 1.6x other days
|
|
102
|
-
*
|
|
103
|
-
* REAL-WORLD ANALOGUE: Bi-monthly payroll cycles drive predictable
|
|
104
|
-
* spikes in deposit and outbound spending volume.
|
|
105
|
-
*
|
|
106
|
-
* ---------------------------------------------------------------
|
|
107
|
-
* 3. FRAUD DETECTION (everything)
|
|
108
|
-
*
|
|
109
|
-
* PATTERN: ~1-2% of users experience a fraud burst at the timeline
|
|
110
|
-
* midpoint: 3-5 rapid high-value transactions, then card locked
|
|
111
|
-
* (reason="suspicious_activity"), dispute filed (reason="unauthorized"),
|
|
112
|
-
* and support contacted (issue_type="card"). No flag — derive cohort
|
|
113
|
-
* by joining users who had all three event types within ~1 hour.
|
|
114
|
-
*
|
|
115
|
-
* HOW TO FIND IT IN MIXPANEL:
|
|
116
|
-
*
|
|
117
|
-
* Report 1: Fraud Cohort
|
|
118
|
-
* - Report type: Cohort builder
|
|
119
|
-
* - Filter: did "card locked" with reason="suspicious_activity"
|
|
120
|
-
* AND "dispute filed" with reason="unauthorized"
|
|
121
|
-
* within 1 hour of each other
|
|
122
|
-
* - Expected: ~1-2% of total users
|
|
123
|
-
*
|
|
124
|
-
* Report 2: Fraud Resolution Funnel
|
|
125
|
-
* - Report type: Funnels
|
|
126
|
-
* - Steps: "card locked" -> "dispute filed" -> "support contacted"
|
|
127
|
-
* - Filter: card locked.reason = "suspicious_activity"
|
|
128
|
-
* - Expected: high completion across all three resolution steps
|
|
129
|
-
*
|
|
130
|
-
* REAL-WORLD ANALOGUE: A small but consistent slice of accounts
|
|
131
|
-
* triggers fraud pipelines every cycle, generating the bulk of
|
|
132
|
-
* dispute and support load.
|
|
133
|
-
*
|
|
134
|
-
* ---------------------------------------------------------------
|
|
135
|
-
* 4. LOW BALANCE CHURN (everything)
|
|
136
|
-
*
|
|
137
|
-
* PATTERN: Users with 3+ "balance checked" events where account_balance
|
|
138
|
-
* < $15K lose 50% of their events after day 30. No flag — derive cohort
|
|
139
|
-
* by counting low-balance checks per user.
|
|
140
|
-
*
|
|
141
|
-
* HOW TO FIND IT IN MIXPANEL:
|
|
142
|
-
*
|
|
143
|
-
* Report 1: Activity by Low Balance Cohort
|
|
144
|
-
* - Cohort A: users with >= 3 "balance checked" where account_balance < 15000
|
|
145
|
-
* - Cohort B: users with < 3
|
|
146
|
-
* - Event: any event
|
|
147
|
-
* - Measure: Total per user, line chart by day
|
|
148
|
-
* - Expected: A growth post-d30 ~ 0.5x B's growth (suppressed)
|
|
149
|
-
*
|
|
150
|
-
* Report 2: Activity Decline Timeline
|
|
151
|
-
* - Report type: Insights (with cohort A above)
|
|
152
|
-
* - Event: any event
|
|
153
|
-
* - Measure: Total
|
|
154
|
-
* - Line chart by day
|
|
155
|
-
* - Expected: pre-d30/post-d30 ratio ~ 1.0x for A vs ~ 2.0x for B
|
|
156
|
-
*
|
|
157
|
-
* REAL-WORLD ANALOGUE: Customers running thin balances lose trust
|
|
158
|
-
* in the platform and migrate their primary banking elsewhere.
|
|
159
|
-
*
|
|
160
|
-
* ---------------------------------------------------------------
|
|
161
|
-
* 5. BUDGET USERS SAVE MORE (everything)
|
|
162
|
-
*
|
|
163
|
-
* PATTERN: Users with any "budget created" event get 2x savings
|
|
164
|
-
* contributions, 1.5x investment amounts, and extra cloned savings
|
|
165
|
-
* goal events. No flag — derive cohort behaviorally.
|
|
166
|
-
*
|
|
167
|
-
* HOW TO FIND IT IN MIXPANEL:
|
|
168
|
-
*
|
|
169
|
-
* Report 1: Savings Contribution by Budget Cohort
|
|
170
|
-
* - Cohort A: users with >= 1 "budget created" event
|
|
171
|
-
* - Cohort B: users with 0
|
|
172
|
-
* - Event: "savings goal set"
|
|
173
|
-
* - Measure: Average of "monthly_contribution"
|
|
174
|
-
* - Expected: A ~ 2x B
|
|
175
|
-
*
|
|
176
|
-
* Report 2: Investment Size by Budget Cohort
|
|
177
|
-
* - Cohort A vs B (as above)
|
|
178
|
-
* - Event: "investment made"
|
|
179
|
-
* - Measure: Average of "amount"
|
|
180
|
-
* - Expected: A ~ 1.5x B
|
|
181
|
-
*
|
|
182
|
-
* REAL-WORLD ANALOGUE: Active budget tooling correlates strongly
|
|
183
|
-
* with healthier savings rates and broader product adoption.
|
|
184
|
-
*
|
|
185
|
-
* ---------------------------------------------------------------
|
|
186
|
-
* 6. AUTO-PAY LOYALTY (event)
|
|
187
|
-
*
|
|
188
|
-
* PATTERN: Manual payers (auto_pay=false) miss 30% of their bill
|
|
189
|
-
* payments — those events are renamed to "bill payment missed".
|
|
190
|
-
* Auto-pay users never miss.
|
|
191
|
-
*
|
|
192
|
-
* HOW TO FIND IT IN MIXPANEL:
|
|
193
|
-
*
|
|
194
|
-
* Report 1: Bill Outcomes by Manual vs Auto-Pay
|
|
195
|
-
* - Report type: Insights
|
|
196
|
-
* - Events: "bill paid" and "bill payment missed"
|
|
197
|
-
* - Measure: Total
|
|
198
|
-
* - Expected: missed events appear only for manual payers, ~30% rate
|
|
199
|
-
*
|
|
200
|
-
* Report 2: Bill Completion Rate by Auto-Pay
|
|
201
|
-
* - Report type: Insights
|
|
202
|
-
* - Event: "bill paid"
|
|
203
|
-
* - Measure: Total
|
|
204
|
-
* - Breakdown: "auto_pay"
|
|
205
|
-
* - Expected: auto_pay=false ~ 70% completion vs auto_pay=true ~ 100%
|
|
206
|
-
*
|
|
207
|
-
* REAL-WORLD ANALOGUE: Auto-pay locks users into a frictionless
|
|
208
|
-
* payment cadence that virtually eliminates missed bills.
|
|
209
|
-
*
|
|
210
|
-
* ---------------------------------------------------------------
|
|
211
|
-
* 7. PREMIUM TIER VALUE (everything)
|
|
212
|
-
*
|
|
213
|
-
* PATTERN: Premium-tier users get 3x reward values and 2x sell
|
|
214
|
-
* returns on investments. Plus tier gets 1.5x rewards. No flag —
|
|
215
|
-
* mutates raw "value"/"amount" properties; discover via account_tier breakdown.
|
|
216
|
-
*
|
|
217
|
-
* HOW TO FIND IT IN MIXPANEL:
|
|
218
|
-
*
|
|
219
|
-
* Report 1: Reward Value by Tier
|
|
220
|
-
* - Report type: Insights
|
|
221
|
-
* - Event: "reward redeemed"
|
|
222
|
-
* - Measure: Average of "value"
|
|
223
|
-
* - Breakdown: "account_tier"
|
|
224
|
-
* - Expected: Premium ~ $30, Plus ~ $15, Basic ~ $10
|
|
225
|
-
*
|
|
226
|
-
* Report 2: Investment Sell Amount by Tier
|
|
227
|
-
* - Report type: Insights
|
|
228
|
-
* - Event: "investment made"
|
|
229
|
-
* - Measure: Average of "amount"
|
|
230
|
-
* - Filter: "action" = "sell"
|
|
231
|
-
* - Breakdown: "account_tier"
|
|
232
|
-
* - Expected: Premium ~ 2x baseline; Basic/Plus baseline
|
|
233
|
-
*
|
|
234
|
-
* REAL-WORLD ANALOGUE: Premium subscription tiers justify their
|
|
235
|
-
* price by delivering visibly better cashback and investment perks.
|
|
236
|
-
*
|
|
237
|
-
* ---------------------------------------------------------------
|
|
238
|
-
* 8. MONTH-END ANXIETY (everything)
|
|
239
|
-
*
|
|
240
|
-
* PATTERN: On days >= 28 of the calendar month, app sessions run
|
|
241
|
-
* 40% longer and reported balances are 30% lower. No flag — discover
|
|
242
|
-
* via day-of-month breakdown.
|
|
243
|
-
*
|
|
244
|
-
* HOW TO FIND IT IN MIXPANEL:
|
|
245
|
-
*
|
|
246
|
-
* Report 1: Session Duration by Day of Month
|
|
247
|
-
* - Report type: Insights
|
|
248
|
-
* - Event: "app session"
|
|
249
|
-
* - Measure: Average of "session_duration_sec"
|
|
250
|
-
* - Breakdown: day of month
|
|
251
|
-
* - Expected: days >= 28 ~ 1.4x other days
|
|
252
|
-
*
|
|
253
|
-
* Report 2: Balance by Day of Month
|
|
254
|
-
* - Report type: Insights
|
|
255
|
-
* - Event: "balance checked"
|
|
256
|
-
* - Measure: Average of "account_balance"
|
|
257
|
-
* - Breakdown: day of month
|
|
258
|
-
* - Expected: days >= 28 ~ 0.7x other days
|
|
259
|
-
*
|
|
260
|
-
* REAL-WORLD ANALOGUE: Users obsessively check balances at month
|
|
261
|
-
* end as bills hit and runway tightens.
|
|
262
|
-
*
|
|
263
|
-
* ---------------------------------------------------------------
|
|
264
|
-
* 9. ONBOARDING TIME-TO-CONVERT (everything)
|
|
265
|
-
*
|
|
266
|
-
* PATTERN: Premium tier users complete the Onboarding funnel 1.5x
|
|
267
|
-
* faster (factor 0.67); Basic users 1.33x slower (factor 1.33).
|
|
268
|
-
* Applied in the everything hook via findFirstSequence + scaleFunnelTTC,
|
|
269
|
-
* so the effect is visible in both Mixpanel funnels and cross-event
|
|
270
|
-
* MIN→MIN SQL queries.
|
|
271
|
-
*
|
|
272
|
-
* HOW TO FIND IT IN MIXPANEL:
|
|
273
|
-
*
|
|
274
|
-
* Report 1: Onboarding Median Time-to-Convert by Tier
|
|
275
|
-
* - Funnels > "account opened" -> "app session" -> "balance checked"
|
|
276
|
-
* - Measure: Median time to convert
|
|
277
|
-
* - Breakdown: account_tier
|
|
278
|
-
* - Expected: premium ~ 0.67x baseline; basic ~ 1.33x
|
|
279
|
-
*
|
|
280
|
-
* ---------------------------------------------------------------
|
|
281
|
-
* 10. TRANSACTION-COUNT MAGIC NUMBER (everything)
|
|
282
|
-
*
|
|
283
|
-
* PATTERN: Sweet 6-10 transactions/user → +40% on investment-made
|
|
284
|
-
* amount (engaged transactor compounds wealth). Over 11+ → drop 20%
|
|
285
|
-
* of premium-upgraded events. No flag.
|
|
286
|
-
*
|
|
287
|
-
* HOW TO FIND IT IN MIXPANEL:
|
|
288
|
-
*
|
|
289
|
-
* Report 1: Avg Investment Amount by Transaction Bucket
|
|
290
|
-
* - Cohort A: users with 6-10 "transaction completed"
|
|
291
|
-
* - Cohort B: users with 0-5
|
|
292
|
-
* - Event: "investment made"
|
|
293
|
-
* - Measure: Average of "amount"
|
|
294
|
-
* - Expected: A ~ 1.4x B
|
|
295
|
-
*
|
|
296
|
-
* Report 2: Premium Upgrades on Heavy Transactors
|
|
297
|
-
* - Cohort C: users with >= 11 "transaction completed"
|
|
298
|
-
* - Cohort A: users with 6-10
|
|
299
|
-
* - Event: "premium upgraded"
|
|
300
|
-
* - Measure: Total per user
|
|
301
|
-
* - Expected: C ~ 20% fewer upgrades per user
|
|
302
|
-
*
|
|
303
|
-
* REAL-WORLD ANALOGUE: Engaged transactors invest more; over-active
|
|
304
|
-
* already extract value without upgrading.
|
|
305
|
-
*
|
|
306
|
-
* ===================================================================
|
|
307
|
-
* EXPECTED METRICS SUMMARY
|
|
308
|
-
* ===================================================================
|
|
309
|
-
*
|
|
310
|
-
* Hook | Metric | Baseline | Effect | Ratio
|
|
311
|
-
* ----------------------|-----------------------|----------|-----------|------
|
|
312
|
-
* Personal vs Business | Avg transaction amt | $50 | $200+ | ~ 4x
|
|
313
|
-
* Payday Patterns | Deposit amt 1st/15th | 1x | 3x | 3x
|
|
314
|
-
* Fraud Detection | Users affected | 0% | 3% | --
|
|
315
|
-
* Low Balance Churn | D30+ events | 1x | 0.5x | -50%
|
|
316
|
-
* Budget Discipline | Savings contribution | 1x | 2x | 2x
|
|
317
|
-
* Auto-Pay Loyalty | Bill completion rate | 100% | 70% | -30%
|
|
318
|
-
* Premium Tier Value | Reward value (Premium)| 1x | 3x | 3x
|
|
319
|
-
* Month-End Anxiety | Session duration d28+ | 1x | 1.4x | 1.4x
|
|
320
|
-
* Onboarding T2C (H9) | median min by tier | 1x | 0.67/1.33x| 2x range
|
|
321
|
-
* Txn-Count Magic Num | sweet investment amt | 1x | 1.4x | 1.4x
|
|
322
|
-
* Txn-Count Magic Num | over premium upgrades | 1x | 0.8x | -20%
|
|
323
|
-
*/
|
|
324
|
-
|
|
325
|
-
// ── SCALE ──
|
|
326
|
-
const SEED = "harness-fintech";
|
|
327
|
-
const NUM_USERS = 10_000;
|
|
328
|
-
const DATASET_START = "2026-01-01T00:00:00Z";
|
|
329
|
-
const DATASET_END = "2026-05-01T23:59:59Z";
|
|
330
|
-
const EVENTS_PER_DAY = 1.2;
|
|
331
|
-
const token = process.env.MP_TOKEN || "your-mixpanel-token";
|
|
332
|
-
|
|
333
|
-
const chance = u.initChance(SEED);
|
|
334
|
-
|
|
335
|
-
// ── KNOBS (tweak these to reshape stories) ──
|
|
336
|
-
// H1: Personal vs Business
|
|
337
|
-
const BUSINESS_LIKELIHOOD = 20;
|
|
338
|
-
const BUSINESS_TXN_MULT = 4;
|
|
339
|
-
|
|
340
|
-
// H2: Payday Patterns
|
|
341
|
-
const PAYDAY_DEPOSIT_MULT = 3;
|
|
342
|
-
const PAYDAY_TRANSFER_MULT = 2.0;
|
|
343
|
-
const PAYDAY_TRANSFER_LIKELIHOOD = 60;
|
|
344
|
-
|
|
345
|
-
// H3: Fraud Detection
|
|
346
|
-
const FRAUD_LIKELIHOOD = 15;
|
|
347
|
-
const FRAUD_BURST_MIN = 3;
|
|
348
|
-
const FRAUD_BURST_MAX = 5;
|
|
349
|
-
const FRAUD_AMOUNT_MIN = 500;
|
|
350
|
-
const FRAUD_AMOUNT_MAX = 3000;
|
|
351
|
-
|
|
352
|
-
// H4: Low Balance Churn
|
|
353
|
-
const LOW_BALANCE_THRESHOLD = 15000;
|
|
354
|
-
const LOW_BALANCE_CHECK_THRESHOLD = 3;
|
|
355
|
-
const LOW_BALANCE_CHURN_CUTOFF_DAYS = 30;
|
|
356
|
-
const LOW_BALANCE_DROP_LIKELIHOOD = 50;
|
|
357
|
-
|
|
358
|
-
// H5: Budget Discipline
|
|
359
|
-
const BUDGET_SAVINGS_MULT = 2;
|
|
360
|
-
const BUDGET_INVESTMENT_MULT = 1.5;
|
|
361
|
-
const BUDGET_CLONE_LIKELIHOOD = 50;
|
|
362
|
-
|
|
363
|
-
// H6: Auto-Pay Loyalty
|
|
364
|
-
const MISSED_BILL_LIKELIHOOD = 30;
|
|
365
|
-
|
|
366
|
-
// H7: Premium Tier Value
|
|
367
|
-
const PREMIUM_REWARD_MULT = 3;
|
|
368
|
-
const PLUS_REWARD_MULT = 1.5;
|
|
369
|
-
const PREMIUM_INVEST_SELL_MULT = 2;
|
|
370
|
-
|
|
371
|
-
// H8: Month-End Anxiety
|
|
372
|
-
const MONTH_END_DAY_THRESHOLD = 28;
|
|
373
|
-
const MONTH_END_SESSION_MULT = 1.4;
|
|
374
|
-
const MONTH_END_BALANCE_MULT = 0.7;
|
|
375
|
-
|
|
376
|
-
// H9: Onboarding TTC
|
|
377
|
-
const TTC_PREMIUM_FACTOR = 0.67;
|
|
378
|
-
const TTC_BASIC_FACTOR = 1.33;
|
|
379
|
-
const TTC_MAX_GAP_MINUTES = 60 * 24 * 30; // 30-day max gap between steps
|
|
380
|
-
|
|
381
|
-
// H10: Transaction-Count Magic Number
|
|
382
|
-
const TXN_SWEET_MIN = 6;
|
|
383
|
-
const TXN_SWEET_MAX = 10;
|
|
384
|
-
const TXN_OVER_THRESHOLD = 11;
|
|
385
|
-
const TXN_INVESTMENT_BOOST = 1.4;
|
|
386
|
-
const TXN_PREMIUM_DROP_LIKELIHOOD = 20;
|
|
387
|
-
|
|
388
|
-
// ── HELPER FUNCTIONS ──
|
|
389
|
-
function handleUserHooks(record) {
|
|
390
|
-
// H1: PERSONAL VS BUSINESS ACCOUNTS — role-based attrs.
|
|
391
|
-
const isBusiness = chance.bool({ likelihood: BUSINESS_LIKELIHOOD });
|
|
392
|
-
if (isBusiness) {
|
|
393
|
-
record.account_segment = "business";
|
|
394
|
-
record.employee_count = chance.integer({ min: 5, max: 500 });
|
|
395
|
-
record.annual_revenue = chance.integer({ min: 100000, max: 10000000 });
|
|
396
|
-
record.industry = chance.pickone(["tech", "retail", "food", "services", "healthcare"]);
|
|
397
|
-
} else {
|
|
398
|
-
record.account_segment = "personal";
|
|
399
|
-
record.age_range = `${chance.pickone([18, 25, 35, 45, 55])}-${chance.pickone([24, 34, 44, 54, 65])}`;
|
|
400
|
-
record.life_stage = chance.pickone(["student", "early_career", "established", "pre_retirement", "retired"]);
|
|
401
|
-
}
|
|
402
|
-
return record;
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
function handleEventHooks(record) {
|
|
406
|
-
// H6: AUTO-PAY LOYALTY — manual bill-paid events have 30% chance of
|
|
407
|
-
// becoming "bill payment missed". Mutates event name.
|
|
408
|
-
if (record.event === "bill paid" && record.auto_pay !== true && chance.bool({ likelihood: MISSED_BILL_LIKELIHOOD })) {
|
|
409
|
-
record.event = "bill payment missed";
|
|
410
|
-
}
|
|
411
|
-
return record;
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
function handleEverythingHooks(record, meta) {
|
|
415
|
-
const datasetStart = dayjs.unix(meta.datasetStart);
|
|
416
|
-
const userEvents = record;
|
|
417
|
-
const profile = meta.profile;
|
|
418
|
-
|
|
419
|
-
userEvents.forEach(e => {
|
|
420
|
-
e.account_tier = profile.account_tier;
|
|
421
|
-
e.Platform = profile.Platform;
|
|
422
|
-
});
|
|
423
|
-
|
|
424
|
-
// H9: ONBOARDING TIME-TO-CONVERT — Premium 1.5x faster (factor 0.67);
|
|
425
|
-
// Basic 1.33x slower (factor 1.33). Finds first onboarding sequence
|
|
426
|
-
// and scales the inter-step gaps.
|
|
427
|
-
{
|
|
428
|
-
const ttcFactor = (
|
|
429
|
-
profile.account_tier === "premium" ? TTC_PREMIUM_FACTOR :
|
|
430
|
-
profile.account_tier === "basic" ? TTC_BASIC_FACTOR :
|
|
431
|
-
1.0
|
|
432
|
-
);
|
|
433
|
-
if (ttcFactor !== 1.0) {
|
|
434
|
-
const onboardingSeq = findFirstSequence(
|
|
435
|
-
userEvents,
|
|
436
|
-
["account opened", "app session", "balance checked"],
|
|
437
|
-
TTC_MAX_GAP_MINUTES
|
|
438
|
-
);
|
|
439
|
-
if (onboardingSeq) {
|
|
440
|
-
scaleFunnelTTC(onboardingSeq, ttcFactor);
|
|
441
|
-
}
|
|
442
|
-
}
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
// H1B: PERSONAL VS BUSINESS — business segment txns 4x larger
|
|
446
|
-
// (per Report 2 in JSDoc: business ~ $200, personal ~ $50).
|
|
447
|
-
if (profile.account_segment === "business") {
|
|
448
|
-
userEvents.forEach(e => {
|
|
449
|
-
if (e.event === "transaction completed" && typeof e.amount === "number") {
|
|
450
|
-
e.amount = Math.floor(e.amount * BUSINESS_TXN_MULT);
|
|
451
|
-
}
|
|
452
|
-
});
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
// H2: PAYDAY PATTERNS — 1st & 15th: direct_deposit amount 3x.
|
|
456
|
-
// Days 1-3 and 15-17: 60% of transfers get amount 2x. No flag.
|
|
457
|
-
for (const e of userEvents) {
|
|
458
|
-
const dayOfMonth = new Date(e.time).getUTCDate();
|
|
459
|
-
if (e.event === "transaction completed" && e.transaction_type === "direct_deposit") {
|
|
460
|
-
if (dayOfMonth === 1 || dayOfMonth === 15) {
|
|
461
|
-
e.amount = Math.floor((e.amount || 50) * PAYDAY_DEPOSIT_MULT);
|
|
462
|
-
}
|
|
463
|
-
}
|
|
464
|
-
if (e.event === "transfer sent") {
|
|
465
|
-
const isPaydayWindow = (dayOfMonth >= 1 && dayOfMonth <= 3) || (dayOfMonth >= 15 && dayOfMonth <= 17);
|
|
466
|
-
if (isPaydayWindow && chance.bool({ likelihood: PAYDAY_TRANSFER_LIKELIHOOD })) {
|
|
467
|
-
e.amount = Math.floor((e.amount || 200) * PAYDAY_TRANSFER_MULT);
|
|
468
|
-
}
|
|
469
|
-
}
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
// H8: MONTH-END ANXIETY — days >= 28: app_session duration 1.4x;
|
|
473
|
-
// balance_checked account_balance 0.7x. Mutates raw props.
|
|
474
|
-
for (const e of userEvents) {
|
|
475
|
-
const dayOfMonth = new Date(e.time).getUTCDate();
|
|
476
|
-
if (dayOfMonth >= MONTH_END_DAY_THRESHOLD) {
|
|
477
|
-
if (e.event === "app session") {
|
|
478
|
-
e.session_duration_sec = Math.floor((e.session_duration_sec || 60) * MONTH_END_SESSION_MULT);
|
|
479
|
-
}
|
|
480
|
-
if (e.event === "balance checked") {
|
|
481
|
-
e.account_balance = Math.floor((e.account_balance || 2500) * MONTH_END_BALANCE_MULT);
|
|
482
|
-
}
|
|
483
|
-
}
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
// H7: PREMIUM TIER VALUE — Premium 3x reward value + 2x investment-sell
|
|
487
|
-
// amount; Plus 1.5x reward value. Reads tier from profile. No flag.
|
|
488
|
-
const tier = profile.account_tier;
|
|
489
|
-
userEvents.forEach(e => {
|
|
490
|
-
if (e.event === "reward redeemed") {
|
|
491
|
-
if (tier === "premium") e.value = Math.floor((e.value || 10) * PREMIUM_REWARD_MULT);
|
|
492
|
-
else if (tier === "plus") e.value = Math.floor((e.value || 10) * PLUS_REWARD_MULT);
|
|
493
|
-
}
|
|
494
|
-
if (e.event === "investment made" && e.action === "sell" && tier === "premium") {
|
|
495
|
-
e.amount = Math.floor((e.amount || 250) * PREMIUM_INVEST_SELL_MULT);
|
|
496
|
-
}
|
|
497
|
-
});
|
|
498
|
-
|
|
499
|
-
// H3: FRAUD DETECTION — 15% of users get fraud burst (3-5 rapid
|
|
500
|
-
// high-value transactions + card locked + dispute + support contacted)
|
|
501
|
-
// at timeline midpoint. No flag — discover via cohort builder on users
|
|
502
|
-
// with card-locked + dispute-filed.
|
|
503
|
-
if (chance.bool({ likelihood: FRAUD_LIKELIHOOD }) && userEvents.length >= 2) {
|
|
504
|
-
const midIdx = Math.floor(userEvents.length / 2);
|
|
505
|
-
const midEvent = userEvents[midIdx];
|
|
506
|
-
const midTime = dayjs(midEvent.time);
|
|
507
|
-
const distinctId = midEvent.user_id;
|
|
508
|
-
const burstCount = chance.integer({ min: FRAUD_BURST_MIN, max: FRAUD_BURST_MAX });
|
|
509
|
-
const fraudEvents = [];
|
|
510
|
-
const txnTemplate = userEvents.find(e => e.event === "transaction completed");
|
|
511
|
-
const cardTemplate = userEvents.find(e => e.event === "card locked");
|
|
512
|
-
const disputeTemplate = userEvents.find(e => e.event === "dispute filed");
|
|
513
|
-
const supportTemplate = userEvents.find(e => e.event === "support contacted");
|
|
514
|
-
|
|
515
|
-
for (let i = 0; i < burstCount; i++) {
|
|
516
|
-
if (txnTemplate) {
|
|
517
|
-
fraudEvents.push({
|
|
518
|
-
...txnTemplate,
|
|
519
|
-
time: midTime.add(i * 10, "minutes").toISOString(),
|
|
520
|
-
user_id: distinctId,
|
|
521
|
-
transaction_type: "purchase",
|
|
522
|
-
amount: chance.integer({ min: FRAUD_AMOUNT_MIN, max: FRAUD_AMOUNT_MAX }),
|
|
523
|
-
merchant_category: chance.pickone(["online", "retail"]),
|
|
524
|
-
payment_method: "credit",
|
|
525
|
-
});
|
|
526
|
-
}
|
|
527
|
-
}
|
|
528
|
-
if (cardTemplate) fraudEvents.push({
|
|
529
|
-
...cardTemplate,
|
|
530
|
-
time: midTime.add(burstCount * 10 + 5, "minutes").toISOString(),
|
|
531
|
-
user_id: distinctId,
|
|
532
|
-
reason: "suspicious_activity",
|
|
533
|
-
});
|
|
534
|
-
if (disputeTemplate) fraudEvents.push({
|
|
535
|
-
...disputeTemplate,
|
|
536
|
-
time: midTime.add(burstCount * 10 + 30, "minutes").toISOString(),
|
|
537
|
-
user_id: distinctId,
|
|
538
|
-
dispute_amount: chance.integer({ min: FRAUD_AMOUNT_MIN, max: FRAUD_AMOUNT_MAX }),
|
|
539
|
-
reason: "unauthorized",
|
|
540
|
-
});
|
|
541
|
-
if (supportTemplate) fraudEvents.push({
|
|
542
|
-
...supportTemplate,
|
|
543
|
-
time: midTime.add(burstCount * 10 + 45, "minutes").toISOString(),
|
|
544
|
-
user_id: distinctId,
|
|
545
|
-
channel: "phone",
|
|
546
|
-
issue_type: "card",
|
|
547
|
-
resolved: true,
|
|
548
|
-
});
|
|
549
|
-
userEvents.splice(midIdx + 1, 0, ...fraudEvents);
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
// H4: LOW BALANCE CHURN — users with 3+ balance checks under $15K lose
|
|
553
|
-
// 50% of post-day-30 events. No flag.
|
|
554
|
-
const lowBalanceChecks = userEvents.filter(e =>
|
|
555
|
-
e.event === "balance checked" && (e.account_balance || 0) < LOW_BALANCE_THRESHOLD
|
|
556
|
-
).length;
|
|
557
|
-
if (lowBalanceChecks >= LOW_BALANCE_CHECK_THRESHOLD) {
|
|
558
|
-
const dayCutoff = datasetStart.add(LOW_BALANCE_CHURN_CUTOFF_DAYS, "days");
|
|
559
|
-
for (let i = userEvents.length - 1; i >= 0; i--) {
|
|
560
|
-
if (dayjs(userEvents[i].time).isAfter(dayCutoff) && chance.bool({ likelihood: LOW_BALANCE_DROP_LIKELIHOOD })) {
|
|
561
|
-
userEvents.splice(i, 1);
|
|
562
|
-
}
|
|
563
|
-
}
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
// H5: BUDGET DISCIPLINE — users with any budget-created event get
|
|
567
|
-
// savings 2x, investment amounts 1.5x, and extra cloned savings-goal
|
|
568
|
-
// events. No flag.
|
|
569
|
-
const hasBudget = userEvents.some(e => e.event === "budget created");
|
|
570
|
-
if (hasBudget) {
|
|
571
|
-
userEvents.forEach((event, idx) => {
|
|
572
|
-
const eventTime = dayjs(event.time);
|
|
573
|
-
if (event.event === "savings goal set") {
|
|
574
|
-
event.monthly_contribution = Math.floor((event.monthly_contribution || 200) * BUDGET_SAVINGS_MULT);
|
|
575
|
-
}
|
|
576
|
-
if (event.event === "investment made") {
|
|
577
|
-
event.amount = Math.floor((event.amount || 250) * BUDGET_INVESTMENT_MULT);
|
|
578
|
-
}
|
|
579
|
-
if (event.event === "budget created" && chance.bool({ likelihood: BUDGET_CLONE_LIKELIHOOD })) {
|
|
580
|
-
const savingsTemplate = userEvents.find(e => e.event === "savings goal set");
|
|
581
|
-
if (savingsTemplate) {
|
|
582
|
-
userEvents.splice(idx + 1, 0, {
|
|
583
|
-
...savingsTemplate,
|
|
584
|
-
time: eventTime.add(chance.integer({ min: 1, max: 7 }), "days").toISOString(),
|
|
585
|
-
user_id: event.user_id,
|
|
586
|
-
goal_type: chance.pickone(["emergency", "vacation", "car", "home"]),
|
|
587
|
-
target_amount: chance.integer({ min: 1000, max: 20000 }),
|
|
588
|
-
monthly_contribution: chance.integer({ min: 100, max: 800 }),
|
|
589
|
-
});
|
|
590
|
-
}
|
|
591
|
-
}
|
|
592
|
-
});
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
// H10: TRANSACTION-COUNT MAGIC NUMBER (no flags)
|
|
596
|
-
// Sweet 6-10 transactions/user → +40% on investment_made amount.
|
|
597
|
-
// Over 11+ → drop 20% of premium-upgraded events.
|
|
598
|
-
const txnCount = userEvents.filter(e => e.event === "transaction completed").length;
|
|
599
|
-
if (txnCount >= TXN_SWEET_MIN && txnCount <= TXN_SWEET_MAX) {
|
|
600
|
-
userEvents.forEach(e => {
|
|
601
|
-
if (e.event === "investment made" && typeof e.amount === "number") {
|
|
602
|
-
e.amount = Math.round(e.amount * TXN_INVESTMENT_BOOST);
|
|
603
|
-
}
|
|
604
|
-
});
|
|
605
|
-
} else if (txnCount >= TXN_OVER_THRESHOLD) {
|
|
606
|
-
for (let i = userEvents.length - 1; i >= 0; i--) {
|
|
607
|
-
if (userEvents[i].event === "premium upgraded" && chance.bool({ likelihood: TXN_PREMIUM_DROP_LIKELIHOOD })) {
|
|
608
|
-
userEvents.splice(i, 1);
|
|
609
|
-
}
|
|
610
|
-
}
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
return record;
|
|
614
|
-
}
|
|
615
|
-
|
|
616
|
-
// ── CONFIG ──
|
|
617
|
-
/** @type {Config} */
|
|
618
|
-
const config = {
|
|
619
|
-
version: 2,
|
|
620
|
-
seed: SEED,
|
|
621
|
-
datasetStart: DATASET_START,
|
|
622
|
-
datasetEnd: DATASET_END,
|
|
623
|
-
avgEventsPerUserPerDay: EVENTS_PER_DAY,
|
|
624
|
-
numUsers: NUM_USERS,
|
|
625
|
-
format: "json",
|
|
626
|
-
gzip: true,
|
|
627
|
-
credentials: {
|
|
628
|
-
token,
|
|
629
|
-
},
|
|
630
|
-
switches: {
|
|
631
|
-
hasSessionIds: true,
|
|
632
|
-
alsoInferFunnels: false,
|
|
633
|
-
hasLocation: true,
|
|
634
|
-
hasAndroidDevices: true,
|
|
635
|
-
hasIOSDevices: true,
|
|
636
|
-
hasDesktopDevices: true,
|
|
637
|
-
hasBrowser: false,
|
|
638
|
-
hasCampaigns: false,
|
|
639
|
-
isAnonymous: false,
|
|
640
|
-
hasAdSpend: false,
|
|
641
|
-
hasAvatar: true,
|
|
642
|
-
},
|
|
643
|
-
identity: {
|
|
644
|
-
avgDevicePerUser: 2,
|
|
645
|
-
},
|
|
646
|
-
concurrency: 1,
|
|
647
|
-
writeToDisk: false,
|
|
648
|
-
|
|
649
|
-
scdProps: {
|
|
650
|
-
account_tier: {
|
|
651
|
-
values: ["basic", "plus", "premium"],
|
|
652
|
-
frequency: "month",
|
|
653
|
-
timing: "fuzzy",
|
|
654
|
-
max: 6
|
|
655
|
-
},
|
|
656
|
-
risk_category: {
|
|
657
|
-
values: ["low", "medium", "high", "critical"],
|
|
658
|
-
frequency: "month",
|
|
659
|
-
timing: "fixed",
|
|
660
|
-
max: 8,
|
|
661
|
-
type: "household_id"
|
|
662
|
-
}
|
|
663
|
-
},
|
|
664
|
-
|
|
665
|
-
funnels: [
|
|
666
|
-
{
|
|
667
|
-
sequence: ["account opened", "app session", "balance checked"],
|
|
668
|
-
isFirstFunnel: true,
|
|
669
|
-
conversionRate: 85,
|
|
670
|
-
timeToConvert: 0.25,
|
|
671
|
-
},
|
|
672
|
-
{
|
|
673
|
-
// Daily banking: check balance, view transactions - most common activity
|
|
674
|
-
sequence: ["app session", "balance checked", "transaction completed"],
|
|
675
|
-
conversionRate: 80,
|
|
676
|
-
timeToConvert: 0.5,
|
|
677
|
-
weight: 5,
|
|
678
|
-
},
|
|
679
|
-
{
|
|
680
|
-
// Transfers and notifications
|
|
681
|
-
sequence: ["app session", "transfer sent", "notification opened"],
|
|
682
|
-
conversionRate: 50,
|
|
683
|
-
timeToConvert: 1,
|
|
684
|
-
weight: 3,
|
|
685
|
-
},
|
|
686
|
-
{
|
|
687
|
-
// Bill payment flow
|
|
688
|
-
sequence: ["app session", "bill paid", "notification opened"],
|
|
689
|
-
conversionRate: 60,
|
|
690
|
-
timeToConvert: 1,
|
|
691
|
-
weight: 3,
|
|
692
|
-
},
|
|
693
|
-
{
|
|
694
|
-
// Financial planning: budgets and savings
|
|
695
|
-
sequence: ["budget created", "budget alert", "savings goal set"],
|
|
696
|
-
conversionRate: 40,
|
|
697
|
-
timeToConvert: 12,
|
|
698
|
-
weight: 2,
|
|
699
|
-
},
|
|
700
|
-
{
|
|
701
|
-
// Investment and rewards
|
|
702
|
-
sequence: ["balance checked", "investment made", "reward redeemed"],
|
|
703
|
-
conversionRate: 30,
|
|
704
|
-
timeToConvert: 5,
|
|
705
|
-
weight: 2,
|
|
706
|
-
},
|
|
707
|
-
{
|
|
708
|
-
// Support and account management
|
|
709
|
-
sequence: ["support contacted", "card locked", "dispute filed"],
|
|
710
|
-
conversionRate: 35,
|
|
711
|
-
timeToConvert: 2,
|
|
712
|
-
weight: 1,
|
|
713
|
-
},
|
|
714
|
-
{
|
|
715
|
-
// Lending flow
|
|
716
|
-
sequence: ["loan applied", "loan approved", "premium upgraded"],
|
|
717
|
-
conversionRate: 25,
|
|
718
|
-
timeToConvert: 10,
|
|
719
|
-
weight: 1,
|
|
720
|
-
},
|
|
721
|
-
],
|
|
722
|
-
|
|
723
|
-
events: [
|
|
724
|
-
{
|
|
725
|
-
event: "account opened",
|
|
726
|
-
weight: 1,
|
|
727
|
-
isFirstEvent: true,
|
|
728
|
-
isAuthEvent: true,
|
|
729
|
-
properties: {
|
|
730
|
-
"account_type": ["personal", "business", "personal"],
|
|
731
|
-
"signup_channel": ["app", "web", "referral", "branch"],
|
|
732
|
-
}
|
|
733
|
-
},
|
|
734
|
-
{
|
|
735
|
-
event: "app session",
|
|
736
|
-
weight: 20,
|
|
737
|
-
isStrictEvent: false,
|
|
738
|
-
properties: {
|
|
739
|
-
"session_duration_sec": u.weighNumRange(10, 600, 0.3, 60),
|
|
740
|
-
"pages_viewed": u.weighNumRange(1, 15, 0.5, 3),
|
|
741
|
-
}
|
|
742
|
-
},
|
|
743
|
-
{
|
|
744
|
-
event: "balance checked",
|
|
745
|
-
weight: 15,
|
|
746
|
-
isStrictEvent: false,
|
|
747
|
-
properties: {
|
|
748
|
-
"account_balance": u.weighNumRange(0, 50000, 0.8, 2500),
|
|
749
|
-
"account_type": ["checking", "savings", "investment"],
|
|
750
|
-
}
|
|
751
|
-
},
|
|
752
|
-
{
|
|
753
|
-
event: "transaction completed",
|
|
754
|
-
weight: 18,
|
|
755
|
-
isStrictEvent: false,
|
|
756
|
-
properties: {
|
|
757
|
-
"transaction_type": ["purchase", "atm", "direct_deposit", "refund"],
|
|
758
|
-
"amount": u.weighNumRange(1, 5000, 0.3, 50),
|
|
759
|
-
"merchant_category": ["grocery", "restaurant", "gas", "retail", "online", "subscription", "utilities"],
|
|
760
|
-
"payment_method": ["debit", "credit", "contactless", "online"],
|
|
761
|
-
}
|
|
762
|
-
},
|
|
763
|
-
{
|
|
764
|
-
event: "transfer sent",
|
|
765
|
-
weight: 8,
|
|
766
|
-
isStrictEvent: false,
|
|
767
|
-
properties: {
|
|
768
|
-
"transfer_type": ["internal", "external", "p2p", "wire"],
|
|
769
|
-
"amount": u.weighNumRange(10, 10000, 0.3, 200),
|
|
770
|
-
"recipient_type": ["friend", "family", "business", "self"],
|
|
771
|
-
}
|
|
772
|
-
},
|
|
773
|
-
{
|
|
774
|
-
event: "bill paid",
|
|
775
|
-
weight: 6,
|
|
776
|
-
isStrictEvent: false,
|
|
777
|
-
properties: {
|
|
778
|
-
"bill_type": ["rent", "utilities", "phone", "insurance", "subscription", "loan_payment"],
|
|
779
|
-
"amount": u.weighNumRange(20, 3000, 0.5, 150),
|
|
780
|
-
"auto_pay": [false, false, false, true, true],
|
|
781
|
-
}
|
|
782
|
-
},
|
|
783
|
-
{
|
|
784
|
-
event: "bill payment missed",
|
|
785
|
-
weight: 1,
|
|
786
|
-
isStrictEvent: true, // hook-only: created by Hook 6 from manual bill-paid events
|
|
787
|
-
properties: {
|
|
788
|
-
"bill_type": ["rent", "utilities", "phone", "insurance", "subscription", "loan_payment"],
|
|
789
|
-
"amount": u.weighNumRange(20, 3000, 0.5, 150),
|
|
790
|
-
}
|
|
791
|
-
},
|
|
792
|
-
{
|
|
793
|
-
event: "budget created",
|
|
794
|
-
weight: 3,
|
|
795
|
-
isStrictEvent: false,
|
|
796
|
-
properties: {
|
|
797
|
-
"category": ["food", "transport", "entertainment", "shopping", "bills", "savings"],
|
|
798
|
-
"monthly_limit": u.weighNumRange(50, 2000, 0.5, 300),
|
|
799
|
-
}
|
|
800
|
-
},
|
|
801
|
-
{
|
|
802
|
-
event: "budget alert",
|
|
803
|
-
weight: 4,
|
|
804
|
-
properties: {
|
|
805
|
-
"alert_type": ["approaching_limit", "exceeded", "on_track"],
|
|
806
|
-
"percent_used": u.weighNumRange(50, 150, 1, 90),
|
|
807
|
-
}
|
|
808
|
-
},
|
|
809
|
-
{
|
|
810
|
-
event: "savings goal set",
|
|
811
|
-
weight: 3,
|
|
812
|
-
isStrictEvent: false,
|
|
813
|
-
properties: {
|
|
814
|
-
"goal_type": ["emergency", "vacation", "car", "home", "education", "retirement"],
|
|
815
|
-
"target_amount": u.weighNumRange(500, 50000, 0.3, 5000),
|
|
816
|
-
"monthly_contribution": u.weighNumRange(25, 2000, 0.5, 200),
|
|
817
|
-
}
|
|
818
|
-
},
|
|
819
|
-
{
|
|
820
|
-
event: "investment made",
|
|
821
|
-
weight: 4,
|
|
822
|
-
isStrictEvent: false,
|
|
823
|
-
properties: {
|
|
824
|
-
"investment_type": ["stocks", "etf", "crypto", "bonds", "mutual_fund"],
|
|
825
|
-
"amount": u.weighNumRange(10, 10000, 0.3, 250),
|
|
826
|
-
"action": ["buy", "sell", "buy"],
|
|
827
|
-
}
|
|
828
|
-
},
|
|
829
|
-
{
|
|
830
|
-
event: "card locked",
|
|
831
|
-
weight: 2,
|
|
832
|
-
isStrictEvent: false,
|
|
833
|
-
properties: {
|
|
834
|
-
"reason": ["lost", "stolen", "suspicious_activity", "travel"],
|
|
835
|
-
}
|
|
836
|
-
},
|
|
837
|
-
{
|
|
838
|
-
event: "dispute filed",
|
|
839
|
-
weight: 2,
|
|
840
|
-
isStrictEvent: false,
|
|
841
|
-
properties: {
|
|
842
|
-
"dispute_amount": u.weighNumRange(10, 2000, 0.5, 100),
|
|
843
|
-
"reason": ["unauthorized", "duplicate", "not_received", "damaged", "wrong_amount"],
|
|
844
|
-
}
|
|
845
|
-
},
|
|
846
|
-
{
|
|
847
|
-
event: "loan applied",
|
|
848
|
-
weight: 2,
|
|
849
|
-
properties: {
|
|
850
|
-
"loan_type": ["personal", "auto", "home", "student", "business"],
|
|
851
|
-
"requested_amount": u.weighNumRange(1000, 100000, 0.3, 10000),
|
|
852
|
-
}
|
|
853
|
-
},
|
|
854
|
-
{
|
|
855
|
-
event: "loan approved",
|
|
856
|
-
weight: 1,
|
|
857
|
-
properties: {
|
|
858
|
-
"loan_type": ["personal", "auto", "home", "student", "business"],
|
|
859
|
-
"approved_amount": u.weighNumRange(1000, 100000, 0.3, 10000),
|
|
860
|
-
"interest_rate": u.weighNumRange(3, 25, 1, 8),
|
|
861
|
-
}
|
|
862
|
-
},
|
|
863
|
-
{
|
|
864
|
-
event: "premium upgraded",
|
|
865
|
-
weight: 2,
|
|
866
|
-
isStrictEvent: false,
|
|
867
|
-
properties: {
|
|
868
|
-
"old_tier": ["basic", "plus", "premium"],
|
|
869
|
-
"new_tier": ["plus", "premium", "premium"],
|
|
870
|
-
"monthly_fee": [4.99, 9.99, 14.99],
|
|
871
|
-
}
|
|
872
|
-
},
|
|
873
|
-
{
|
|
874
|
-
event: "support contacted",
|
|
875
|
-
weight: 3,
|
|
876
|
-
isStrictEvent: false,
|
|
877
|
-
properties: {
|
|
878
|
-
"channel": ["chat", "phone", "email", "in_app"],
|
|
879
|
-
"issue_type": ["transaction", "account", "card", "transfer", "technical"],
|
|
880
|
-
"resolved": [false, true, true, true, true],
|
|
881
|
-
}
|
|
882
|
-
},
|
|
883
|
-
{
|
|
884
|
-
event: "notification opened",
|
|
885
|
-
weight: 10,
|
|
886
|
-
properties: {
|
|
887
|
-
"notification_type": ["transaction", "low_balance", "bill_due", "reward", "security", "promo"],
|
|
888
|
-
"action_taken": [false, false, true, true, true],
|
|
889
|
-
}
|
|
890
|
-
},
|
|
891
|
-
{
|
|
892
|
-
event: "reward redeemed",
|
|
893
|
-
weight: 4,
|
|
894
|
-
isStrictEvent: false,
|
|
895
|
-
properties: {
|
|
896
|
-
"reward_type": ["cashback", "points", "discount", "partner_offer"],
|
|
897
|
-
"value": u.weighNumRange(1, 100, 0.5, 10),
|
|
898
|
-
}
|
|
899
|
-
}
|
|
900
|
-
],
|
|
901
|
-
|
|
902
|
-
superProps: {
|
|
903
|
-
account_tier: ["basic", "basic", "basic", "plus", "plus", "premium"],
|
|
904
|
-
Platform: ["ios", "android", "web"],
|
|
905
|
-
},
|
|
906
|
-
|
|
907
|
-
userProps: {
|
|
908
|
-
account_tier: ["basic", "basic", "basic", "plus", "plus", "premium"],
|
|
909
|
-
Platform: ["ios", "android", "web"],
|
|
910
|
-
"credit_score_range": ["300-579", "580-669", "670-739", "740-799", "800-850"],
|
|
911
|
-
"income_bracket": ["under_30k", "30k_50k", "50k_75k", "75k_100k", "100k_150k", "over_150k"],
|
|
912
|
-
"account_age_months": u.weighNumRange(1, 60, 0.5, 12),
|
|
913
|
-
"total_balance": u.weighNumRange(0, 100000, 0.3, 5000),
|
|
914
|
-
"has_direct_deposit": [false, false, true, true, true],
|
|
915
|
-
"account_segment": ["personal"],
|
|
916
|
-
"employee_count": [0],
|
|
917
|
-
"annual_revenue": [0],
|
|
918
|
-
"industry": [""],
|
|
919
|
-
"age_range": [""],
|
|
920
|
-
"life_stage": [""],
|
|
921
|
-
},
|
|
922
|
-
|
|
923
|
-
groupKeys: [
|
|
924
|
-
["household_id", 500, ["transaction completed", "transfer sent", "bill paid", "savings goal set"]],
|
|
925
|
-
],
|
|
926
|
-
|
|
927
|
-
groupProps: {
|
|
928
|
-
household_id: {
|
|
929
|
-
"household_size": u.weighNumRange(1, 6),
|
|
930
|
-
"combined_income": u.weighNumRange(20000, 300000, 0.3, 75000),
|
|
931
|
-
"financial_health_score": u.weighNumRange(1, 100, 1, 65),
|
|
932
|
-
"primary_bank": ["NexBank_only", "multi_bank", "NexBank_only"],
|
|
933
|
-
}
|
|
934
|
-
},
|
|
935
|
-
|
|
936
|
-
lookupTables: [],
|
|
937
|
-
|
|
938
|
-
hook(record, type, meta) {
|
|
939
|
-
if (type === "user") return handleUserHooks(record);
|
|
940
|
-
if (type === "event") return handleEventHooks(record);
|
|
941
|
-
if (type === "everything") return handleEverythingHooks(record, meta);
|
|
942
|
-
return record;
|
|
943
|
-
}
|
|
944
|
-
};
|
|
945
|
-
|
|
946
|
-
export default config;
|