@ak--47/dungeon-master 1.4.2 → 1.4.3
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/dungeons/vertical/community.js +75 -6
- package/dungeons/vertical/crypto.js +63 -8
- package/dungeons/vertical/dating.js +36 -4
- package/dungeons/vertical/devtools.js +25 -0
- package/dungeons/vertical/ecommerce.js +260 -31
- package/dungeons/vertical/education.js +107 -39
- package/dungeons/vertical/fintech.js +28 -28
- package/dungeons/vertical/fitness.js +48 -34
- package/dungeons/vertical/food-delivery.js +112 -30
- package/dungeons/vertical/gaming.js +48 -58
- package/dungeons/vertical/healthcare.js +47 -37
- package/dungeons/vertical/insurance-application.js +342 -61
- package/dungeons/vertical/logistics.js +37 -5
- package/dungeons/vertical/marketplace.js +42 -18
- package/dungeons/vertical/media.js +125 -57
- package/dungeons/vertical/real-estate.js +25 -0
- package/dungeons/vertical/sass.js +161 -20
- package/dungeons/vertical/social.js +25 -0
- package/dungeons/vertical/travel.js +74 -3
- package/index.js +2 -1
- package/lib/utils/utils.js +80 -7
- package/lib/verify/emulate-breakdown.js +1 -1
- package/lib/verify/index.js +1 -0
- package/lib/verify/schema-validator.js +258 -0
- package/lib/verify/verify-dungeon.js +6 -5
- package/package.json +1 -1
- package/scripts/verify-runner.mjs +1 -1
- package/types.d.ts +29 -1
|
@@ -13,6 +13,7 @@ import utc from "dayjs/plugin/utc.js";
|
|
|
13
13
|
import "dotenv/config";
|
|
14
14
|
import * as u from "../../lib/utils/utils.js";
|
|
15
15
|
import * as v from "ak-tools";
|
|
16
|
+
import { findFirstSequence, scaleFunnelTTC } from "../../lib/hook-helpers/timing.js";
|
|
16
17
|
|
|
17
18
|
dayjs.extend(utc);
|
|
18
19
|
const chance = u.initChance(SEED);
|
|
@@ -110,7 +111,7 @@ const clinicIds = v.range(1, 25).map(() => `CLINIC_${v.uid(4)}`);
|
|
|
110
111
|
* 3. EXPERIENCED DOCTOR SATISFACTION (everything hook)
|
|
111
112
|
* ───────────────────────────────────────────────────────────────
|
|
112
113
|
*
|
|
113
|
-
* PATTERN: Users who had >
|
|
114
|
+
* PATTERN: Users who had >12 consultation events get higher avg
|
|
114
115
|
* satisfaction_score (boosted to 4.0-5.0 range) vs baseline 1-5.
|
|
115
116
|
* Simulates experienced doctors earning better reviews.
|
|
116
117
|
*
|
|
@@ -227,23 +228,26 @@ const clinicIds = v.range(1, 25).map(() => `CLINIC_${v.uid(4)}`);
|
|
|
227
228
|
* and limited scheduling, reducing completed consultations.
|
|
228
229
|
*
|
|
229
230
|
* ───────────────────────────────────────────────────────────────
|
|
230
|
-
* 9. BOOKING FUNNEL
|
|
231
|
+
* 9. BOOKING FUNNEL TTC BY TIER (everything hook — property scaling)
|
|
231
232
|
*
|
|
232
|
-
* PATTERN: Premium
|
|
233
|
-
* (
|
|
233
|
+
* PATTERN: Premium users get shorter wait times and consultation
|
|
234
|
+
* durations (0.67x); Free users get longer (1.4x); Basic at 1.0x.
|
|
235
|
+
* Scales `wait_time_hours` on "appointment booked" and
|
|
236
|
+
* `duration_minutes` on "consultation completed".
|
|
234
237
|
*
|
|
235
238
|
* HOW TO FIND IT IN MIXPANEL:
|
|
236
239
|
*
|
|
237
|
-
* Report 1:
|
|
238
|
-
* -
|
|
239
|
-
* - Measure:
|
|
240
|
+
* Report 1: Wait Time by Subscription Tier
|
|
241
|
+
* - Insights > "appointment booked"
|
|
242
|
+
* - Measure: Average of "wait_time_hours"
|
|
240
243
|
* - Breakdown: subscription_tier
|
|
241
|
-
* - Expected: premium ~ 0.
|
|
244
|
+
* - Expected: premium ~ 0.67x baseline; free ~ 1.4x baseline
|
|
242
245
|
*
|
|
243
|
-
*
|
|
244
|
-
*
|
|
245
|
-
*
|
|
246
|
-
*
|
|
246
|
+
* Report 2: Consultation Duration by Tier
|
|
247
|
+
* - Insights > "consultation completed"
|
|
248
|
+
* - Measure: Average of "duration_minutes"
|
|
249
|
+
* - Breakdown: subscription_tier
|
|
250
|
+
* - Expected: premium ~ 0.67x baseline; free ~ 1.4x baseline
|
|
247
251
|
*
|
|
248
252
|
* ───────────────────────────────────────────────────────────────
|
|
249
253
|
* 10. CONSULTATION-COUNT MAGIC NUMBER (everything)
|
|
@@ -279,13 +283,13 @@ const clinicIds = v.range(1, 25).map(() => `CLINIC_${v.uid(4)}`);
|
|
|
279
283
|
* ----------------------------|---------------------|----------|-----------|------
|
|
280
284
|
* After-Hours Pricing | consultation_fee | 1x | 1.5x | 1.5x
|
|
281
285
|
* Flu Season Spike | respiratory share | ~ 15% | ~ 60% | 4x
|
|
282
|
-
* Experienced Doctor Sat. | satisfaction_score |
|
|
286
|
+
* Experienced Doctor Sat. | satisfaction_score | ~2.0 | 4.0-5.0 | ~2x
|
|
283
287
|
* Video Follow-Up Lift | follow-ups/user | 1x | 2x | 2x
|
|
284
288
|
* Chronic Refill Chain | refills (chronic) | 1 | 3-4 | 3-4x
|
|
285
289
|
* Occasional No-Shows | booking→consult | 95% | 75% | -20%
|
|
286
290
|
* Doctor Specialization | years_experience | 5 | 22 | 4.4x
|
|
287
291
|
* Free-Tier Conversion Drop | funnel conversion | 40% | 28% | -30%
|
|
288
|
-
* Booking
|
|
292
|
+
* Booking TTC by Tier | wait_time/duration | 1x | 0.67/1.4x | ~ 2.1x range
|
|
289
293
|
* Consult-Count Magic Number | sweet consult fee | 1x | 1.25x | 1.25x
|
|
290
294
|
* Consult-Count Magic Number | over days_until_fu | 1x | 1.5x | +50%
|
|
291
295
|
*/
|
|
@@ -706,27 +710,6 @@ const config = {
|
|
|
706
710
|
}
|
|
707
711
|
}
|
|
708
712
|
|
|
709
|
-
// HOOK 9 (T2C): BOOKING FUNNEL TIME-TO-CONVERT (funnel-post)
|
|
710
|
-
// Premium tier completes Booking funnel 1.35x faster (factor 0.74);
|
|
711
|
-
// Free tier 1.33x slower (factor 1.33).
|
|
712
|
-
if (type === "funnel-post") {
|
|
713
|
-
const segment = meta?.profile?.subscription_tier;
|
|
714
|
-
if (Array.isArray(record) && record.length > 1) {
|
|
715
|
-
const factor = (
|
|
716
|
-
segment === "premium" ? 0.74 :
|
|
717
|
-
segment === "free" ? 1.33 :
|
|
718
|
-
1.0
|
|
719
|
-
);
|
|
720
|
-
if (factor !== 1.0) {
|
|
721
|
-
for (let i = 1; i < record.length; i++) {
|
|
722
|
-
const prev = dayjs(record[i - 1].time);
|
|
723
|
-
const newGap = Math.round(dayjs(record[i].time).diff(prev) * factor);
|
|
724
|
-
record[i].time = prev.add(newGap, "milliseconds").toISOString();
|
|
725
|
-
}
|
|
726
|
-
}
|
|
727
|
-
}
|
|
728
|
-
}
|
|
729
|
-
|
|
730
713
|
// (HOOK 1: AFTER-HOURS SURGE PRICING moved to everything hook — hour checks
|
|
731
714
|
// must run after bunchIntoSessions redistributes timestamps)
|
|
732
715
|
// (HOOK 2: FLU SEASON SPIKE moved to everything hook — same reason)
|
|
@@ -750,6 +733,33 @@ const config = {
|
|
|
750
733
|
});
|
|
751
734
|
}
|
|
752
735
|
|
|
736
|
+
// HOOK 9: BOOKING FUNNEL TTC BY TIER (property scaling)
|
|
737
|
+
// Premium users get shorter wait_time_hours (0.67x) and duration_minutes (0.67x).
|
|
738
|
+
// Free users get longer wait_time_hours (1.4x) and duration_minutes (1.4x).
|
|
739
|
+
// Basic users stay at baseline. SQL-measurable via AVG(wait_time_hours) broken by tier.
|
|
740
|
+
if (profile) {
|
|
741
|
+
const userTier = profile.subscription_tier;
|
|
742
|
+
const ttcFactor = userTier === "premium" ? 0.67 : userTier === "free" ? 1.4 : 1.0;
|
|
743
|
+
if (ttcFactor !== 1.0) {
|
|
744
|
+
// Timestamp shift: affects Mixpanel funnel TTC
|
|
745
|
+
const bookingSeq = findFirstSequence(
|
|
746
|
+
record,
|
|
747
|
+
["appointment booked", "consultation completed", "follow up scheduled"],
|
|
748
|
+
60 * 24 * 30
|
|
749
|
+
);
|
|
750
|
+
if (bookingSeq) scaleFunnelTTC(bookingSeq, ttcFactor);
|
|
751
|
+
// Property scale: affects Insights AVG reports
|
|
752
|
+
record.forEach(e => {
|
|
753
|
+
if (e.event === "appointment booked" && typeof e.wait_time_hours === "number") {
|
|
754
|
+
e.wait_time_hours = Math.round(e.wait_time_hours * ttcFactor * 10) / 10;
|
|
755
|
+
}
|
|
756
|
+
if (e.event === "consultation completed" && typeof e.duration_minutes === "number") {
|
|
757
|
+
e.duration_minutes = Math.round(e.duration_minutes * ttcFactor);
|
|
758
|
+
}
|
|
759
|
+
});
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
|
|
753
763
|
// HOOK 1: AFTER-HOURS SURGE PRICING — consultations 7PM-7AM
|
|
754
764
|
// UTC get consultation_fee 1.5x. No flag — discover via HOD chart.
|
|
755
765
|
record.forEach(e => {
|
|
@@ -783,13 +793,13 @@ const config = {
|
|
|
783
793
|
}
|
|
784
794
|
|
|
785
795
|
// ── HOOK 3: EXPERIENCED DOCTOR SATISFACTION ──────
|
|
786
|
-
// Users with >
|
|
796
|
+
// Users with >12 consultation events get boosted satisfaction scores.
|
|
787
797
|
let consultCount = 0;
|
|
788
798
|
record.forEach(e => {
|
|
789
799
|
if (e.event === "consultation completed") consultCount++;
|
|
790
800
|
});
|
|
791
801
|
|
|
792
|
-
if (consultCount >
|
|
802
|
+
if (consultCount > 12) {
|
|
793
803
|
record.forEach(e => {
|
|
794
804
|
if (e.event === "consultation completed") {
|
|
795
805
|
e.satisfaction_score = chance.floating({ min: 4.0, max: 5.0, fixed: 1 });
|
|
@@ -31,6 +31,9 @@ const chance = u.initChance(SEED);
|
|
|
31
31
|
*
|
|
32
32
|
* - 5,000 users over 100 days
|
|
33
33
|
* - 600,000 events across 18 event types
|
|
34
|
+
* - 10 hooks (version stamping, ticket volume, conversion boost, magic number,
|
|
35
|
+
* TTC scaling, claims experiment, risk approval, doc upload retention,
|
|
36
|
+
* renewal spike, claim-to-premium)
|
|
34
37
|
* - 5 funnels (onboarding, application, approval, claims, renewal)
|
|
35
38
|
* - 5 insurance types as super property (auto, home, life, health, renters)
|
|
36
39
|
* - Deterministic app versioning (2.10 → 2.11 → 2.12 → 2.13)
|
|
@@ -52,12 +55,13 @@ const chance = u.initChance(SEED);
|
|
|
52
55
|
|
|
53
56
|
/*
|
|
54
57
|
* ===================================================================
|
|
55
|
-
* ANALYTICS HOOKS (
|
|
58
|
+
* ANALYTICS HOOKS (10 hooks)
|
|
56
59
|
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
60
|
+
* Hooks 1-5: Version stamping, support ticket volume drop, application
|
|
61
|
+
* conversion boost, step-count magic number, application TTC by account type.
|
|
62
|
+
*
|
|
63
|
+
* Hooks 6-10: Claims experiment, risk profile approval gating, document
|
|
64
|
+
* upload retention, end-of-quarter renewal spike, claim-to-premium increase.
|
|
61
65
|
* ===================================================================
|
|
62
66
|
*
|
|
63
67
|
* NOTE: All cohort effects are HIDDEN — no flag stamping. Discoverable
|
|
@@ -169,6 +173,150 @@ const chance = u.initChance(SEED);
|
|
|
169
173
|
* REAL-WORLD ANALOGUE: Engaged applicants get higher premiums approved;
|
|
170
174
|
* over-engaged ones look like fraud and get flagged.
|
|
171
175
|
*
|
|
176
|
+
* -------------------------------------------------------------------
|
|
177
|
+
* 5. APPLICATION COMPLETION TIME-TO-CONVERT (everything)
|
|
178
|
+
* -------------------------------------------------------------------
|
|
179
|
+
*
|
|
180
|
+
* PATTERN: In the everything hook, place each "application approved"
|
|
181
|
+
* event at a fixed target offset from the first "application started"
|
|
182
|
+
* event, determined by account_type (assigned via deterministic hash):
|
|
183
|
+
* - business: ~36h target offset (~0.75x baseline)
|
|
184
|
+
* - individual: ~48h target offset (baseline)
|
|
185
|
+
* - family: ~63h target offset (~1.31x baseline)
|
|
186
|
+
*
|
|
187
|
+
* HOW TO FIND IT IN MIXPANEL:
|
|
188
|
+
*
|
|
189
|
+
* Report 1: Application Funnel TTC by Account Type
|
|
190
|
+
* - Report type: Funnels
|
|
191
|
+
* - Steps: "application started" → "application approved"
|
|
192
|
+
* - Breakdown: account_type (from account created event)
|
|
193
|
+
* - Measure: Median time to convert
|
|
194
|
+
* - Expected: business < individual < family
|
|
195
|
+
*
|
|
196
|
+
* REAL-WORLD ANALOGUE: Business applicants have streamlined processes
|
|
197
|
+
* and pre-filled forms; family policies require more documentation.
|
|
198
|
+
*
|
|
199
|
+
* -------------------------------------------------------------------
|
|
200
|
+
* 6. CLAIMS PROCESS EXPERIMENT (funnel config — no hook code)
|
|
201
|
+
* -------------------------------------------------------------------
|
|
202
|
+
*
|
|
203
|
+
* PATTERN: A/B experiment on the Claims Process funnel. The engine
|
|
204
|
+
* fires $experiment_started events and applies conversion/TTC
|
|
205
|
+
* multipliers automatically. "Simplified Claims" variant gets 1.3x
|
|
206
|
+
* conversion and 0.8x time-to-convert. Experiment runs in the last
|
|
207
|
+
* 35 days of the dataset.
|
|
208
|
+
*
|
|
209
|
+
* HOW TO FIND IT IN MIXPANEL:
|
|
210
|
+
*
|
|
211
|
+
* Report 1: Experiment Results
|
|
212
|
+
* - Report type: Funnels
|
|
213
|
+
* - Steps: "claim filed" -> "claim status checked" -> "support ticket created"
|
|
214
|
+
* - Breakdown: "Variant name"
|
|
215
|
+
* - Date range: last 35 days of dataset
|
|
216
|
+
* - Expected: "Simplified Claims" has ~1.3x conversion vs "Control"
|
|
217
|
+
*
|
|
218
|
+
* Report 2: Experiment Started Events
|
|
219
|
+
* - Report type: Insights
|
|
220
|
+
* - Event: "$experiment_started"
|
|
221
|
+
* - Breakdown: "Variant name"
|
|
222
|
+
* - Expected: roughly equal distribution between Control and Simplified Claims
|
|
223
|
+
*
|
|
224
|
+
* REAL-WORLD ANALOGUE: A/B testing a simplified claims flow to reduce
|
|
225
|
+
* friction and improve completion rates.
|
|
226
|
+
*
|
|
227
|
+
* -------------------------------------------------------------------
|
|
228
|
+
* 7. RISK PROFILE AFFECTS APPROVAL (funnel-pre)
|
|
229
|
+
* -------------------------------------------------------------------
|
|
230
|
+
*
|
|
231
|
+
* PATTERN: In the funnel-pre hook, low-risk users get 1.8x approval
|
|
232
|
+
* funnel conversion (capped at 95%), high-risk users get 0.3x.
|
|
233
|
+
* Medium-risk users are unchanged.
|
|
234
|
+
*
|
|
235
|
+
* HOW TO FIND IT IN MIXPANEL:
|
|
236
|
+
*
|
|
237
|
+
* Report 1: Application Approval Funnel by Risk Profile
|
|
238
|
+
* - Report type: Funnels
|
|
239
|
+
* - Steps: "application submitted" -> "application approved" -> "policy activated"
|
|
240
|
+
* - Breakdown: "risk_profile" (user property)
|
|
241
|
+
* - Expected: low >> medium >> high conversion rates
|
|
242
|
+
*
|
|
243
|
+
* REAL-WORLD ANALOGUE: Underwriting engines auto-approve low-risk
|
|
244
|
+
* applicants and require manual review for high-risk ones.
|
|
245
|
+
*
|
|
246
|
+
* -------------------------------------------------------------------
|
|
247
|
+
* 8. DOCUMENT UPLOAD RETENTION (everything — retention magic number)
|
|
248
|
+
* -------------------------------------------------------------------
|
|
249
|
+
*
|
|
250
|
+
* PATTERN: Users who upload 3+ documents in their first 14 days
|
|
251
|
+
* retain normally. Users with fewer uploads lose 75% of events after
|
|
252
|
+
* day 30. Only affects born-in-dataset users.
|
|
253
|
+
*
|
|
254
|
+
* HOW TO FIND IT IN MIXPANEL:
|
|
255
|
+
*
|
|
256
|
+
* Report 1: Retention by Document Upload Cohort
|
|
257
|
+
* - Report type: Retention
|
|
258
|
+
* - Starting event: "account created"
|
|
259
|
+
* - Return event: any event
|
|
260
|
+
* - Cohort A: users with >= 3 "document uploaded" in first 14 days
|
|
261
|
+
* - Cohort B: users with < 3 "document uploaded" in first 14 days
|
|
262
|
+
* - Expected: Cohort A retains well past day 30; Cohort B drops off sharply
|
|
263
|
+
*
|
|
264
|
+
* Report 2: Post-Day-30 Event Volume
|
|
265
|
+
* - Report type: Insights
|
|
266
|
+
* - Event: all events
|
|
267
|
+
* - Compare doc-uploader cohort vs non-uploader
|
|
268
|
+
* - Expected: non-uploaders have ~75% fewer events after day 30
|
|
269
|
+
*
|
|
270
|
+
* REAL-WORLD ANALOGUE: Users who complete onboarding paperwork early
|
|
271
|
+
* are invested in the product and retain; those who don't churn.
|
|
272
|
+
*
|
|
273
|
+
* -------------------------------------------------------------------
|
|
274
|
+
* 9. END-OF-QUARTER RENEWAL SPIKE (everything — temporal)
|
|
275
|
+
* -------------------------------------------------------------------
|
|
276
|
+
*
|
|
277
|
+
* PATTERN: Days 85-95 of the dataset get 3x renewal completed events
|
|
278
|
+
* and 2x coverage reviewed events via cloning. Simulates end-of-quarter
|
|
279
|
+
* policy renewal batch processing.
|
|
280
|
+
*
|
|
281
|
+
* HOW TO FIND IT IN MIXPANEL:
|
|
282
|
+
*
|
|
283
|
+
* Report 1: Renewal Volume Over Time
|
|
284
|
+
* - Report type: Insights
|
|
285
|
+
* - Event: "renewal completed"
|
|
286
|
+
* - Measure: Total
|
|
287
|
+
* - Line chart by day
|
|
288
|
+
* - Expected: visible spike at days 85-95, ~3x baseline volume
|
|
289
|
+
*
|
|
290
|
+
* Report 2: Coverage Review Spike
|
|
291
|
+
* - Report type: Insights
|
|
292
|
+
* - Event: "coverage reviewed"
|
|
293
|
+
* - Line chart by day
|
|
294
|
+
* - Expected: ~2x volume during days 85-95
|
|
295
|
+
*
|
|
296
|
+
* REAL-WORLD ANALOGUE: Insurance companies batch-process renewals at
|
|
297
|
+
* quarter-end, producing predictable volume spikes.
|
|
298
|
+
*
|
|
299
|
+
* -------------------------------------------------------------------
|
|
300
|
+
* 10. CLAIM-TO-PREMIUM INCREASE (event — closure Map)
|
|
301
|
+
* -------------------------------------------------------------------
|
|
302
|
+
*
|
|
303
|
+
* PATTERN: After a user files a claim, their next "payment made" event
|
|
304
|
+
* gets premium_amount doubled (2.0x). Uses a module-level Map to
|
|
305
|
+
* track state across events. The Map entry is consumed (deleted) on
|
|
306
|
+
* the first payment after the claim, so the effect is one-shot.
|
|
307
|
+
*
|
|
308
|
+
* HOW TO FIND IT IN MIXPANEL:
|
|
309
|
+
*
|
|
310
|
+
* Report 1: Average Premium After Claim
|
|
311
|
+
* - Report type: Insights
|
|
312
|
+
* - Event: "payment made"
|
|
313
|
+
* - Measure: Average of "premium_amount"
|
|
314
|
+
* - Cohort A: users who did "claim filed" before
|
|
315
|
+
* - Cohort B: users who never filed a claim
|
|
316
|
+
* - Expected: Cohort A premium_amount ~ 2.0x Cohort B
|
|
317
|
+
*
|
|
318
|
+
* REAL-WORLD ANALOGUE: Insurance premiums increase after filing a claim.
|
|
319
|
+
*
|
|
172
320
|
* ===================================================================
|
|
173
321
|
* EXPECTED METRICS SUMMARY
|
|
174
322
|
* ===================================================================
|
|
@@ -180,8 +328,20 @@ const chance = u.initChance(SEED);
|
|
|
180
328
|
* Application Conversion | approval rate pre/post | 1x | ~ 1.3x | step-up
|
|
181
329
|
* Step-Count Magic Number | sweet approved_premium | 1x | 1.35x | 1.35x
|
|
182
330
|
* Step-Count Magic Number | over approvals/user | 1x | 0.6x | -40%
|
|
331
|
+
* Application TTC | business vs individual | 1x | 0.74x | faster
|
|
332
|
+
* Application TTC | family vs individual | 1x | 1.3x | slower
|
|
333
|
+
* Claims Experiment | simplified vs control | 1x | 1.3x | +30%
|
|
334
|
+
* Risk Profile Approval | low vs medium conv rate | 1x | 1.8x | +80%
|
|
335
|
+
* Risk Profile Approval | high vs medium conv rate| 1x | 0.3x | -70%
|
|
336
|
+
* Document Upload Ret. | post-d30 non-uploaders | 1x | 0.25x | -75%
|
|
337
|
+
* Renewal Spike | renewals d85-95 vs base | 1x | 3x | +200%
|
|
338
|
+
* Renewal Spike | reviews d85-95 vs base | 1x | 2x | +100%
|
|
339
|
+
* Claim-to-Premium | premium after claim | 1x | 2.0x | +100%
|
|
183
340
|
*/
|
|
184
341
|
|
|
342
|
+
// ── H10 closure state: tracks users who filed a claim ──
|
|
343
|
+
const claimFilers = new Map();
|
|
344
|
+
|
|
185
345
|
/** @type {Config} */
|
|
186
346
|
const config = {
|
|
187
347
|
version: 2,
|
|
@@ -364,6 +524,7 @@ const config = {
|
|
|
364
524
|
properties: {
|
|
365
525
|
payment_method: ["credit_card", "bank_transfer", "auto_pay", "check"],
|
|
366
526
|
amount: u.weighNumRange(30, 800, 0.3, 150),
|
|
527
|
+
premium_amount: u.weighNumRange(50, 600, 0.3, 150),
|
|
367
528
|
payment_status: ["success", "success", "success", "success", "failed"],
|
|
368
529
|
},
|
|
369
530
|
},
|
|
@@ -477,6 +638,14 @@ const config = {
|
|
|
477
638
|
conversionRate: 50,
|
|
478
639
|
timeToConvert: 24,
|
|
479
640
|
weight: 2,
|
|
641
|
+
experiment: {
|
|
642
|
+
name: "Simplified Claims Flow",
|
|
643
|
+
variants: [
|
|
644
|
+
{ name: "Control" },
|
|
645
|
+
{ name: "Simplified Claims", conversionMultiplier: 1.3, ttcMultiplier: 0.8 },
|
|
646
|
+
],
|
|
647
|
+
startDaysBeforeEnd: 35,
|
|
648
|
+
},
|
|
480
649
|
},
|
|
481
650
|
{
|
|
482
651
|
name: "Policy Renewal",
|
|
@@ -508,53 +677,50 @@ const config = {
|
|
|
508
677
|
|
|
509
678
|
// ── Hook Function ──
|
|
510
679
|
/**
|
|
511
|
-
* ARCHITECTED ANALYTICS HOOKS
|
|
512
|
-
*
|
|
513
|
-
* This hook function creates 3 deliberate patterns in the data:
|
|
514
|
-
*
|
|
515
|
-
* 1. VERSION STAMPING (everything): Every event gets a deterministic app_version
|
|
516
|
-
* based on its final timestamp. v2.10 → v2.11 → v2.12 → v2.13.
|
|
517
|
-
* All users shift simultaneously on release dates. Uses the everything
|
|
518
|
-
* hook (not event hook) because funnel events adjust time after the
|
|
519
|
-
* event hook runs.
|
|
680
|
+
* ARCHITECTED ANALYTICS HOOKS (10 total)
|
|
520
681
|
*
|
|
521
|
-
*
|
|
522
|
-
* support ticket volume (2-3 extra tickets per user with bug-related
|
|
523
|
-
* categories). Post-v2.13, tickets are progressively removed — creating
|
|
524
|
-
* a clear volume drop that trends downward.
|
|
682
|
+
* This hook function creates 10 deliberate patterns in the data:
|
|
525
683
|
*
|
|
526
|
-
*
|
|
527
|
-
*
|
|
528
|
-
*
|
|
529
|
-
*
|
|
684
|
+
* 1. VERSION STAMPING (everything): Deterministic app_version based on timestamp.
|
|
685
|
+
* 2. SUPPORT TICKET VOLUME (everything): Inflated pre-v2.13, progressive drop post.
|
|
686
|
+
* 3. APPLICATION CONVERSION BOOST (everything): Pre-v2.13 approval drop.
|
|
687
|
+
* 4. STEP-COUNT MAGIC NUMBER (everything): 8-14 steps = +35% premium; 15+ = -40% approvals.
|
|
688
|
+
* 5. APPLICATION TTC (everything): Business 0.74x faster, family 1.3x slower.
|
|
689
|
+
* 6. CLAIMS EXPERIMENT (funnel config): A/B test on claims funnel (engine-handled).
|
|
690
|
+
* 7. RISK PROFILE APPROVAL (funnel-pre): Low-risk 1.8x, high-risk 0.3x conversion.
|
|
691
|
+
* 8. DOCUMENT UPLOAD RETENTION (everything): 3+ uploads in 14d = retain; else -75% post-d30.
|
|
692
|
+
* 9. RENEWAL SPIKE (everything): Days 85-95 get 3x renewals, 2x coverage reviews.
|
|
693
|
+
* 10. CLAIM-TO-PREMIUM (event): Premium 2.0x on first payment after claim filed.
|
|
530
694
|
*/
|
|
531
695
|
hook: function (record, type, meta) {
|
|
532
|
-
//
|
|
533
|
-
//
|
|
534
|
-
//
|
|
535
|
-
if (type === "funnel-
|
|
536
|
-
const
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
accountType === "family" ? 1.3 :
|
|
542
|
-
1.0
|
|
543
|
-
);
|
|
544
|
-
if (factor !== 1.0) {
|
|
545
|
-
for (let i = 1; i < record.length; i++) {
|
|
546
|
-
const prev = dayjs(record[i - 1].time);
|
|
547
|
-
const newGap = Math.round(dayjs(record[i].time).diff(prev) * factor);
|
|
548
|
-
record[i].time = prev.add(newGap, "milliseconds").toISOString();
|
|
549
|
-
}
|
|
550
|
-
}
|
|
696
|
+
// =============================================================
|
|
697
|
+
// H7: RISK PROFILE AFFECTS APPROVAL (funnel-pre)
|
|
698
|
+
// =============================================================
|
|
699
|
+
if (type === "funnel-pre") {
|
|
700
|
+
const isApprovalFunnel = meta.funnel?.sequence?.includes("application approved");
|
|
701
|
+
if (isApprovalFunnel) {
|
|
702
|
+
const risk = meta.profile?.risk_profile;
|
|
703
|
+
if (risk === "low") record.conversionRate = Math.min(95, Math.round(record.conversionRate * 1.8));
|
|
704
|
+
else if (risk === "high") record.conversionRate = Math.round(record.conversionRate * 0.3);
|
|
551
705
|
}
|
|
552
706
|
}
|
|
553
707
|
|
|
554
708
|
// =============================================================
|
|
555
|
-
//
|
|
556
|
-
//
|
|
557
|
-
|
|
709
|
+
// H10: CLAIM-TO-PREMIUM INCREASE (event — closure Map)
|
|
710
|
+
// =============================================================
|
|
711
|
+
if (type === "event") {
|
|
712
|
+
if (record.event === "claim filed") {
|
|
713
|
+
claimFilers.set(record.user_id, true);
|
|
714
|
+
}
|
|
715
|
+
if (record.event === "payment made" && claimFilers.has(record.user_id)) {
|
|
716
|
+
record.premium_amount = Math.round((record.premium_amount || 500) * 2.0);
|
|
717
|
+
claimFilers.delete(record.user_id);
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
// =============================================================
|
|
722
|
+
// Everything hooks: H5 first (timestamps), then H2-H4, then
|
|
723
|
+
// H8-H9, then H1 (version stamping LAST).
|
|
558
724
|
// =============================================================
|
|
559
725
|
if (type === "everything") {
|
|
560
726
|
const datasetStart = dayjs.unix(meta.datasetStart);
|
|
@@ -573,27 +739,61 @@ const config = {
|
|
|
573
739
|
e.app_version = profile.app_version;
|
|
574
740
|
});
|
|
575
741
|
|
|
576
|
-
// ─── Hook #1: VERSION STAMPING ───
|
|
577
|
-
// Deterministic app_version on every event based on its final timestamp.
|
|
578
|
-
// v2.10 (days 0-30) → v2.11 (30-60) → v2.12 (60-90) → v2.13 (last 10 days)
|
|
579
|
-
for (const evt of userEvents) {
|
|
580
|
-
const eventTime = dayjs(evt.time);
|
|
581
|
-
if (eventTime.isBefore(V211_DATE)) {
|
|
582
|
-
evt.app_version = "2.10";
|
|
583
|
-
} else if (eventTime.isBefore(V212_DATE)) {
|
|
584
|
-
evt.app_version = "2.11";
|
|
585
|
-
} else if (eventTime.isBefore(V213_DATE)) {
|
|
586
|
-
evt.app_version = "2.12";
|
|
587
|
-
} else {
|
|
588
|
-
evt.app_version = "2.13";
|
|
589
|
-
}
|
|
590
|
-
}
|
|
591
|
-
|
|
592
742
|
// Find a user_id from any existing event
|
|
593
743
|
const userId =
|
|
594
744
|
userEvents.find((e) => e.user_id)?.user_id ||
|
|
595
745
|
userEvents[0]?.device_id;
|
|
596
746
|
|
|
747
|
+
// ─── Hook #5: APPLICATION COMPLETION TIME-TO-CONVERT ───
|
|
748
|
+
// Runs FIRST: adjusts timestamps before version stamping.
|
|
749
|
+
// Business accounts complete the application funnel faster (0.74x),
|
|
750
|
+
// family accounts slower (1.3x). Individual stays at baseline.
|
|
751
|
+
// Assigns account_type deterministically per user via djb2 hash
|
|
752
|
+
// (independent of engine RNG). Places each "application approved"
|
|
753
|
+
// event at a fixed offset from the first "application started".
|
|
754
|
+
{
|
|
755
|
+
// djb2 hash of userId → deterministic cohort
|
|
756
|
+
let h = 5381;
|
|
757
|
+
for (let i = 0; i < userId.length; i++) {
|
|
758
|
+
h = ((h << 5) + h + userId.charCodeAt(i)) | 0;
|
|
759
|
+
}
|
|
760
|
+
const acctTypes = ["individual", "family", "business"];
|
|
761
|
+
const userAccountType = acctTypes[((h % 3) + 3) % 3];
|
|
762
|
+
// Stamp account created events with the deterministic type
|
|
763
|
+
for (const evt of userEvents) {
|
|
764
|
+
if (evt.event === "account created") {
|
|
765
|
+
evt.account_type = userAccountType;
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
userEvents.sort((a, b) => new Date(a.time) - new Date(b.time));
|
|
769
|
+
// Collect all "application started" times
|
|
770
|
+
const startedTimes = [];
|
|
771
|
+
for (const evt of userEvents) {
|
|
772
|
+
if (evt.event === "application started") {
|
|
773
|
+
startedTimes.push(dayjs(evt.time).valueOf());
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
if (startedTimes.length > 0) {
|
|
777
|
+
const firstStartTime = startedTimes[0];
|
|
778
|
+
// Target hours: biz=36, indiv=48, family=63
|
|
779
|
+
// Ratios: biz/indiv≈0.75, family/indiv≈1.31
|
|
780
|
+
const targetHours = (
|
|
781
|
+
userAccountType === "business" ? 36 :
|
|
782
|
+
userAccountType === "family" ? 63 :
|
|
783
|
+
48
|
|
784
|
+
);
|
|
785
|
+
const targetMs = targetHours * 3600000;
|
|
786
|
+
for (const evt of userEvents) {
|
|
787
|
+
if (evt.event !== "application approved") continue;
|
|
788
|
+
const evtTime = dayjs(evt.time).valueOf();
|
|
789
|
+
const origGap = evtTime - firstStartTime;
|
|
790
|
+
// Small jitter from original gap (mod 4h) for variance
|
|
791
|
+
const jitter = origGap > 0 ? (origGap % (4 * 3600000)) : 0;
|
|
792
|
+
evt.time = dayjs(firstStartTime + targetMs + jitter).toISOString();
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
|
|
597
797
|
// ─── Hook #2: SUPPORT TICKET VOLUME ───
|
|
598
798
|
// PRE-V2.13: Inject 2-3 extra support tickets with bug-related categories
|
|
599
799
|
const preV213Tickets = userEvents.filter(
|
|
@@ -715,7 +915,88 @@ const config = {
|
|
|
715
915
|
}
|
|
716
916
|
}
|
|
717
917
|
|
|
718
|
-
//
|
|
918
|
+
// ─── Hook #8: DOCUMENT UPLOAD RETENTION ───
|
|
919
|
+
// Users with 3+ "document uploaded" in first 14 days retain;
|
|
920
|
+
// others lose 75% of events post-day-30.
|
|
921
|
+
if (meta.userIsBornInDataset) {
|
|
922
|
+
const firstT = userEvents[0]?.time;
|
|
923
|
+
if (firstT) {
|
|
924
|
+
const window14 = dayjs(firstT).add(14, "days").toISOString();
|
|
925
|
+
const docUploads = userEvents.filter(
|
|
926
|
+
(e) => e.event === "document uploaded" && e.time <= window14
|
|
927
|
+
).length;
|
|
928
|
+
if (docUploads < 3) {
|
|
929
|
+
const cutoff = dayjs(firstT).add(30, "days");
|
|
930
|
+
for (let i = userEvents.length - 1; i >= 0; i--) {
|
|
931
|
+
if (
|
|
932
|
+
dayjs(userEvents[i].time).isAfter(cutoff) &&
|
|
933
|
+
chance.bool({ likelihood: 75 })
|
|
934
|
+
) {
|
|
935
|
+
userEvents.splice(i, 1);
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
// ─── Hook #9: END-OF-QUARTER RENEWAL SPIKE ───
|
|
943
|
+
// Days 85-95 get 3x renewal clones + 2x coverage reviewed clones
|
|
944
|
+
{
|
|
945
|
+
const spikeStart = datasetStart.add(85, "days");
|
|
946
|
+
const spikeEnd = datasetStart.add(95, "days");
|
|
947
|
+
const clones = [];
|
|
948
|
+
userEvents.forEach((e) => {
|
|
949
|
+
const t = dayjs(e.time);
|
|
950
|
+
if (t.isAfter(spikeStart) && t.isBefore(spikeEnd)) {
|
|
951
|
+
if (e.event === "renewal completed") {
|
|
952
|
+
for (let c = 0; c < 2; c++) {
|
|
953
|
+
clones.push({
|
|
954
|
+
...e,
|
|
955
|
+
time: t
|
|
956
|
+
.add(
|
|
957
|
+
chance.integer({ min: 5, max: 240 }),
|
|
958
|
+
"minutes"
|
|
959
|
+
)
|
|
960
|
+
.toISOString(),
|
|
961
|
+
insert_id: chance.guid(),
|
|
962
|
+
});
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
if (e.event === "coverage reviewed") {
|
|
966
|
+
clones.push({
|
|
967
|
+
...e,
|
|
968
|
+
time: t
|
|
969
|
+
.add(
|
|
970
|
+
chance.integer({ min: 5, max: 120 }),
|
|
971
|
+
"minutes"
|
|
972
|
+
)
|
|
973
|
+
.toISOString(),
|
|
974
|
+
insert_id: chance.guid(),
|
|
975
|
+
});
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
});
|
|
979
|
+
if (clones.length) userEvents.push(...clones);
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
// ─── Hook #1: VERSION STAMPING ───
|
|
983
|
+
// Runs LAST: stamps app_version based on FINAL timestamps
|
|
984
|
+
// (after Hook #5 TTC scaling has adjusted event times).
|
|
985
|
+
// v2.10 (days 0-30) → v2.11 (30-60) → v2.12 (60-90) → v2.13 (last 10 days)
|
|
986
|
+
for (const evt of userEvents) {
|
|
987
|
+
const eventTime = dayjs(evt.time);
|
|
988
|
+
if (eventTime.isBefore(V211_DATE)) {
|
|
989
|
+
evt.app_version = "2.10";
|
|
990
|
+
} else if (eventTime.isBefore(V212_DATE)) {
|
|
991
|
+
evt.app_version = "2.11";
|
|
992
|
+
} else if (eventTime.isBefore(V213_DATE)) {
|
|
993
|
+
evt.app_version = "2.12";
|
|
994
|
+
} else {
|
|
995
|
+
evt.app_version = "2.13";
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
// Sort events by time after injection/removal/shifting
|
|
719
1000
|
userEvents.sort(
|
|
720
1001
|
(a, b) => new Date(a.time) - new Date(b.time)
|
|
721
1002
|
);
|