@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
|
@@ -12,6 +12,7 @@ import dayjs from "dayjs";
|
|
|
12
12
|
import utc from "dayjs/plugin/utc.js";
|
|
13
13
|
import "dotenv/config";
|
|
14
14
|
import * as u from "../../lib/utils/utils.js";
|
|
15
|
+
import { findFirstSequence, scaleFunnelTTC } from "../../lib/hook-helpers/timing.js";
|
|
15
16
|
|
|
16
17
|
dayjs.extend(utc);
|
|
17
18
|
const chance = u.initChance(SEED);
|
|
@@ -283,10 +284,13 @@ const chance = u.initChance(SEED);
|
|
|
283
284
|
* - By credit_score_range: loan approvals, tier adoption
|
|
284
285
|
*
|
|
285
286
|
* ---------------------------------------------------------------
|
|
286
|
-
* 9. ONBOARDING TIME-TO-CONVERT (
|
|
287
|
+
* 9. ONBOARDING TIME-TO-CONVERT (everything)
|
|
287
288
|
*
|
|
288
289
|
* PATTERN: Premium tier users complete the Onboarding funnel 1.5x
|
|
289
290
|
* faster (factor 0.67); Basic users 1.33x slower (factor 1.33).
|
|
291
|
+
* Applied in the everything hook via findFirstSequence + scaleFunnelTTC,
|
|
292
|
+
* so the effect is visible in both Mixpanel funnels and cross-event
|
|
293
|
+
* MIN→MIN SQL queries.
|
|
290
294
|
*
|
|
291
295
|
* HOW TO FIND IT IN MIXPANEL:
|
|
292
296
|
*
|
|
@@ -296,11 +300,6 @@ const chance = u.initChance(SEED);
|
|
|
296
300
|
* - Breakdown: account_tier
|
|
297
301
|
* - Expected: premium ~ 0.67x baseline; basic ~ 1.33x
|
|
298
302
|
*
|
|
299
|
-
* NOTE (funnel-post measurement): visible only via Mixpanel funnel
|
|
300
|
-
* median TTC. Cross-event MIN→MIN SQL queries on raw events do NOT
|
|
301
|
-
* show this — funnel-post adjusts gaps within funnel instances, not
|
|
302
|
-
* across the user's full event history.
|
|
303
|
-
*
|
|
304
303
|
* ---------------------------------------------------------------
|
|
305
304
|
* 10. TRANSACTION-COUNT MAGIC NUMBER (everything)
|
|
306
305
|
*
|
|
@@ -341,7 +340,7 @@ const chance = u.initChance(SEED);
|
|
|
341
340
|
* Auto-Pay Loyalty | Bill completion rate | 100% | 70% | -30%
|
|
342
341
|
* Premium Tier Value | Reward value (Premium)| 1x | 3x | 3x
|
|
343
342
|
* Month-End Anxiety | Session duration d28+ | 1x | 1.4x | 1.4x
|
|
344
|
-
* Onboarding T2C
|
|
343
|
+
* Onboarding T2C (H9) | median min by tier | 1x | 0.67/1.33x| 2x range
|
|
345
344
|
* Txn-Count Magic Num | sweet investment amt | 1x | 1.4x | 1.4x
|
|
346
345
|
* Txn-Count Magic Num | over premium upgrades | 1x | 0.8x | -20%
|
|
347
346
|
*/
|
|
@@ -690,27 +689,6 @@ const config = {
|
|
|
690
689
|
}
|
|
691
690
|
}
|
|
692
691
|
|
|
693
|
-
// HOOK 9 (T2C): ONBOARDING TIME-TO-CONVERT (funnel-post)
|
|
694
|
-
// Premium tier completes Onboarding funnel 1.5x faster (factor 0.67);
|
|
695
|
-
// Basic users 1.33x slower (factor 1.33).
|
|
696
|
-
if (type === "funnel-post") {
|
|
697
|
-
const segment = meta?.profile?.account_tier;
|
|
698
|
-
if (Array.isArray(record) && record.length > 1) {
|
|
699
|
-
const factor = (
|
|
700
|
-
segment === "premium" ? 0.67 :
|
|
701
|
-
segment === "basic" ? 1.33 :
|
|
702
|
-
1.0
|
|
703
|
-
);
|
|
704
|
-
if (factor !== 1.0) {
|
|
705
|
-
for (let i = 1; i < record.length; i++) {
|
|
706
|
-
const prev = dayjs(record[i - 1].time);
|
|
707
|
-
const newGap = Math.round(dayjs(record[i].time).diff(prev) * factor);
|
|
708
|
-
record[i].time = prev.add(newGap, "milliseconds").toISOString();
|
|
709
|
-
}
|
|
710
|
-
}
|
|
711
|
-
}
|
|
712
|
-
}
|
|
713
|
-
|
|
714
692
|
if (type === "everything") {
|
|
715
693
|
const datasetStart = dayjs.unix(meta.datasetStart);
|
|
716
694
|
const userEvents = record;
|
|
@@ -721,6 +699,28 @@ const config = {
|
|
|
721
699
|
e.Platform = profile.Platform;
|
|
722
700
|
});
|
|
723
701
|
|
|
702
|
+
// HOOK 9 (T2C): ONBOARDING TIME-TO-CONVERT (everything)
|
|
703
|
+
// Premium tier completes Onboarding funnel 1.5x faster (factor 0.67);
|
|
704
|
+
// Basic users 1.33x slower (factor 1.33). Finds the first occurrence
|
|
705
|
+
// of the onboarding sequence in the user's events and scales the gaps.
|
|
706
|
+
{
|
|
707
|
+
const ttcFactor = (
|
|
708
|
+
profile.account_tier === "premium" ? 0.67 :
|
|
709
|
+
profile.account_tier === "basic" ? 1.33 :
|
|
710
|
+
1.0
|
|
711
|
+
);
|
|
712
|
+
if (ttcFactor !== 1.0) {
|
|
713
|
+
const onboardingSeq = findFirstSequence(
|
|
714
|
+
userEvents,
|
|
715
|
+
["account opened", "app session", "balance checked"],
|
|
716
|
+
60 * 24 * 30 // 30-day max gap between steps
|
|
717
|
+
);
|
|
718
|
+
if (onboardingSeq) {
|
|
719
|
+
scaleFunnelTTC(onboardingSeq, ttcFactor);
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
|
|
724
724
|
// HOOK 1B: PERSONAL VS BUSINESS — business segment txns 4x larger
|
|
725
725
|
// (per Report 2 in JSDoc: business ~ $200, personal ~ $50).
|
|
726
726
|
if (profile.account_segment === "business") {
|
|
@@ -103,9 +103,11 @@ const chance = u.initChance(SEED);
|
|
|
103
103
|
* 3. STREAK RETENTION (everything hook)
|
|
104
104
|
* ───────────────────────────────────────────────────────────────
|
|
105
105
|
*
|
|
106
|
-
* PATTERN: Users with
|
|
106
|
+
* PATTERN: Users with >=2 workout events get streak_days set
|
|
107
107
|
* to their actual workout count on their profile, and receive
|
|
108
|
-
*
|
|
108
|
+
* achievement clones with super-linear scaling: 1 per workout
|
|
109
|
+
* for workouts 2-4, then 4 per workout beyond that. This
|
|
110
|
+
* amplifies the gap so athlete/casual ratio reaches 2x+.
|
|
109
111
|
*
|
|
110
112
|
* HOW TO FIND IT IN MIXPANEL:
|
|
111
113
|
*
|
|
@@ -264,25 +266,27 @@ const chance = u.initChance(SEED);
|
|
|
264
266
|
* 10. WORKOUT-COUNT MAGIC NUMBER (everything)
|
|
265
267
|
* ───────────────────────────────────────────────────────────────
|
|
266
268
|
*
|
|
267
|
-
* PATTERN: Sweet 12-
|
|
268
|
-
* duration_minutes (peak progression). Over
|
|
269
|
-
* post-day-30 events (overtraining
|
|
269
|
+
* PATTERN: Sweet 12-14 workouts/user → +35% on workout
|
|
270
|
+
* duration_minutes (peak progression). Over 15+ → drop 65% of
|
|
271
|
+
* post-day-30 non-workout, non-progress events (overtraining
|
|
272
|
+
* churn). Preserves workout + progress events so H8 funnel
|
|
273
|
+
* lift isn't diluted. No flag.
|
|
270
274
|
*
|
|
271
275
|
* HOW TO FIND IT IN MIXPANEL:
|
|
272
276
|
*
|
|
273
277
|
* Report 1: Avg Workout Duration by Workout-Count Bucket
|
|
274
|
-
* - Cohort A: users with 12-
|
|
278
|
+
* - Cohort A: users with 12-14 "workout completed"
|
|
275
279
|
* - Cohort B: users with 0-11
|
|
276
280
|
* - Event: "workout completed"
|
|
277
281
|
* - Measure: Average of "duration_minutes"
|
|
278
282
|
* - Expected: A ~ 1.35x B
|
|
279
283
|
*
|
|
280
284
|
* Report 2: D30+ Activity on Heavy Workout Cohort
|
|
281
|
-
* - Cohort C: users with >=
|
|
282
|
-
* - Cohort A: users with 12-
|
|
285
|
+
* - Cohort C: users with >= 15 "workout completed"
|
|
286
|
+
* - Cohort A: users with 12-14
|
|
283
287
|
* - Event: any event
|
|
284
|
-
* - Measure:
|
|
285
|
-
* - Expected: C ~
|
|
288
|
+
* - Measure: post-d30/pre-d30 ratio per user
|
|
289
|
+
* - Expected: C ~ 70% lower post/pre ratio than A (overtraining churn)
|
|
286
290
|
*
|
|
287
291
|
* REAL-WORLD ANALOGUE: Sweet-spot training drives progression;
|
|
288
292
|
* over-training causes injury and burnout.
|
|
@@ -303,7 +307,7 @@ const chance = u.initChance(SEED);
|
|
|
303
307
|
* Annual Funnel Lift | funnel conversion | 45% | 63% | 1.4x
|
|
304
308
|
* Workout Loop T2C | median min by tier | 1x | 0.77/1.25x| ~ 1.6x range
|
|
305
309
|
* Workout Magic Number | sweet duration_min | 1x | 1.35x | 1.35x
|
|
306
|
-
* Workout Magic Number | over D30+
|
|
310
|
+
* Workout Magic Number | over D30+ post/pre | 1x | 0.29x | -71%
|
|
307
311
|
*/
|
|
308
312
|
|
|
309
313
|
/** @type {Config} */
|
|
@@ -766,31 +770,38 @@ const config = {
|
|
|
766
770
|
}
|
|
767
771
|
|
|
768
772
|
// ── HOOK 3: STREAK RETENTION ─────────────────────
|
|
769
|
-
// Users with
|
|
770
|
-
// cloned achievement events
|
|
773
|
+
// Users with >=2 workouts get streak_days updated and
|
|
774
|
+
// cloned achievement events. Achievements scale super-
|
|
775
|
+
// linearly: 1 per workout for workouts 2-4, then 4 per
|
|
776
|
+
// workout beyond that. This amplifies the gap between
|
|
777
|
+
// high-workout segments (athlete/coach) and casual users.
|
|
771
778
|
const workoutEvents = events.filter(e => e.event === "workout completed");
|
|
772
|
-
if (workoutEvents.length
|
|
779
|
+
if (workoutEvents.length >= 2) {
|
|
773
780
|
// Update profile streak_days via a profile update event
|
|
774
781
|
if (meta && meta.profile) {
|
|
775
782
|
meta.profile.streak_days = workoutEvents.length;
|
|
776
783
|
}
|
|
777
784
|
|
|
778
|
-
//
|
|
785
|
+
// Super-linear achievement scaling:
|
|
786
|
+
// workouts 2-4: 1 achievement each
|
|
787
|
+
// workouts 5+: 4 achievements each
|
|
779
788
|
const templateAchievement = events.find(e => e.event === "achievement unlocked");
|
|
780
789
|
if (templateAchievement) {
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
790
|
+
let achievementCount = Math.min(workoutEvents.length - 1, 3); // 1 each for workouts 2-4
|
|
791
|
+
if (workoutEvents.length > 4) {
|
|
792
|
+
achievementCount += (workoutEvents.length - 4) * 4; // 4 each for workouts 5+
|
|
793
|
+
}
|
|
794
|
+
for (let a = 0; a < achievementCount; a++) {
|
|
795
|
+
const srcIdx = Math.min(a, workoutEvents.length - 1);
|
|
796
|
+
const sourceEvent = workoutEvents[srcIdx];
|
|
797
|
+
events.push({
|
|
798
|
+
...templateAchievement,
|
|
799
|
+
time: dayjs(sourceEvent.time).add(chance.integer({ min: 1, max: 60 }), "minutes").toISOString(),
|
|
800
|
+
user_id: sourceEvent.user_id,
|
|
801
|
+
achievement_type: "streak_milestone",
|
|
802
|
+
streak_days_at_unlock: a + 2,
|
|
803
|
+
});
|
|
804
|
+
}
|
|
794
805
|
}
|
|
795
806
|
}
|
|
796
807
|
|
|
@@ -838,20 +849,23 @@ const config = {
|
|
|
838
849
|
}
|
|
839
850
|
|
|
840
851
|
// HOOK 10: WORKOUT-COUNT MAGIC NUMBER (no flags)
|
|
841
|
-
// Sweet 12-
|
|
842
|
-
// progression). Over
|
|
843
|
-
// (overtraining → churn).
|
|
852
|
+
// Sweet 12-14 workouts → +35% on workout duration_minutes (peak
|
|
853
|
+
// progression). Over 15+ → drop 65% of post-day-30 non-workout
|
|
854
|
+
// events (overtraining → churn). Workout events are preserved
|
|
855
|
+
// so the bucket categorization stays consistent.
|
|
844
856
|
const workoutCount = events.filter(e => e.event === "workout completed").length;
|
|
845
|
-
if (workoutCount >= 12 && workoutCount <=
|
|
857
|
+
if (workoutCount >= 12 && workoutCount <= 14) {
|
|
846
858
|
events.forEach(e => {
|
|
847
859
|
if (e.event === "workout completed" && typeof e.duration_minutes === "number") {
|
|
848
860
|
e.duration_minutes = Math.round(e.duration_minutes * 1.35);
|
|
849
861
|
}
|
|
850
862
|
});
|
|
851
|
-
} else if (workoutCount >=
|
|
863
|
+
} else if (workoutCount >= 15) {
|
|
852
864
|
const day30 = datasetStart.add(30, "days");
|
|
865
|
+
const preserveEvents = new Set(["workout completed", "progress checked"]);
|
|
853
866
|
for (let i = events.length - 1; i >= 0; i--) {
|
|
854
|
-
if (
|
|
867
|
+
if (!preserveEvents.has(events[i].event) &&
|
|
868
|
+
dayjs(events[i].time).isAfter(day30) && chance.bool({ likelihood: 65 })) {
|
|
855
869
|
events.splice(i, 1);
|
|
856
870
|
}
|
|
857
871
|
}
|
|
@@ -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);
|
|
@@ -49,12 +50,11 @@ const chance = u.initChance(SEED);
|
|
|
49
50
|
|
|
50
51
|
/*
|
|
51
52
|
* ═══════════════════════════════════════════════════════════════════════════════
|
|
52
|
-
* ANALYTICS HOOKS (
|
|
53
|
+
* ANALYTICS HOOKS (10 hooks)
|
|
53
54
|
*
|
|
54
|
-
* Adds 9. ORDER LIFECYCLE TIME-TO-CONVERT: QuickBite+ 0.
|
|
55
|
-
* slower (
|
|
56
|
-
*
|
|
57
|
-
* Cross-event MIN→MIN SQL queries on raw events do NOT show this.
|
|
55
|
+
* Adds 9. ORDER LIFECYCLE TIME-TO-CONVERT: QuickBite+ 0.67x delivery times,
|
|
56
|
+
* Free 1.4x slower (everything hook, property scaling). Discover via
|
|
57
|
+
* avg(actual_delivery_mins) on "order delivered" by subscription_tier.
|
|
58
58
|
* ═══════════════════════════════════════════════════════════════════════════════
|
|
59
59
|
*
|
|
60
60
|
* NOTE: All cohort effects are HIDDEN — no flag stamping. Discoverable only via
|
|
@@ -212,8 +212,8 @@ const chance = u.initChance(SEED);
|
|
|
212
212
|
* ───────────────────────────────────────────────────────────────────────────────
|
|
213
213
|
*
|
|
214
214
|
* PATTERN: Users in the 4-8 order-placed sweet spot get +40% on order_total.
|
|
215
|
-
* Users with 9+ orders are over-engaged;
|
|
216
|
-
*
|
|
215
|
+
* Users with 9+ orders are over-engaged; their order_total is reduced to
|
|
216
|
+
* 0.65x (basket fatigue). No flag — discover by binning users on order count.
|
|
217
217
|
*
|
|
218
218
|
* HOW TO FIND IT IN MIXPANEL:
|
|
219
219
|
*
|
|
@@ -225,18 +225,66 @@ const chance = u.initChance(SEED);
|
|
|
225
225
|
* - Measure: Average of "order_total"
|
|
226
226
|
* - Expected: A ~ 1.4x B
|
|
227
227
|
*
|
|
228
|
-
* Report 2:
|
|
228
|
+
* Report 2: Avg Order Total on Heavy Orderers
|
|
229
229
|
* - Report type: Insights (with cohort)
|
|
230
230
|
* - Cohort C: users with >= 9 "order placed"
|
|
231
231
|
* - Cohort A: users with 4-8
|
|
232
232
|
* - Event: "order placed"
|
|
233
|
-
* - Measure:
|
|
234
|
-
* - Expected: C ~
|
|
233
|
+
* - Measure: Average of "order_total"
|
|
234
|
+
* - Expected: C ~ 0.65x order_total vs A (basket fatigue)
|
|
235
235
|
*
|
|
236
236
|
* REAL-WORLD ANALOGUE: Engaged orderers lift basket size; over-orderers
|
|
237
237
|
* hit fatigue and slow down.
|
|
238
238
|
*
|
|
239
239
|
* ───────────────────────────────────────────────────────────────────────────────
|
|
240
|
+
* 9. ORDER LIFECYCLE TTC (everything)
|
|
241
|
+
* ───────────────────────────────────────────────────────────────────────────────
|
|
242
|
+
*
|
|
243
|
+
* PATTERN: QuickBite+ users get delivery timing properties scaled 0.67x
|
|
244
|
+
* (faster), Free users get 1.4x (slower). Affects actual_delivery_mins,
|
|
245
|
+
* eta_mins, delivery_time_est_mins. No flag — discover via property avg
|
|
246
|
+
* breakdown by subscription_tier.
|
|
247
|
+
*
|
|
248
|
+
* HOW TO FIND IT IN MIXPANEL:
|
|
249
|
+
*
|
|
250
|
+
* Report 1: Avg Delivery Time by Subscription Tier
|
|
251
|
+
* - Report type: Insights
|
|
252
|
+
* - Event: "order delivered"
|
|
253
|
+
* - Measure: Average of "actual_delivery_mins"
|
|
254
|
+
* - Breakdown: "subscription_tier"
|
|
255
|
+
* - Expected: QuickBite+ ~ 0.67x Free
|
|
256
|
+
*
|
|
257
|
+
* Report 2: Avg ETA by Subscription Tier
|
|
258
|
+
* - Report type: Insights
|
|
259
|
+
* - Event: "order tracked"
|
|
260
|
+
* - Measure: Average of "eta_mins"
|
|
261
|
+
* - Breakdown: "subscription_tier"
|
|
262
|
+
* - Expected: QuickBite+ ~ 0.67x Free
|
|
263
|
+
*
|
|
264
|
+
* REAL-WORLD ANALOGUE: Premium subscribers get priority dispatch and faster
|
|
265
|
+
* delivery routing.
|
|
266
|
+
*
|
|
267
|
+
* ───────────────────────────────────────────────────────────────────────────────
|
|
268
|
+
* 10. CITY DENSITY REORDER BOOST (funnel-pre)
|
|
269
|
+
* ───────────────────────────────────────────────────────────────────────────────
|
|
270
|
+
*
|
|
271
|
+
* PATTERN: On the reorder funnel (order delivered → order rated → reorder
|
|
272
|
+
* initiated), dense cities (SF, NYC) convert at 1.4x; sprawl cities
|
|
273
|
+
* (Houston, Phoenix) at 0.7x. Scoped to the funnel containing
|
|
274
|
+
* "reorder initiated".
|
|
275
|
+
*
|
|
276
|
+
* HOW TO FIND IT IN MIXPANEL:
|
|
277
|
+
*
|
|
278
|
+
* Report 1: Reorder Funnel Conversion by City
|
|
279
|
+
* - Report type: Funnels
|
|
280
|
+
* - Steps: "order delivered" → "order rated" → "reorder initiated"
|
|
281
|
+
* - Breakdown: "city"
|
|
282
|
+
* - Expected: SF / NYC ~ 1.4x baseline; Houston / Phoenix ~ 0.7x
|
|
283
|
+
*
|
|
284
|
+
* REAL-WORLD ANALOGUE: Dense cities have more restaurant choice and
|
|
285
|
+
* faster delivery, driving higher repeat ordering behavior.
|
|
286
|
+
*
|
|
287
|
+
* ───────────────────────────────────────────────────────────────────────────────
|
|
240
288
|
* EXPECTED METRICS SUMMARY
|
|
241
289
|
* ───────────────────────────────────────────────────────────────────────────────
|
|
242
290
|
*
|
|
@@ -250,7 +298,11 @@ const chance = u.initChance(SEED);
|
|
|
250
298
|
* Trial Conversion | post-day-14 activity | 1x | ~ 0.4x | -60%
|
|
251
299
|
* First Order Bonus | returning conversion | 1x | 0.7x | -30%
|
|
252
300
|
* Order-Count Magic Num | sweet order_total | 1x | 1.4x | 1.4x
|
|
253
|
-
* Order-Count Magic Num | over
|
|
301
|
+
* Order-Count Magic Num | over order_total | 1x | 0.65x | -35%
|
|
302
|
+
* Order Lifecycle TTC | QB+ delivery_mins | 1x | 0.67x | -33%
|
|
303
|
+
* Order Lifecycle TTC | Free delivery_mins | 1x | 1.4x | +40%
|
|
304
|
+
* City Density Reorder | SF/NYC reorder conv | 1x | 1.4x | 1.4x
|
|
305
|
+
* City Density Reorder | HOU/PHX reorder conv | 1x | 0.7x | -30%
|
|
254
306
|
*/
|
|
255
307
|
|
|
256
308
|
// Generate consistent IDs for lookup tables and event properties
|
|
@@ -593,23 +645,17 @@ const config = {
|
|
|
593
645
|
lookupTables: [],
|
|
594
646
|
|
|
595
647
|
hook: function (record, type, meta) {
|
|
596
|
-
// HOOK
|
|
597
|
-
//
|
|
598
|
-
// (
|
|
599
|
-
if (type === "funnel-
|
|
600
|
-
const
|
|
601
|
-
if (
|
|
602
|
-
const
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
if (factor !== 1.0) {
|
|
608
|
-
for (let i = 1; i < record.length; i++) {
|
|
609
|
-
const prev = dayjs(record[i - 1].time);
|
|
610
|
-
const newGap = Math.round(dayjs(record[i].time).diff(prev) * factor);
|
|
611
|
-
record[i].time = prev.add(newGap, "milliseconds").toISOString();
|
|
612
|
-
}
|
|
648
|
+
// HOOK 10: CITY DENSITY REORDER BOOST (funnel-pre)
|
|
649
|
+
// Dense cities (SF, NYC) convert 1.4x on the reorder funnel;
|
|
650
|
+
// sprawl cities (Houston, Phoenix) at 0.7x.
|
|
651
|
+
if (type === "funnel-pre") {
|
|
652
|
+
const isReorderFunnel = meta.funnel?.sequence?.includes("reorder initiated");
|
|
653
|
+
if (isReorderFunnel) {
|
|
654
|
+
const city = meta.profile?.city;
|
|
655
|
+
if (city === "San Francisco" || city === "New York") {
|
|
656
|
+
record.conversionRate = Math.min(95, Math.round(record.conversionRate * 1.4));
|
|
657
|
+
} else if (city === "Houston" || city === "Phoenix") {
|
|
658
|
+
record.conversionRate = Math.round(record.conversionRate * 0.7);
|
|
613
659
|
}
|
|
614
660
|
}
|
|
615
661
|
}
|
|
@@ -630,6 +676,42 @@ const config = {
|
|
|
630
676
|
});
|
|
631
677
|
}
|
|
632
678
|
|
|
679
|
+
// HOOK 9 (TTC): ORDER LIFECYCLE TIME-TO-CONVERT (everything)
|
|
680
|
+
// QuickBite+ users experience faster delivery times (0.67x);
|
|
681
|
+
// Free users experience slower delivery times (1.4x).
|
|
682
|
+
// Scales timing properties: actual_delivery_mins, eta_mins,
|
|
683
|
+
// delivery_time_est_mins. Discover via avg(actual_delivery_mins)
|
|
684
|
+
// on "order delivered" broken down by subscription_tier.
|
|
685
|
+
if (profile) {
|
|
686
|
+
const tier = profile.subscription_tier;
|
|
687
|
+
const ttcFactor = (
|
|
688
|
+
tier === "QuickBite+" ? 0.67 :
|
|
689
|
+
tier === "Free" ? 1.4 :
|
|
690
|
+
1.0
|
|
691
|
+
);
|
|
692
|
+
if (ttcFactor !== 1.0) {
|
|
693
|
+
// Timestamp shift: affects Mixpanel funnel TTC
|
|
694
|
+
const orderSeq = findFirstSequence(
|
|
695
|
+
userEvents,
|
|
696
|
+
["checkout started", "order placed", "order tracked", "order delivered"],
|
|
697
|
+
60 * 24 * 7
|
|
698
|
+
);
|
|
699
|
+
if (orderSeq) scaleFunnelTTC(orderSeq, ttcFactor);
|
|
700
|
+
// Property scale: affects Insights AVG reports
|
|
701
|
+
userEvents.forEach(e => {
|
|
702
|
+
if (typeof e.actual_delivery_mins === "number") {
|
|
703
|
+
e.actual_delivery_mins = Math.round(e.actual_delivery_mins * ttcFactor);
|
|
704
|
+
}
|
|
705
|
+
if (typeof e.eta_mins === "number") {
|
|
706
|
+
e.eta_mins = Math.round(e.eta_mins * ttcFactor);
|
|
707
|
+
}
|
|
708
|
+
if (typeof e.delivery_time_est_mins === "number") {
|
|
709
|
+
e.delivery_time_est_mins = Math.round(e.delivery_time_est_mins * ttcFactor);
|
|
710
|
+
}
|
|
711
|
+
});
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
|
|
633
715
|
// HOOK 3: LATE NIGHT MUNCHIES — 10PM-2AM UTC, 70% of restaurant
|
|
634
716
|
// views/cart additions get cuisine flipped to American, 1.3x
|
|
635
717
|
// item_price. Mutates existing props. No flag — discover via
|
|
@@ -694,7 +776,7 @@ const config = {
|
|
|
694
776
|
}
|
|
695
777
|
}
|
|
696
778
|
|
|
697
|
-
// HOOK
|
|
779
|
+
// HOOK 7: FIRST ORDER BONUS — hash-based ~50% of users (returning)
|
|
698
780
|
// drop 30% of order delivered events. New users keep all.
|
|
699
781
|
// Discover via cohort builder on hash bucket vs conversion.
|
|
700
782
|
const hashUser = userEvents[0] && userEvents[0].user_id;
|
|
@@ -782,7 +864,7 @@ const config = {
|
|
|
782
864
|
});
|
|
783
865
|
if (rainyDuplicates.length > 0) userEvents.push(...rainyDuplicates);
|
|
784
866
|
|
|
785
|
-
// HOOK
|
|
867
|
+
// HOOK 8: ORDER-COUNT MAGIC NUMBER (no flags)
|
|
786
868
|
// Sweet 4-8 orders → +40% on order_total. Over 9+ → drop 35% of
|
|
787
869
|
// order placed events (oversaturated; analyst sees inverted-U).
|
|
788
870
|
if (orderPlacedCount >= 4 && orderPlacedCount <= 8) {
|
|
@@ -172,8 +172,8 @@ const chance = u.initChance(SEED);
|
|
|
172
172
|
* 9. GOLD REWARD BY LEVEL (PROGRESSION SCALING — everything)
|
|
173
173
|
*
|
|
174
174
|
* PATTERN: Quest gold reward scales with player level using
|
|
175
|
-
* reward_gold *= (1 + level * 0.
|
|
176
|
-
*
|
|
175
|
+
* reward_gold *= (1 + level * 0.15). Level-10 earns ~2.5x, level-20
|
|
176
|
+
* earns ~4x vs level-1. No flag — discover via user-property level breakdown.
|
|
177
177
|
*
|
|
178
178
|
* HOW TO FIND IT IN MIXPANEL:
|
|
179
179
|
*
|
|
@@ -182,7 +182,7 @@ const chance = u.initChance(SEED);
|
|
|
182
182
|
* - Event: "quest turned in"
|
|
183
183
|
* - Measure: Average of "reward_gold"
|
|
184
184
|
* - Breakdown: user property "level" (bucketed)
|
|
185
|
-
* - Expected: linear ramp; level-10 ~
|
|
185
|
+
* - Expected: linear ramp; level-10 ~ 2.5x, level-20 ~ 4x vs level-1
|
|
186
186
|
*
|
|
187
187
|
* REAL-WORLD ANALOGUE: Quest economies scale rewards with player level
|
|
188
188
|
* so high-level zones remain meaningfully lucrative.
|
|
@@ -235,10 +235,12 @@ const chance = u.initChance(SEED);
|
|
|
235
235
|
* audience segmentation lens for narrative design and content tuning.
|
|
236
236
|
*
|
|
237
237
|
* ───────────────────────────────────────────────────────────────────────────────
|
|
238
|
-
* 12. COMBAT FUNNEL TIME-TO-CONVERT (
|
|
238
|
+
* 12. COMBAT FUNNEL TIME-TO-CONVERT (everything)
|
|
239
239
|
*
|
|
240
|
-
* PATTERN: Elite tier completes the Combat funnel
|
|
241
|
-
* 0.
|
|
240
|
+
* PATTERN: Elite tier completes the Combat funnel ~3.3x faster (factor
|
|
241
|
+
* 0.30); Free tier ~1.4x slower (factor 1.40). Finds combat funnel
|
|
242
|
+
* sequences (combat initiated → combat completed → use item) and scales
|
|
243
|
+
* the inter-step time gaps in the everything hook.
|
|
242
244
|
*
|
|
243
245
|
* HOW TO FIND IT IN MIXPANEL:
|
|
244
246
|
*
|
|
@@ -246,14 +248,7 @@ const chance = u.initChance(SEED);
|
|
|
246
248
|
* - Funnels > "combat initiated" -> "combat completed" -> "use item"
|
|
247
249
|
* - Measure: Median time to convert
|
|
248
250
|
* - Breakdown: subscription_tier
|
|
249
|
-
* - Expected: Elite ~ 0.
|
|
250
|
-
*
|
|
251
|
-
* NOTE (funnel-post measurement): visible only via Mixpanel funnel
|
|
252
|
-
* median TTC. Cross-event MIN→MIN SQL queries on raw events do NOT
|
|
253
|
-
* show this — funnel-post adjusts gaps within funnel instances, not
|
|
254
|
-
* across the user's full event history. (This dungeon also has an
|
|
255
|
-
* everything-hook companion that compresses the cross-event Elite gap
|
|
256
|
-
* between earliest combat_initiated and earliest use_item.)
|
|
251
|
+
* - Expected: Elite ~ 0.30x; Free ~ 1.40x vs Premium baseline
|
|
257
252
|
*
|
|
258
253
|
* ───────────────────────────────────────────────────────────────────────────────
|
|
259
254
|
* 13. COMBAT-PREP MAGIC NUMBER (in-funnel, everything)
|
|
@@ -296,10 +291,10 @@ const chance = u.initChance(SEED);
|
|
|
296
291
|
* Legendary Weapon | Combat win rate | 60% | 90% | 1.5x
|
|
297
292
|
* Premium Tier | Quest reward | 1x | 1.4x | 1.4x
|
|
298
293
|
* Elite Tier | Quest reward | 1x | 1.8x | 1.8x
|
|
299
|
-
* Gold Scaling (lvl 10) | Avg quest gold | ~ 100 | ~
|
|
294
|
+
* Gold Scaling (lvl 10) | Avg quest gold | ~ 100 | ~ 250 | 2.5x
|
|
300
295
|
* Whale Purchases | Avg real money spend | 1x | 1.8x | 1.8x
|
|
301
296
|
* Hero/Villain/Neutral | User share | -- | 22/22/56% | n/a
|
|
302
|
-
* Combat T2C | median min by tier | 1x | 0.
|
|
297
|
+
* Combat T2C | median min by tier | 1x | 0.30/1.40x | ~ 4.7x range
|
|
303
298
|
* Combat-Prep Magic Num | sweet treasure_value | 1x | 1.3x | 1.3x
|
|
304
299
|
* Combat-Prep Magic Num | over boss victory | 1x | 0.75x | -25%
|
|
305
300
|
*/
|
|
@@ -695,7 +690,7 @@ const config = {
|
|
|
695
690
|
lookupTables: [],
|
|
696
691
|
|
|
697
692
|
/**
|
|
698
|
-
* 🎯 ARCHITECTED ANALYTICS HOOKS —
|
|
693
|
+
* 🎯 ARCHITECTED ANALYTICS HOOKS — 13 patterns
|
|
699
694
|
*
|
|
700
695
|
* 1. CONVERSION: Ancient Compass users have 3x quest completion + 1.5x rewards
|
|
701
696
|
* 2. TIME-BASED: "Cursed Week" (days 40-47) has 5x death rates
|
|
@@ -705,9 +700,11 @@ const config = {
|
|
|
705
700
|
* 6. BEHAVIORS TOGETHER: inspect + search before dungeon = 85% completion vs 45%
|
|
706
701
|
* 7. TIMED RELEASE: Legendary weapon released day 45, early adopters dominate
|
|
707
702
|
* 8. SUBSCRIPTION TIER: Premium/Elite users have higher engagement and success
|
|
708
|
-
* 9. PROGRESSION SCALING: Quest gold scales with player level (1 + level * 0.
|
|
703
|
+
* 9. PROGRESSION SCALING: Quest gold scales with player level (1 + level * 0.15)
|
|
709
704
|
* 10. WHALE PURCHASES: ~33% of users via deterministic hash spend 1.8x more
|
|
710
705
|
* 11. ALIGNMENT ARCHETYPE: Good=hero, Evil=villain, other=neutral (user hook)
|
|
706
|
+
* 12. COMBAT FUNNEL TTC: Elite 0.30x faster, Free 1.40x slower combat funnel T2C
|
|
707
|
+
* 13. COMBAT-PREP MAGIC NUMBER: 3-6 prep events = +30% treasure; 7+ = -25% boss wins
|
|
711
708
|
*/
|
|
712
709
|
hook: function (record, type, meta) {
|
|
713
710
|
// Hook #11: ALIGNMENT ARCHETYPE — derive archetype on user profile
|
|
@@ -727,27 +724,8 @@ const config = {
|
|
|
727
724
|
// (moved to everything hook — event hook fires before bunchIntoSessions
|
|
728
725
|
// reshuffles timestamps, causing tagged events to leak across the d45 boundary)
|
|
729
726
|
|
|
730
|
-
// HOOK 12 (T2C):
|
|
731
|
-
//
|
|
732
|
-
// Free tier 1.25x slower (factor 1.25).
|
|
733
|
-
if (type === "funnel-post") {
|
|
734
|
-
const segment = meta?.profile?.subscription_tier;
|
|
735
|
-
if (Array.isArray(record) && record.length > 1) {
|
|
736
|
-
const factor = (
|
|
737
|
-
segment === "Elite" ? 0.30 :
|
|
738
|
-
segment === "Premium" ? 0.70 :
|
|
739
|
-
segment === "Free" ? 1.40 :
|
|
740
|
-
1.0
|
|
741
|
-
);
|
|
742
|
-
if (factor !== 1.0) {
|
|
743
|
-
for (let i = 1; i < record.length; i++) {
|
|
744
|
-
const prev = dayjs(record[i - 1].time);
|
|
745
|
-
const newGap = Math.round(dayjs(record[i].time).diff(prev) * factor);
|
|
746
|
-
record[i].time = prev.add(newGap, "milliseconds").toISOString();
|
|
747
|
-
}
|
|
748
|
-
}
|
|
749
|
-
}
|
|
750
|
-
}
|
|
727
|
+
// HOOK 12 (T2C): moved to everything hook — funnel-post effects
|
|
728
|
+
// were invisible to SQL verification.
|
|
751
729
|
|
|
752
730
|
// Hooks #1, #3, #4, #5, #6, #8, #9, #10: per-user behavioral patterns
|
|
753
731
|
if (type === "everything") {
|
|
@@ -772,10 +750,10 @@ const config = {
|
|
|
772
750
|
}
|
|
773
751
|
});
|
|
774
752
|
|
|
775
|
-
// Hook #12
|
|
776
|
-
//
|
|
777
|
-
//
|
|
778
|
-
//
|
|
753
|
+
// Hook #12: COMBAT T2C — scale time gaps in combat funnel
|
|
754
|
+
// sequences (combat initiated → combat completed → use item).
|
|
755
|
+
// Elite ~0.30x (faster), Premium ~0.70x, Free ~1.40x (slower).
|
|
756
|
+
// Finds all 3-step sequences and shifts step 2/3 timestamps.
|
|
779
757
|
const tier = profile.subscription_tier;
|
|
780
758
|
const t2cFactor = (
|
|
781
759
|
tier === "Elite" ? 0.30 :
|
|
@@ -784,23 +762,34 @@ const config = {
|
|
|
784
762
|
1.0
|
|
785
763
|
);
|
|
786
764
|
if (t2cFactor !== 1.0) {
|
|
787
|
-
|
|
788
|
-
|
|
765
|
+
// Collect indices for each combat funnel step
|
|
766
|
+
const combatInitiated = [];
|
|
767
|
+
const combatCompleted = [];
|
|
768
|
+
const useItem = [];
|
|
789
769
|
for (let i = 0; i < userEvents.length; i++) {
|
|
790
770
|
const e = userEvents[i];
|
|
791
|
-
if (e.event === "combat initiated"
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
if (e.event === "use item" && firstUseItemIdx === -1) {
|
|
795
|
-
firstUseItemIdx = i;
|
|
796
|
-
}
|
|
771
|
+
if (e.event === "combat initiated") combatInitiated.push(i);
|
|
772
|
+
else if (e.event === "combat completed") combatCompleted.push(i);
|
|
773
|
+
else if (e.event === "use item") useItem.push(i);
|
|
797
774
|
}
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
775
|
+
// Match sequences: for each combat initiated, find next
|
|
776
|
+
// combat completed after it, then next use item after that
|
|
777
|
+
const matched = new Set();
|
|
778
|
+
for (const ciIdx of combatInitiated) {
|
|
779
|
+
const ccIdx = combatCompleted.find(j => j > ciIdx && !matched.has(j));
|
|
780
|
+
if (ccIdx === undefined) continue;
|
|
781
|
+
const uiIdx = useItem.find(j => j > ccIdx && !matched.has(j));
|
|
782
|
+
if (uiIdx === undefined) continue;
|
|
783
|
+
matched.add(ccIdx);
|
|
784
|
+
matched.add(uiIdx);
|
|
785
|
+
// Scale gap between step 1→2 and 2→3
|
|
786
|
+
const t0 = dayjs(userEvents[ciIdx].time);
|
|
787
|
+
const t1 = dayjs(userEvents[ccIdx].time);
|
|
788
|
+
const t2 = dayjs(userEvents[uiIdx].time);
|
|
789
|
+
const gap1 = t1.diff(t0);
|
|
790
|
+
const gap2 = t2.diff(t1);
|
|
791
|
+
userEvents[ccIdx].time = t0.add(Math.round(gap1 * t2cFactor), 'milliseconds').toISOString();
|
|
792
|
+
userEvents[uiIdx].time = dayjs(userEvents[ccIdx].time).add(Math.round(gap2 * t2cFactor), 'milliseconds').toISOString();
|
|
804
793
|
}
|
|
805
794
|
}
|
|
806
795
|
|
|
@@ -863,9 +852,10 @@ const config = {
|
|
|
863
852
|
const eventTime = dayjs(event.time);
|
|
864
853
|
|
|
865
854
|
// Hook 9: PROGRESSION SCALING — Quest gold scales with level.
|
|
855
|
+
// Power curve so bucket-averaged high-level/low-level ratio >= 2.0x.
|
|
866
856
|
if (event.event === "quest turned in") {
|
|
867
857
|
const baseGold = event.reward_gold || 100;
|
|
868
|
-
event.reward_gold = Math.floor(baseGold * (1 + userLevel * 0.
|
|
858
|
+
event.reward_gold = Math.floor(baseGold * (1 + userLevel * 0.15));
|
|
869
859
|
}
|
|
870
860
|
|
|
871
861
|
// Hook 1: CONVERSION — Ancient Compass users earn 1.5x quest
|