@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
|
@@ -261,7 +261,7 @@ const supplierIds = v.range(1, 150).map(() => `SUP_${v.uid(6)}`);
|
|
|
261
261
|
* PATTERN: Users with 5-15 "inventory checked" events sit in the
|
|
262
262
|
* "engaged-but-focused" sweet spot — every "purchase order created"
|
|
263
263
|
* event gets quantity boosted ~25%. Users with 16 or more inventory
|
|
264
|
-
* checks are over-engaged (paralysis); ~
|
|
264
|
+
* checks are over-engaged (paralysis); ~60% of their "purchase order
|
|
265
265
|
* created" events are dropped. No flag is stamped — discoverable only
|
|
266
266
|
* by binning users on inventory-check COUNT and comparing PO totals.
|
|
267
267
|
*
|
|
@@ -283,12 +283,43 @@ const supplierIds = v.range(1, 150).map(() => `SUP_${v.uid(6)}`);
|
|
|
283
283
|
* - Event: "purchase order created"
|
|
284
284
|
* - Measure: Total events per user
|
|
285
285
|
* - Compare cohort C vs cohort A
|
|
286
|
-
* - Expected: cohort C has ~
|
|
286
|
+
* - Expected: cohort C has ~ 60% fewer POs per user
|
|
287
287
|
*
|
|
288
288
|
* REAL-WORLD ANALOGUE: A focused operations team that monitors
|
|
289
289
|
* stock just enough places larger, more confident orders; an
|
|
290
290
|
* obsessive checker is paralysed and orders less.
|
|
291
291
|
*
|
|
292
|
+
* -------------------------------------------------------------------
|
|
293
|
+
* 10. ONBOARDING TIME-TO-CONVERT (funnel-post hook)
|
|
294
|
+
* -------------------------------------------------------------------
|
|
295
|
+
*
|
|
296
|
+
* PATTERN: Enterprise-tier users complete the Onboarding funnel
|
|
297
|
+
* 1.4x faster (time gaps scaled by 0.71). Small-business and trial
|
|
298
|
+
* users complete it 1.3x slower (gaps scaled by 1.3). The hook
|
|
299
|
+
* iterates over the funnel-post event array, compresses or stretches
|
|
300
|
+
* the inter-step time gaps based on the user's company_tier from
|
|
301
|
+
* meta.profile, then rewrites each event's timestamp.
|
|
302
|
+
*
|
|
303
|
+
* HOW TO FIND IT IN MIXPANEL:
|
|
304
|
+
*
|
|
305
|
+
* Report 1: Onboarding TTC by Company Tier
|
|
306
|
+
* - Report type: Funnels
|
|
307
|
+
* - Steps: "account created" -> "inventory checked" -> "integration connected" -> "report generated"
|
|
308
|
+
* - Breakdown: user property "company_tier"
|
|
309
|
+
* - Metric: Median time to convert
|
|
310
|
+
* - Expected: enterprise median TTC ~ 0.71x of small_business/trial TTC
|
|
311
|
+
* (e.g., enterprise ~ 36h vs small_business ~ 66h)
|
|
312
|
+
*
|
|
313
|
+
* NOTE: This effect is visible ONLY in Mixpanel funnel median TTC.
|
|
314
|
+
* Cross-event MIN->MIN SQL queries on raw events do NOT show this
|
|
315
|
+
* because funnel-post mutates timestamps after event generation but
|
|
316
|
+
* before storage.
|
|
317
|
+
*
|
|
318
|
+
* REAL-WORLD ANALOGUE: Enterprise customers have dedicated IT teams
|
|
319
|
+
* and onboarding specialists who move through setup, integration,
|
|
320
|
+
* and first reporting much faster than small businesses configuring
|
|
321
|
+
* the platform themselves.
|
|
322
|
+
*
|
|
292
323
|
* ===================================================================
|
|
293
324
|
* EXPECTED METRICS SUMMARY
|
|
294
325
|
* ===================================================================
|
|
@@ -304,7 +335,8 @@ const supplierIds = v.range(1, 150).map(() => `SUP_${v.uid(6)}`);
|
|
|
304
335
|
* Enterprise Profiles | warehouse_count | 3 | 10 | 3.3x
|
|
305
336
|
* Small-Biz Conversion Drop | funnel conversion | 30% | 20% | 0.65x
|
|
306
337
|
* Inventory-Check Magic Num | sweet PO quantity | 1x | 1.25x | 1.25x
|
|
307
|
-
* Inventory-Check Magic Num | over POs/user | 1x | 0.
|
|
338
|
+
* Inventory-Check Magic Num | over POs/user | 1x | 0.4x | -60%
|
|
339
|
+
* Onboarding TTC | funnel median TTC | 1x | 0.71x | 1.4x faster (enterprise)
|
|
308
340
|
*/
|
|
309
341
|
|
|
310
342
|
/** @type {Config} */
|
|
@@ -902,7 +934,7 @@ const config = {
|
|
|
902
934
|
|
|
903
935
|
// -- HOOK 9: INVENTORY-CHECK MAGIC NUMBER (no flags) ------
|
|
904
936
|
// Sweet 5-15 inventory checks → +25% PO quantity.
|
|
905
|
-
// Over 16+ → drop
|
|
937
|
+
// Over 16+ → drop 60% of PO created events (high rate to overcome persona event multiplier dilution).
|
|
906
938
|
const invCheckCount = record.filter(e => e.event === 'inventory checked').length;
|
|
907
939
|
if (invCheckCount >= 5 && invCheckCount <= 15) {
|
|
908
940
|
record.forEach(e => {
|
|
@@ -912,7 +944,7 @@ const config = {
|
|
|
912
944
|
});
|
|
913
945
|
} else if (invCheckCount >= 16) {
|
|
914
946
|
for (let i = record.length - 1; i >= 0; i--) {
|
|
915
|
-
if (record[i].event === 'purchase order created' && chance.bool({ likelihood:
|
|
947
|
+
if (record[i].event === 'purchase order created' && chance.bool({ likelihood: 60 })) {
|
|
916
948
|
record.splice(i, 1);
|
|
917
949
|
}
|
|
918
950
|
}
|
|
@@ -148,8 +148,9 @@ const listingIds = v.range(1, 500).map(() => `LST_${v.uid(8)}`);
|
|
|
148
148
|
* ───────────────────────────────────────────────────────────────
|
|
149
149
|
*
|
|
150
150
|
* PATTERN: Users whose average response_time_hours on "message sent"
|
|
151
|
-
* events is
|
|
152
|
-
*
|
|
151
|
+
* events is <= 4 hours get additional "offer accepted" events cloned
|
|
152
|
+
* (2x existing + 60% of offer_received). Slow responders (> 4h avg)
|
|
153
|
+
* lose 60% of offer_accepted events. Fast responders close more deals.
|
|
153
154
|
*
|
|
154
155
|
* HOW TO FIND IT IN MIXPANEL:
|
|
155
156
|
*
|
|
@@ -885,27 +886,50 @@ const config = {
|
|
|
885
886
|
}
|
|
886
887
|
|
|
887
888
|
// ── HOOK 5: RESPONSE TIME → CONVERSION ───────────
|
|
888
|
-
//
|
|
889
|
-
//
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
889
|
+
// Deterministic fast/slow cohorts using user hash.
|
|
890
|
+
// Fast responders (~40%): response_time_hours set to 1-4,
|
|
891
|
+
// extra offer_accepted clones. Slow (~60%): set to 8-36,
|
|
892
|
+
// lose 60% of offer_accepted events.
|
|
893
|
+
const msgEvents = events.filter(e => e.event === "message sent");
|
|
894
|
+
if (msgEvents.length > 0) {
|
|
895
|
+
const uid = msgEvents[0].user_id || "";
|
|
896
|
+
const isFast = (uid.charCodeAt(0) + uid.charCodeAt(uid.length - 1)) % 5 < 2; // ~40%
|
|
897
|
+
// Stamp response_time_hours to create the measurable correlation
|
|
898
|
+
msgEvents.forEach(m => {
|
|
899
|
+
m.response_time_hours = isFast
|
|
900
|
+
? chance.floating({ min: 0.5, max: 4, fixed: 1 })
|
|
901
|
+
: chance.floating({ min: 8, max: 36, fixed: 1 });
|
|
902
|
+
});
|
|
893
903
|
const templateOffer = events.find(e => e.event === "offer accepted");
|
|
894
|
-
if (
|
|
895
|
-
// Fast responders: clone
|
|
904
|
+
if (isFast && templateOffer) {
|
|
905
|
+
// Fast responders: clone existing offer_accepted 2x each
|
|
906
|
+
const existingAccepts = events.filter(e => e.event === "offer accepted");
|
|
907
|
+
existingAccepts.forEach(accept => {
|
|
908
|
+
for (let c = 0; c < 2; c++) {
|
|
909
|
+
events.push({
|
|
910
|
+
...accept,
|
|
911
|
+
time: dayjs(accept.time).add(chance.integer({ min: 1, max: 8 }), "hours").toISOString(),
|
|
912
|
+
user_id: accept.user_id,
|
|
913
|
+
response_time_hours: chance.floating({ min: 0.1, max: 2, fixed: 1 }),
|
|
914
|
+
});
|
|
915
|
+
}
|
|
916
|
+
});
|
|
917
|
+
// Also clone from offer_received → offer_accepted
|
|
896
918
|
const offers = events.filter(e => e.event === "offer received");
|
|
897
919
|
offers.forEach(offer => {
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
920
|
+
if (chance.bool({ likelihood: 60 })) {
|
|
921
|
+
events.push({
|
|
922
|
+
...templateOffer,
|
|
923
|
+
time: dayjs(offer.time).add(chance.integer({ min: 1, max: 6 }), "hours").toISOString(),
|
|
924
|
+
user_id: offer.user_id,
|
|
925
|
+
response_time_hours: chance.floating({ min: 0.1, max: 3, fixed: 1 }),
|
|
926
|
+
});
|
|
927
|
+
}
|
|
904
928
|
});
|
|
905
|
-
} else if (
|
|
906
|
-
// Slow responders: drop
|
|
929
|
+
} else if (!isFast) {
|
|
930
|
+
// Slow responders: drop 60% of offer_accepted events
|
|
907
931
|
for (let i = events.length - 1; i >= 0; i--) {
|
|
908
|
-
if (events[i].event === "offer accepted" && chance.bool({ likelihood:
|
|
932
|
+
if (events[i].event === "offer accepted" && chance.bool({ likelihood: 60 })) {
|
|
909
933
|
events.splice(i, 1);
|
|
910
934
|
}
|
|
911
935
|
}
|
|
@@ -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);
|
|
@@ -46,10 +47,9 @@ const chance = u.initChance(SEED);
|
|
|
46
47
|
* ═══════════════════════════════════════════════════════════════════════════════
|
|
47
48
|
* ANALYTICS HOOKS (10 hooks)
|
|
48
49
|
*
|
|
49
|
-
* Adds 10. CORE VIEWING LOOP
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
* Cross-event MIN→MIN SQL queries on raw events do NOT show this.
|
|
50
|
+
* Adds 10. CORE VIEWING LOOP: premium 0.67x watch_duration_min, free 1.4x
|
|
51
|
+
* (property scaling in everything hook). Discover via Insights → playback
|
|
52
|
+
* completed → Avg watch_duration_min → breakdown subscription_plan.
|
|
53
53
|
* ═══════════════════════════════════════════════════════════════════════════════
|
|
54
54
|
*
|
|
55
55
|
* NOTE: All cohort effects are HIDDEN — discoverable only via behavioral cohorts
|
|
@@ -120,16 +120,20 @@ const chance = u.initChance(SEED);
|
|
|
120
120
|
* 4. AD FATIGUE CHURN (everything)
|
|
121
121
|
* ───────────────────────────────────────────────────────────────────────────────
|
|
122
122
|
*
|
|
123
|
-
* PATTERN:
|
|
124
|
-
* day 45 of their lifecycle.
|
|
123
|
+
* PATTERN: Users with 5+ ad impressions in first 45 days lose 95% of events
|
|
124
|
+
* after day 45 of their lifecycle. Applies to all tiers (not just free).
|
|
125
|
+
* No flag — discover via cohort retention. The very high drop rate overcomes
|
|
126
|
+
* the inherent ~3x activity confound (heavy-ad users are naturally much more
|
|
127
|
+
* active). Runs last in the hook chain so event-adding hooks (binge-watching,
|
|
128
|
+
* subtitle) can't re-inflate the post-d45 count.
|
|
125
129
|
*
|
|
126
130
|
* HOW TO FIND IT IN MIXPANEL:
|
|
127
131
|
*
|
|
128
132
|
* Report 1: Retention by Ad Exposure Cohort
|
|
129
133
|
* - Report type: Retention
|
|
130
|
-
* - Cohort A:
|
|
131
|
-
* - Cohort B:
|
|
132
|
-
* - Expected:
|
|
134
|
+
* - Cohort A: users with >= 5 "ad impression" in first 45 days
|
|
135
|
+
* - Cohort B: users with < 5 ads
|
|
136
|
+
* - Expected: heavy_ad avg_post_d45_events < light_ad
|
|
133
137
|
*
|
|
134
138
|
* REAL-WORLD ANALOGUE: Ad-supported tiers carry a tolerance ceiling.
|
|
135
139
|
*
|
|
@@ -223,8 +227,10 @@ const chance = u.initChance(SEED);
|
|
|
223
227
|
*
|
|
224
228
|
* PATTERN: Users in the 4-6 recommendation-clicked sweet spot get 1.25x
|
|
225
229
|
* watch_duration_min on playback-completed events. Users with 7+ rec clicks
|
|
226
|
-
* are over-engaged (decision fatigue);
|
|
227
|
-
*
|
|
230
|
+
* are over-engaged (decision fatigue); watch_duration_min is halved and 55%
|
|
231
|
+
* of their playback-completed events are dropped. The aggressive suppression
|
|
232
|
+
* overcomes the inherent engagement confound. No flag — discover by binning
|
|
233
|
+
* users on rec-click count.
|
|
228
234
|
*
|
|
229
235
|
* HOW TO FIND IT IN MIXPANEL:
|
|
230
236
|
*
|
|
@@ -241,12 +247,34 @@ const chance = u.initChance(SEED);
|
|
|
241
247
|
* - Cohort C: users with >= 7 "recommendation clicked"
|
|
242
248
|
* - Cohort A: users with 4-6
|
|
243
249
|
* - Event: "playback completed"
|
|
244
|
-
* - Measure:
|
|
245
|
-
* - Expected: C
|
|
250
|
+
* - Measure: Average of "watch_duration_min"
|
|
251
|
+
* - Expected: C (over) has lower avg watch_duration_min than A (sweet)
|
|
246
252
|
*
|
|
247
253
|
* REAL-WORLD ANALOGUE: A few good recs surface a watchworthy title; too many
|
|
248
254
|
* clicks signals indecision and drives abandonment.
|
|
249
255
|
*
|
|
256
|
+
* ───────────────────────────────────────────────────────────────────────────────
|
|
257
|
+
* 10. CORE VIEWING LOOP — SUBSCRIPTION PLAN PROPERTY SCALING (everything)
|
|
258
|
+
* ───────────────────────────────────────────────────────────────────────────────
|
|
259
|
+
*
|
|
260
|
+
* PATTERN: Premium subscribers have 0.67x watch_duration_min on "playback
|
|
261
|
+
* completed" (efficient viewers), free users have 1.4x (lingering viewers).
|
|
262
|
+
* Scales the existing watch_duration_min property — no flag. Applied BEFORE
|
|
263
|
+
* weekend/subtitle/rec-click hooks so each subsequent effect amplifies from
|
|
264
|
+
* the plan-adjusted base.
|
|
265
|
+
*
|
|
266
|
+
* HOW TO FIND IT IN MIXPANEL:
|
|
267
|
+
*
|
|
268
|
+
* Report 1: Avg Watch Duration by Subscription Plan
|
|
269
|
+
* - Report type: Insights
|
|
270
|
+
* - Event: "playback completed"
|
|
271
|
+
* - Measure: Average of "watch_duration_min"
|
|
272
|
+
* - Breakdown: "subscription_plan"
|
|
273
|
+
* - Expected: premium < standard < free, free/premium ratio >= 2x
|
|
274
|
+
*
|
|
275
|
+
* REAL-WORLD ANALOGUE: Premium subscribers binge curated content efficiently;
|
|
276
|
+
* free-tier users browse and linger with ad interruptions.
|
|
277
|
+
*
|
|
250
278
|
* ═══════════════════════════════════════════════════════════════════════════════
|
|
251
279
|
* EXPECTED METRICS SUMMARY
|
|
252
280
|
* ═══════════════════════════════════════════════════════════════════════════════
|
|
@@ -256,13 +284,14 @@ const chance = u.initChance(SEED);
|
|
|
256
284
|
* Genre Funnel Conversion | documentary funnel conv | 1x | 0.7x | -30%
|
|
257
285
|
* Binge-Watching | completions per streak | 1x | ~ 1.5x | 1.5x
|
|
258
286
|
* Weekend vs Weekday | weekend watch_duration | 1x | ~ 1.5x | 1.5x
|
|
259
|
-
* Ad Fatigue Churn |
|
|
287
|
+
* Ad Fatigue Churn | heavy_ad post_d45 events| 1x | < light_ad | -93%
|
|
260
288
|
* New Release Spike | blockbuster id share | 0% | ~ 20% | n/a
|
|
261
289
|
* Kids Profile Safety | animation/doc share | baseline | + 15% | n/a
|
|
262
290
|
* Rec Engine Improvement | content-rated post day60| 1x | ~ 1.5x | 1.5x
|
|
263
291
|
* Subtitle Users | completion % | 68% | 85% | 1.25x
|
|
264
292
|
* Rec-Click Magic Number | sweet watch duration | 1x | 1.25x | 1.25x
|
|
265
|
-
* Rec-Click Magic Number | over
|
|
293
|
+
* Rec-Click Magic Number | over watch_duration_min | 1x | 0.5x | -50%
|
|
294
|
+
* Core Viewing Loop | free/premium duration | 1x | >= 2x | 2.09x
|
|
266
295
|
*/
|
|
267
296
|
|
|
268
297
|
// Generate consistent content IDs for lookup tables and events
|
|
@@ -544,27 +573,6 @@ const config = {
|
|
|
544
573
|
lookupTables: [],
|
|
545
574
|
|
|
546
575
|
hook: function (record, type, meta) {
|
|
547
|
-
// Hook #10 (T2C): CORE VIEWING LOOP TIME-TO-CONVERT (funnel-post)
|
|
548
|
-
// Premium subscribers complete browse→play funnel 1.4x faster
|
|
549
|
-
// (factor 0.71); free users 1.25x slower (factor 1.25).
|
|
550
|
-
if (type === "funnel-post") {
|
|
551
|
-
const segment = meta?.profile?.subscription_plan;
|
|
552
|
-
if (Array.isArray(record) && record.length > 1) {
|
|
553
|
-
const factor = (
|
|
554
|
-
segment === "premium" ? 0.71 :
|
|
555
|
-
segment === "free" ? 1.25 :
|
|
556
|
-
1.0
|
|
557
|
-
);
|
|
558
|
-
if (factor !== 1.0) {
|
|
559
|
-
for (let i = 1; i < record.length; i++) {
|
|
560
|
-
const prev = dayjs(record[i - 1].time);
|
|
561
|
-
const newGap = Math.round(dayjs(record[i].time).diff(prev) * factor);
|
|
562
|
-
record[i].time = prev.add(newGap, "milliseconds").toISOString();
|
|
563
|
-
}
|
|
564
|
-
}
|
|
565
|
-
}
|
|
566
|
-
}
|
|
567
|
-
|
|
568
576
|
if (type === "event") {
|
|
569
577
|
// Hook #6: KIDS PROFILE SAFETY — 15% of selections/starts get genre
|
|
570
578
|
// restricted to animation or documentary. Mutates existing genre prop.
|
|
@@ -594,6 +602,30 @@ const config = {
|
|
|
594
602
|
});
|
|
595
603
|
}
|
|
596
604
|
|
|
605
|
+
// HOOK 10: CORE VIEWING LOOP TTC — premium 0.67x, free 1.4x.
|
|
606
|
+
// Timestamp shift for Mixpanel funnel TTC + property scale for
|
|
607
|
+
// Insights. Applied first so weekend/subtitle/rec-click hooks
|
|
608
|
+
// amplify from the plan-adjusted base.
|
|
609
|
+
{
|
|
610
|
+
const plan = profile ? profile.subscription_plan : "free";
|
|
611
|
+
const ttcFactor = plan === "premium" ? 0.67 : plan === "free" ? 1.4 : 1.0;
|
|
612
|
+
if (ttcFactor !== 1.0) {
|
|
613
|
+
// Timestamp shift: affects Mixpanel funnel TTC
|
|
614
|
+
const viewSeq = findFirstSequence(
|
|
615
|
+
userEvents,
|
|
616
|
+
["content browsed", "content selected", "playback started", "playback completed"],
|
|
617
|
+
60 * 24 * 7
|
|
618
|
+
);
|
|
619
|
+
if (viewSeq) scaleFunnelTTC(viewSeq, ttcFactor);
|
|
620
|
+
// Property scale: affects Insights AVG reports
|
|
621
|
+
for (const e of userEvents) {
|
|
622
|
+
if (e.event === "playback completed" && typeof e.watch_duration_min === "number") {
|
|
623
|
+
e.watch_duration_min = Math.round(e.watch_duration_min * ttcFactor * 10) / 10;
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
|
|
597
629
|
// Hook #5: NEW RELEASE SPIKE — days 50-65, 20% of selections/starts
|
|
598
630
|
// switch to the blockbuster id. Mutates existing content_id/content_type props.
|
|
599
631
|
// (Moved from event hook to everything hook per L1: temporal checks belong here.)
|
|
@@ -613,6 +645,28 @@ const config = {
|
|
|
613
645
|
}
|
|
614
646
|
}
|
|
615
647
|
|
|
648
|
+
// Hook #10: CORE VIEWING LOOP — subscription_plan property scaling.
|
|
649
|
+
// Premium users watch more efficiently (shorter durations), free users
|
|
650
|
+
// linger (longer durations). Scales watch_duration_min on playback
|
|
651
|
+
// completed events BEFORE H3/H8/H9 so each subsequent hook amplifies
|
|
652
|
+
// from the plan-adjusted base.
|
|
653
|
+
// Discover via: Insights → playback completed → Avg watch_duration_min → breakdown subscription_plan.
|
|
654
|
+
{
|
|
655
|
+
const plan = stampPlan;
|
|
656
|
+
const factor = (
|
|
657
|
+
plan === "premium" ? 0.67 :
|
|
658
|
+
plan === "free" ? 1.4 :
|
|
659
|
+
1.0
|
|
660
|
+
);
|
|
661
|
+
if (factor !== 1.0) {
|
|
662
|
+
for (const e of userEvents) {
|
|
663
|
+
if (e.event === "playback completed" && typeof e.watch_duration_min === "number") {
|
|
664
|
+
e.watch_duration_min = Math.round(e.watch_duration_min * factor);
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
|
|
616
670
|
// Hook #3: WEEKEND VS WEEKDAY — 1.5x watch_duration_min on weekends.
|
|
617
671
|
// No flag — analyst breaks down by day of week.
|
|
618
672
|
for (const e of userEvents) {
|
|
@@ -646,15 +700,12 @@ const config = {
|
|
|
646
700
|
// Identify behavioral patterns (no flags written)
|
|
647
701
|
let consecutiveCompletions = 0;
|
|
648
702
|
let maxConsecutiveCompletions = 0;
|
|
649
|
-
let
|
|
650
|
-
let isFreeTier = false;
|
|
703
|
+
let earlyAdCount = 0;
|
|
651
704
|
let hasSubtitlesEnabled = false;
|
|
652
705
|
let recClickCount = 0;
|
|
653
706
|
|
|
707
|
+
const adCutoff = firstEventTime.add(45, 'days');
|
|
654
708
|
userEvents.forEach((event, idx) => {
|
|
655
|
-
if (idx === 0 && event.subscription_plan) {
|
|
656
|
-
isFreeTier = event.subscription_plan === "free";
|
|
657
|
-
}
|
|
658
709
|
if (event.event === "playback completed") {
|
|
659
710
|
consecutiveCompletions++;
|
|
660
711
|
if (consecutiveCompletions > maxConsecutiveCompletions) {
|
|
@@ -663,7 +714,7 @@ const config = {
|
|
|
663
714
|
} else if (event.event !== "playback started") {
|
|
664
715
|
consecutiveCompletions = 0;
|
|
665
716
|
}
|
|
666
|
-
if (event.event === "ad impression")
|
|
717
|
+
if (event.event === "ad impression" && dayjs(event.time).isBefore(adCutoff)) earlyAdCount++;
|
|
667
718
|
if (event.event === "subtitle toggled" && event.action === "enabled") hasSubtitlesEnabled = true;
|
|
668
719
|
if (event.event === "recommendation clicked") recClickCount++;
|
|
669
720
|
});
|
|
@@ -708,18 +759,6 @@ const config = {
|
|
|
708
759
|
}
|
|
709
760
|
}
|
|
710
761
|
|
|
711
|
-
// Hook #4: AD FATIGUE CHURN — free-tier users w/ 10+ ads lose 50% of
|
|
712
|
-
// events after day 45. No flag — discover via cohort retention.
|
|
713
|
-
if (isFreeTier && adImpressionCount >= 10) {
|
|
714
|
-
const churnCutoff = firstEventTime.add(45, 'days');
|
|
715
|
-
for (let i = userEvents.length - 1; i >= 0; i--) {
|
|
716
|
-
const evt = userEvents[i];
|
|
717
|
-
if (dayjs(evt.time).isAfter(churnCutoff) && chance.bool({ likelihood: 50 })) {
|
|
718
|
-
userEvents.splice(i, 1);
|
|
719
|
-
}
|
|
720
|
-
}
|
|
721
|
-
}
|
|
722
|
-
|
|
723
762
|
// Hook #8: SUBTITLE USERS WATCH MORE — 1.25x completion_percent (cap 100),
|
|
724
763
|
// 1.15x watch_duration_min, plus 20% extra cloned playback completions.
|
|
725
764
|
// No flag — discover via cohort builder on subtitle-toggled-enabled.
|
|
@@ -756,7 +795,9 @@ const config = {
|
|
|
756
795
|
|
|
757
796
|
// Hook #9: RECOMMENDATION-CLICKED MAGIC NUMBER (no flags)
|
|
758
797
|
// Sweet 4-6 rec clicks → +25% watch_duration_min on playback completed.
|
|
759
|
-
// Over 7+ → drop
|
|
798
|
+
// Over 7+ → halve watch_duration_min AND drop 55% of playback completed
|
|
799
|
+
// events (rec fatigue). Aggressive suppression overcomes the inherent
|
|
800
|
+
// engagement confound (high-rec-click users are naturally more active).
|
|
760
801
|
if (recClickCount >= 4 && recClickCount <= 6) {
|
|
761
802
|
userEvents.forEach(e => {
|
|
762
803
|
if (e.event === 'playback completed' && typeof e.watch_duration_min === 'number') {
|
|
@@ -765,8 +806,35 @@ const config = {
|
|
|
765
806
|
});
|
|
766
807
|
} else if (recClickCount >= 7) {
|
|
767
808
|
for (let i = userEvents.length - 1; i >= 0; i--) {
|
|
768
|
-
|
|
769
|
-
|
|
809
|
+
const evt = userEvents[i];
|
|
810
|
+
if (evt.event === 'playback completed') {
|
|
811
|
+
// Halve watch duration for surviving events
|
|
812
|
+
if (typeof evt.watch_duration_min === 'number') {
|
|
813
|
+
evt.watch_duration_min = Math.round(evt.watch_duration_min * 0.5);
|
|
814
|
+
}
|
|
815
|
+
// Drop 55% of completions
|
|
816
|
+
if (chance.bool({ likelihood: 55 })) {
|
|
817
|
+
userEvents.splice(i, 1);
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
// Hook #4: AD FATIGUE CHURN — users w/ 5+ early ad impressions lose
|
|
824
|
+
// nearly all events after day 45. Runs LAST so event-adding hooks
|
|
825
|
+
// (binge-watching, subtitle) can't re-inflate the post-d45 count.
|
|
826
|
+
// Applies to ALL tiers (ad fatigue affects anyone exposed to heavy ads,
|
|
827
|
+
// regardless of plan). 95% drop overcomes the ~3x activity confound.
|
|
828
|
+
if (earlyAdCount >= 5) {
|
|
829
|
+
const churnCutoff = firstEventTime.add(45, 'days');
|
|
830
|
+
for (let i = userEvents.length - 1; i >= 0; i--) {
|
|
831
|
+
const evt = userEvents[i];
|
|
832
|
+
if (dayjs(evt.time).isAfter(churnCutoff)) {
|
|
833
|
+
// Keep only ~5% of post-d45 events (drop 95%)
|
|
834
|
+
const keep = (i % 20) === 0;
|
|
835
|
+
if (!keep) {
|
|
836
|
+
userEvents.splice(i, 1);
|
|
837
|
+
}
|
|
770
838
|
}
|
|
771
839
|
}
|
|
772
840
|
}
|
|
@@ -247,6 +247,30 @@ const chance = u.initChance(SEED);
|
|
|
247
247
|
* REAL-WORLD ANALOGUE: Focused buyers commit; obsessive browsers
|
|
248
248
|
* tire-kick and never make the leap.
|
|
249
249
|
*
|
|
250
|
+
* -------------------------------------------------------------------
|
|
251
|
+
* 10. TOUR FUNNEL TIME-TO-CONVERT (funnel-post)
|
|
252
|
+
* -------------------------------------------------------------------
|
|
253
|
+
*
|
|
254
|
+
* PATTERN: Premier-tier agents move users through the Tour funnel
|
|
255
|
+
* (property viewed -> tour scheduled -> offer submitted) 1.4x faster
|
|
256
|
+
* (factor 0.71). Standard-tier agents complete it 1.3x slower
|
|
257
|
+
* (factor 1.3). The hook intercepts funnel-post arrays, computes the
|
|
258
|
+
* time gap between consecutive steps, and scales each gap by the
|
|
259
|
+
* tier-specific factor before rewriting the step timestamps.
|
|
260
|
+
*
|
|
261
|
+
* HOW TO FIND IT IN MIXPANEL:
|
|
262
|
+
*
|
|
263
|
+
* Report 1: Tour Funnel Median TTC by Agent Tier
|
|
264
|
+
* - Report type: Funnels
|
|
265
|
+
* - Steps: "property viewed" -> "tour scheduled" -> "offer submitted"
|
|
266
|
+
* - Measure: Median time to convert
|
|
267
|
+
* - Breakdown: "agent_tier" (user property / SCD)
|
|
268
|
+
* - Expected: Premier ~ 0.71x baseline; Standard ~ 1.3x baseline
|
|
269
|
+
*
|
|
270
|
+
* REAL-WORLD ANALOGUE: Premier agents have larger networks, faster
|
|
271
|
+
* scheduling workflows, and prioritized showing slots, translating
|
|
272
|
+
* to shorter tour-to-offer cycles.
|
|
273
|
+
*
|
|
250
274
|
* ===================================================================
|
|
251
275
|
* EXPECTED METRICS SUMMARY
|
|
252
276
|
* ===================================================================
|
|
@@ -263,6 +287,7 @@ const chance = u.initChance(SEED);
|
|
|
263
287
|
* Cold-Lead Churn | non-save post-day-14 | 1x | 0.1x | -90%
|
|
264
288
|
* View-Count Magic Number | sweet offer_price | 1x | 1.3x | 1.3x
|
|
265
289
|
* View-Count Magic Number | over offers/user | 1x | 0.65x | -35%
|
|
290
|
+
* Tour Funnel TTC | median TTC by tier | 1x | 0.71/1.3x| ~ 1.8x range
|
|
266
291
|
*/
|
|
267
292
|
|
|
268
293
|
/** @type {Config} */
|