@ak--47/dungeon-master 1.3.0 → 1.4.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/CHANGELOG.md +58 -0
- package/dungeons/technical/array-of-object-lookup.js +0 -2
- package/dungeons/technical/hook-helpers-verify.js +89 -0
- package/dungeons/technical/identity-model-verify.js +47 -0
- package/dungeons/technical/pattern-aggregate-by-bin.js +41 -0
- package/dungeons/technical/pattern-attributed-by-source.js +42 -0
- package/dungeons/technical/pattern-frequency-by-frequency.js +40 -0
- package/dungeons/technical/pattern-funnel-frequency.js +54 -0
- package/dungeons/technical/pattern-ttc-by-segment.js +45 -0
- package/dungeons/technical/simple.js +3 -4
- package/dungeons/technical/simplest-schema.json +5 -0
- package/dungeons/technical/text-generation.js +1 -1
- package/dungeons/vertical/ai-platform.js +216 -164
- package/dungeons/vertical/community.js +74 -41
- package/dungeons/vertical/crypto.js +343 -233
- package/dungeons/vertical/dating.js +246 -292
- package/dungeons/vertical/devtools.js +179 -70
- package/dungeons/vertical/ecommerce.js +275 -122
- package/dungeons/vertical/education.js +345 -312
- package/dungeons/vertical/fintech.js +445 -314
- package/dungeons/vertical/fitness.js +199 -165
- package/dungeons/vertical/food-delivery.js +309 -330
- package/dungeons/vertical/gaming.js +387 -241
- package/dungeons/vertical/healthcare.js +149 -66
- package/dungeons/vertical/insurance-application.js +173 -75
- package/dungeons/vertical/logistics.js +107 -12
- package/dungeons/vertical/marketplace.js +171 -76
- package/dungeons/vertical/media.js +259 -402
- package/dungeons/vertical/real-estate.js +336 -318
- package/dungeons/vertical/sass.js +294 -288
- package/dungeons/vertical/social.js +289 -234
- package/dungeons/vertical/travel.js +66 -20
- package/index.js +17 -17
- package/lib/core/config-validator.js +226 -175
- package/lib/core/context.js +10 -24
- package/lib/core/storage.js +6 -1
- package/lib/generators/events.js +57 -104
- package/lib/generators/funnels.js +212 -93
- package/lib/generators/mirror.js +3 -2
- package/lib/generators/product-names.js +1 -1
- package/lib/generators/scd.js +2 -1
- package/lib/generators/text.js +1 -1
- package/lib/hook-helpers/_internal.js +23 -0
- package/lib/hook-helpers/cohort.js +124 -0
- package/lib/hook-helpers/identity.js +56 -0
- package/lib/hook-helpers/index.js +44 -0
- package/lib/hook-helpers/inject.js +99 -0
- package/lib/hook-helpers/mutate.js +151 -0
- package/lib/hook-helpers/timing.js +99 -0
- package/lib/hook-patterns/aggregate-per-user-by-bin.js +38 -0
- package/lib/hook-patterns/attributed-by-source.js +72 -0
- package/lib/hook-patterns/frequency-by-frequency.js +46 -0
- package/lib/hook-patterns/funnel-frequency-breakdown.js +73 -0
- package/lib/hook-patterns/index.js +14 -0
- package/lib/hook-patterns/time-to-convert-by-segment.js +41 -0
- package/lib/orchestrators/user-loop.js +143 -293
- package/lib/utils/utils.js +97 -52
- package/lib/verify/emulate-breakdown.js +281 -0
- package/lib/verify/index.js +12 -0
- package/lib/verify/verify-dungeon.js +61 -0
- package/package.json +6 -4
- package/scripts/smoke-test-all.mjs +162 -0
- package/scripts/verify-runner.mjs +72 -24
- package/types.d.ts +434 -225
- package/dungeons/technical/ad-spend-schema.json +0 -128
- package/dungeons/technical/anonymous-users-schema.json +0 -92
- package/dungeons/technical/array-of-object-lookup-schema.json +0 -191
- package/dungeons/technical/experiments-schema.json +0 -203
- package/dungeons/technical/foobar-schema.json +0 -362
- package/dungeons/technical/group-analytics-schema.json +0 -241
- package/dungeons/technical/mirror-strategies-schema.json +0 -84
- package/dungeons/technical/nested-objects-schema.json +0 -145
- package/dungeons/technical/retention-cadence-schema.json +0 -37
- package/dungeons/technical/sanity-schema.json +0 -185
- package/dungeons/technical/scale-test-schema.json +0 -70
- package/dungeons/technical/scd-schema.json +0 -467
- package/dungeons/technical/simple-schema.json +0 -362
- package/dungeons/technical/text-generation-schema.json +0 -1062
- package/dungeons/vertical/ai-platform-schema.json +0 -617
- package/dungeons/vertical/community-schema.json +0 -579
- package/dungeons/vertical/crypto-schema.json +0 -546
- package/dungeons/vertical/dating-schema.json +0 -401
- package/dungeons/vertical/devtools-schema.json +0 -601
- package/dungeons/vertical/ecommerce-schema.json +0 -604
- package/dungeons/vertical/education-schema.json +0 -5686
- package/dungeons/vertical/fintech-schema.json +0 -630
- package/dungeons/vertical/fitness-schema.json +0 -530
- package/dungeons/vertical/food-delivery-schema.json +0 -36728
- package/dungeons/vertical/gaming-schema.json +0 -2703
- package/dungeons/vertical/healthcare-schema.json +0 -549
- package/dungeons/vertical/insurance-application-schema.json +0 -485
- package/dungeons/vertical/logistics-schema.json +0 -574
- package/dungeons/vertical/marketplace-schema.json +0 -533
- package/dungeons/vertical/media-schema.json +0 -4749
- package/dungeons/vertical/real-estate-schema.json +0 -527
- package/dungeons/vertical/sass-schema.json +0 -3128
- package/dungeons/vertical/social-schema.json +0 -620
- package/dungeons/vertical/travel-schema.json +0 -580
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// ── TWEAK THESE ──
|
|
2
2
|
const SEED = "harness-education";
|
|
3
|
-
const num_days =
|
|
4
|
-
const num_users =
|
|
3
|
+
const num_days = 120;
|
|
4
|
+
const num_users = 10_000;
|
|
5
5
|
const avg_events_per_user_per_day = 1.2;
|
|
6
6
|
let token = "your-mixpanel-token";
|
|
7
7
|
|
|
@@ -28,7 +28,7 @@ const chance = u.initChance(SEED);
|
|
|
28
28
|
* Supports self-paced and cohort-based learning with courses, quizzes,
|
|
29
29
|
* assignments, and social study features.
|
|
30
30
|
*
|
|
31
|
-
* Scale:
|
|
31
|
+
* Scale: 10,000 users / ~1.4M events / 120 days / 17 event types
|
|
32
32
|
*
|
|
33
33
|
* CORE LOOP:
|
|
34
34
|
* Register → browse/enroll in courses → watch lectures → practice problems →
|
|
@@ -52,132 +52,256 @@ const chance = u.initChance(SEED);
|
|
|
52
52
|
|
|
53
53
|
/**
|
|
54
54
|
* ═══════════════════════════════════════════════════════════════════════════════
|
|
55
|
-
* ANALYTICS HOOKS (
|
|
55
|
+
* ANALYTICS HOOKS (9 hooks)
|
|
56
56
|
* ═══════════════════════════════════════════════════════════════════════════════
|
|
57
57
|
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
* teaching_experience_years, instructor_rating). Students get learning
|
|
61
|
-
* attributes (learning_goal, study_hours_per_week).
|
|
62
|
-
*
|
|
63
|
-
* Mixpanel reports:
|
|
64
|
-
* • Insights → Any event → Unique users → Breakdown: "account_type"
|
|
65
|
-
* Expected: ~89% students, ~11% instructors
|
|
66
|
-
* • Insights → "instructor feedback given" → Total per user → Breakdown: "account_type"
|
|
67
|
-
* Expected: Instructors dominate feedback; students show learning_goal instead
|
|
68
|
-
*
|
|
69
|
-
* 2. DEADLINE CRAMMING
|
|
70
|
-
* Assignments submitted on Sun/Mon are rushed: 60% late (vs ~20% baseline),
|
|
71
|
-
* quiz scores drop by 25 points. Events carry is_deadline_rush: true.
|
|
72
|
-
*
|
|
73
|
-
* Mixpanel reports:
|
|
74
|
-
* • Insights → "assignment submitted" → Total → Breakdown: "is_deadline_rush"
|
|
75
|
-
* Expected: is_deadline_rush=true shows ~60% late rate vs ~20% baseline
|
|
76
|
-
* • Insights → "quiz completed" → Avg "score_percent" → Breakdown: Day of Week
|
|
77
|
-
* Expected: Sun/Mon scores ~25 points lower (~40 vs ~65)
|
|
78
|
-
*
|
|
79
|
-
* 3. NOTES-TAKERS SUCCEED
|
|
80
|
-
* Students with 5+ notes_taken=true lectures get +20 quiz score boost
|
|
81
|
-
* (capped at 100) and 40% chance of bonus certificate. Marked diligent_student: true.
|
|
82
|
-
*
|
|
83
|
-
* Mixpanel reports:
|
|
84
|
-
* • Insights → "quiz completed" → Avg "score_percent" → Breakdown: "diligent_student"
|
|
85
|
-
* Expected: diligent_student=true ≈ 85 avg vs ~65 baseline (+20 pts)
|
|
86
|
-
* • Insights → "certificate earned" → Total per user → Breakdown: "diligent_student"
|
|
87
|
-
* Expected: diligent_student=true earn ~40% more certificates
|
|
88
|
-
*
|
|
89
|
-
* 4. STUDY GROUP RETENTION
|
|
90
|
-
* Early study group joiners (within 10 days) retain and get bonus discussions.
|
|
91
|
-
* Non-joiners with low quiz scores (<60) churn hard at day 14 (all later events removed).
|
|
92
|
-
*
|
|
93
|
-
* Mixpanel reports:
|
|
94
|
-
* • Retention → A: "account registered" → B: Any event → Segment by early study group join
|
|
95
|
-
* Expected: Early joiners ~90% D14 retention; non-joiners with low scores ~30%
|
|
96
|
-
* • Insights → "discussion posted" → Total per user → Breakdown: "study_group_member"
|
|
97
|
-
* Expected: study_group_member=true users post more
|
|
98
|
-
*
|
|
99
|
-
* 5. HINT DEPENDENCY
|
|
100
|
-
* Hint users get 60% chance of easy problems; non-hint users get 40% chance of
|
|
101
|
-
* hard problems with independent_solver: true.
|
|
102
|
-
*
|
|
103
|
-
* Mixpanel reports:
|
|
104
|
-
* • Insights → "practice problem solved" → Total → Breakdown: "difficulty" → Filter: hint_used=true
|
|
105
|
-
* Expected: ~60% easy (vs ~33% baseline)
|
|
106
|
-
* • Insights → "practice problem solved" → Total → Breakdown: "difficulty" → Filter: hint_used=false
|
|
107
|
-
* Expected: ~40% hard (vs ~33% baseline)
|
|
108
|
-
*
|
|
109
|
-
* 6. SEMESTER-END SPIKE
|
|
110
|
-
* Days 75-85: quiz_started, quiz_completed, assignment_submitted events duplicated
|
|
111
|
-
* at 80% rate. Events carry semester_end_rush: true.
|
|
112
|
-
*
|
|
113
|
-
* Mixpanel reports:
|
|
114
|
-
* • Insights (line) → "quiz started" + "quiz completed" + "assignment submitted" → Daily
|
|
115
|
-
* Expected: ~2x volume spike during days 75-85
|
|
116
|
-
* • Insights → "quiz completed" → Total → Breakdown: "semester_end_rush"
|
|
117
|
-
* Expected: semester_end_rush=true clusters in days 75-85
|
|
118
|
-
*
|
|
119
|
-
* 7. FREE VS PAID COURSES
|
|
120
|
-
* Free users get 0.5x funnel conversion rate; paid subscribers get 1.5x.
|
|
121
|
-
* Free users also lose 55% of certificates. Creates ~2.2x completion gap.
|
|
122
|
-
*
|
|
123
|
-
* Mixpanel reports:
|
|
124
|
-
* • Funnels → "course enrolled" → "lecture completed" → "quiz completed" → "certificate earned"
|
|
125
|
-
* Breakdown: "subscription_status"
|
|
126
|
-
* Expected: free ≈ 15% completion, paid ≈ 33% (~2.2x difference)
|
|
127
|
-
* • Insights → "certificate earned" → Total per user → Breakdown: "subscription_status"
|
|
128
|
-
* Expected: Paid subscribers earn significantly more certificates
|
|
129
|
-
*
|
|
130
|
-
* 8. PLAYBACK SPEED CORRELATION
|
|
131
|
-
* Speed learners (>=2.0x, 3+ lectures): compressed watch_time (0.6x),
|
|
132
|
-
* paradoxically higher quiz scores (+8 pts). Thorough learners (<=1.0x):
|
|
133
|
-
* extended watch_time (1.4x).
|
|
134
|
-
*
|
|
135
|
-
* Mixpanel reports:
|
|
136
|
-
* • Insights → "lecture completed" → Avg "watch_time_mins" → Breakdown: "speed_learner"
|
|
137
|
-
* Expected: speed_learner=true ≈ 0.6x watch time
|
|
138
|
-
* • Insights → "quiz completed" → Avg "score_percent" → Breakdown: "speed_learner_effect"
|
|
139
|
-
* Expected: speed_learner_effect=true shows +8 points (faster = better)
|
|
58
|
+
* NOTE: All cohort effects are HIDDEN — no flag stamping. Discoverable via
|
|
59
|
+
* behavioral cohorts, raw-prop breakdowns, or funnel time-to-convert.
|
|
140
60
|
*
|
|
141
|
-
*
|
|
142
|
-
*
|
|
143
|
-
*
|
|
61
|
+
* 1. STUDENT VS INSTRUCTOR PROFILES (user)
|
|
62
|
+
*
|
|
63
|
+
* PATTERN: Instructor profiles get teaching attributes
|
|
64
|
+
* (courses_created, teaching_experience_years, instructor_rating).
|
|
65
|
+
* Students get learning attributes (learning_goal,
|
|
66
|
+
* study_hours_per_week). Two-sided marketplace at ~89% students,
|
|
67
|
+
* ~11% instructors.
|
|
68
|
+
*
|
|
69
|
+
* HOW TO FIND IT IN MIXPANEL:
|
|
70
|
+
*
|
|
71
|
+
* Report 1: Account Mix
|
|
72
|
+
* - Report type: Insights
|
|
73
|
+
* - Event: any event
|
|
74
|
+
* - Measure: Unique users
|
|
75
|
+
* - Breakdown: "account_type"
|
|
76
|
+
* - Expected: ~89% students, ~11% instructors
|
|
77
|
+
*
|
|
78
|
+
* Report 2: Instructor-Driven Feedback
|
|
79
|
+
* - Report type: Insights
|
|
80
|
+
* - Event: "instructor feedback given"
|
|
81
|
+
* - Measure: Total per user (average)
|
|
82
|
+
* - Breakdown: "account_type"
|
|
83
|
+
* - Expected: instructors dominate feedback volume; students rarely emit
|
|
84
|
+
*
|
|
85
|
+
* REAL-WORLD ANALOGUE: Two-sided learning marketplaces have
|
|
86
|
+
* fundamentally different role personas — teachers create supply,
|
|
87
|
+
* learners consume it.
|
|
88
|
+
*
|
|
89
|
+
* ---------------------------------------------------------------
|
|
90
|
+
* 2. DEADLINE CRAMMING (everything)
|
|
91
|
+
*
|
|
92
|
+
* PATTERN: Assignments submitted on Sun/Mon are rushed — 60% are late
|
|
93
|
+
* (raw is_late prop set true at 60% likelihood) vs ~20% baseline. Quiz
|
|
94
|
+
* scores on Sun/Mon drop by 25 points. No flag — discover via Day of Week
|
|
95
|
+
* breakdown.
|
|
96
|
+
*
|
|
97
|
+
* HOW TO FIND IT IN MIXPANEL:
|
|
98
|
+
*
|
|
99
|
+
* Report 1: Late Submission Rate by Day of Week
|
|
100
|
+
* - Report type: Insights
|
|
101
|
+
* - Event: "assignment submitted"
|
|
102
|
+
* - Measure: count where is_late=true / total
|
|
103
|
+
* - Breakdown: Day of Week
|
|
104
|
+
* - Expected: Sun/Mon ~ 60% late vs other days ~ 20%
|
|
105
|
+
*
|
|
106
|
+
* Report 2: Quiz Score by Day of Week
|
|
107
|
+
* - Report type: Insights
|
|
108
|
+
* - Event: "quiz completed"
|
|
109
|
+
* - Measure: Average of "score_percent"
|
|
110
|
+
* - Breakdown: Day of Week
|
|
111
|
+
* - Expected: Sun/Mon ~ 25, other days ~ 49 (-25 pts)
|
|
112
|
+
*
|
|
113
|
+
* REAL-WORLD ANALOGUE: Procrastination clusters submissions at the
|
|
114
|
+
* deadline weekend and hammers performance.
|
|
115
|
+
*
|
|
116
|
+
* ---------------------------------------------------------------
|
|
117
|
+
* 3. NOTES MAGIC NUMBER (everything, in-funnel)
|
|
118
|
+
*
|
|
119
|
+
* PATTERN: Sweet 5-8 lectures with notes_taken=true → +30% quiz
|
|
120
|
+
* score_percent (cap 100) and 40% chance of bonus cloned certificate.
|
|
121
|
+
* Over 9+ → 35% of certificate-earned events drop (over-noted but
|
|
122
|
+
* stuck in study mode). No flag.
|
|
123
|
+
*
|
|
124
|
+
* HOW TO FIND IT IN MIXPANEL:
|
|
125
|
+
*
|
|
126
|
+
* Report 1: Quiz Score by Notes-Taken Bucket
|
|
127
|
+
* - Cohort A: users with 5-8 "lecture completed" where notes_taken=true
|
|
128
|
+
* - Cohort B: users with 0-4
|
|
129
|
+
* - Event: "quiz completed"
|
|
130
|
+
* - Measure: Average of "score_percent"
|
|
131
|
+
* - Expected: A ~ 1.3x B
|
|
132
|
+
*
|
|
133
|
+
* Report 2: Certificates per User on Heavy Note-Takers
|
|
134
|
+
* - Cohort C: users with >= 9 notes-taken lectures
|
|
135
|
+
* - Cohort A: users with 5-8
|
|
136
|
+
* - Event: "certificate earned"
|
|
137
|
+
* - Measure: Total per user
|
|
138
|
+
* - Expected: C ~ 35% fewer certificates per user vs A
|
|
139
|
+
*
|
|
140
|
+
* REAL-WORLD ANALOGUE: Active note-taking lifts quiz performance, but
|
|
141
|
+
* obsessive note-taking signals "stuck in study mode" without finishing.
|
|
142
|
+
*
|
|
143
|
+
* ---------------------------------------------------------------
|
|
144
|
+
* 4. STUDY GROUP RETENTION (everything)
|
|
145
|
+
*
|
|
146
|
+
* PATTERN: Users who join a study group within 10 days get bonus
|
|
147
|
+
* discussion events. Non-joiners with low quiz scores (<60) churn
|
|
148
|
+
* hard at day 14 — all later events are removed.
|
|
149
|
+
*
|
|
150
|
+
* HOW TO FIND IT IN MIXPANEL:
|
|
151
|
+
*
|
|
152
|
+
* Report 1: D14 Retention by Early Group Join Cohort
|
|
153
|
+
* - Cohort A: users who fired "study group joined" within first 10 days
|
|
154
|
+
* - Cohort B: users with no early study group joined
|
|
155
|
+
* - Compare D14 retention (any event past d14) per cohort
|
|
156
|
+
* - Expected: A ~ 90%+ vs B (with low quiz scores) ~ 30%
|
|
157
|
+
*
|
|
158
|
+
* Report 2: Discussion Volume by Group Cohort
|
|
159
|
+
* - Cohort A vs B (as above)
|
|
160
|
+
* - Event: "discussion posted"
|
|
161
|
+
* - Measure: Total per user
|
|
162
|
+
* - Expected: A posts substantially more
|
|
163
|
+
*
|
|
164
|
+
* REAL-WORLD ANALOGUE: Social learning ties create accountability
|
|
165
|
+
* and dramatically reduce drop-off in cohort-based courses.
|
|
166
|
+
*
|
|
167
|
+
* ---------------------------------------------------------------
|
|
168
|
+
* 5. HINT DEPENDENCY (event)
|
|
169
|
+
*
|
|
170
|
+
* PATTERN: On "practice problem solved", hint_used=true gets difficulty
|
|
171
|
+
* forced to "easy" 60% of the time. hint_used=false gets difficulty forced
|
|
172
|
+
* to "hard" 40% of the time. No flag — discover via difficulty breakdown
|
|
173
|
+
* filtered by hint_used.
|
|
174
|
+
*
|
|
175
|
+
* HOW TO FIND IT IN MIXPANEL:
|
|
176
|
+
*
|
|
177
|
+
* Report 1: Easy Problem Mix for Hint Users
|
|
178
|
+
* - Report type: Insights
|
|
179
|
+
* - Event: "practice problem solved"
|
|
180
|
+
* - Measure: Total
|
|
181
|
+
* - Filter: "hint_used" = true
|
|
182
|
+
* - Breakdown: "difficulty"
|
|
183
|
+
* - Expected: ~60% easy (vs ~33% baseline)
|
|
184
|
+
*
|
|
185
|
+
* Report 2: Hard Problem Mix for Independent Solvers
|
|
186
|
+
* - Report type: Insights
|
|
187
|
+
* - Event: "practice problem solved"
|
|
188
|
+
* - Measure: Total
|
|
189
|
+
* - Filter: "hint_used" = false
|
|
190
|
+
* - Breakdown: "difficulty"
|
|
191
|
+
* - Expected: ~40% hard (vs ~33% baseline)
|
|
192
|
+
*
|
|
193
|
+
* REAL-WORLD ANALOGUE: Learners who lean on hints get nudged toward
|
|
194
|
+
* easier work, while those who push through unaided self-select
|
|
195
|
+
* into harder material.
|
|
196
|
+
*
|
|
197
|
+
* ---------------------------------------------------------------
|
|
198
|
+
* 6. SEMESTER-END SPIKE (everything)
|
|
199
|
+
*
|
|
200
|
+
* PATTERN: Days 75-85 simulate semester crunch. quiz_started, quiz_completed,
|
|
201
|
+
* and assignment_submitted events are duplicated at an 80% rate. No flag —
|
|
202
|
+
* discover via line chart of those event volumes by day.
|
|
203
|
+
*
|
|
204
|
+
* HOW TO FIND IT IN MIXPANEL:
|
|
205
|
+
*
|
|
206
|
+
* Report 1: Assessment Volume Over Time
|
|
207
|
+
* - Report type: Insights
|
|
208
|
+
* - Events: "quiz started" + "quiz completed" + "assignment submitted"
|
|
209
|
+
* - Measure: Total
|
|
210
|
+
* - Line chart by day
|
|
211
|
+
* - Expected: ~2x volume spike on days 75-85
|
|
212
|
+
*
|
|
213
|
+
* REAL-WORLD ANALOGUE: Semester-end deadlines reliably produce a
|
|
214
|
+
* massive last-minute surge in student activity.
|
|
215
|
+
*
|
|
216
|
+
* ---------------------------------------------------------------
|
|
217
|
+
* 7. FREE VS PAID COURSES (funnel-pre + everything)
|
|
218
|
+
*
|
|
219
|
+
* PATTERN: Free users get 0.5x funnel conversion rate; paid
|
|
220
|
+
* subscribers get 1.5x. Free users also lose 55% of their
|
|
221
|
+
* certificates, producing a ~2.2x completion gap.
|
|
222
|
+
*
|
|
223
|
+
* HOW TO FIND IT IN MIXPANEL:
|
|
224
|
+
*
|
|
225
|
+
* Report 1: Course Completion Funnel by Subscription
|
|
226
|
+
* - Report type: Funnels
|
|
227
|
+
* - Steps: "course enrolled" -> "lecture completed" -> "quiz completed" -> "certificate earned"
|
|
228
|
+
* - Breakdown: "subscription_status"
|
|
229
|
+
* - Expected: free ~ 15% completion, paid ~ 33% (~2.2x gap)
|
|
230
|
+
*
|
|
231
|
+
* Report 2: Certificates Earned per User
|
|
232
|
+
* - Report type: Insights
|
|
233
|
+
* - Event: "certificate earned"
|
|
234
|
+
* - Measure: Total per user (average)
|
|
235
|
+
* - Breakdown: "subscription_status"
|
|
236
|
+
* - Expected: paid subscribers earn substantially more certificates
|
|
237
|
+
*
|
|
238
|
+
* REAL-WORLD ANALOGUE: Paid commitment correlates strongly with
|
|
239
|
+
* follow-through; free learners drop off long before completion.
|
|
240
|
+
*
|
|
241
|
+
* ---------------------------------------------------------------
|
|
242
|
+
* 8. PLAYBACK SPEED CORRELATION (everything)
|
|
243
|
+
*
|
|
244
|
+
* PATTERN: Speed learners (>=2.0x speed on 3+ lectures) get 0.6x
|
|
245
|
+
* watch_time and a paradoxical +8 quiz score boost. Thorough
|
|
246
|
+
* learners (<=1.0x) get 1.4x watch_time.
|
|
247
|
+
*
|
|
248
|
+
* HOW TO FIND IT IN MIXPANEL:
|
|
249
|
+
*
|
|
250
|
+
* Report 1: Watch Time by Playback Speed
|
|
251
|
+
* - Report type: Insights
|
|
252
|
+
* - Event: "lecture completed"
|
|
253
|
+
* - Measure: Average of "watch_time_mins"
|
|
254
|
+
* - Breakdown: "playback_speed"
|
|
255
|
+
* - Expected: speed >= 2.0 ~ 0.6x baseline; speed <= 1.0 ~ 1.4x baseline
|
|
256
|
+
*
|
|
257
|
+
* Report 2: Quiz Score by Speed Learner Cohort
|
|
258
|
+
* - Cohort A: users with 3+ "lecture completed" events at playback_speed >= 2.0
|
|
259
|
+
* - Cohort B: rest
|
|
260
|
+
* - Event: "quiz completed"
|
|
261
|
+
* - Measure: Average of "score_percent"
|
|
262
|
+
* - Expected: A ~ +8 pts vs B
|
|
263
|
+
*
|
|
264
|
+
* REAL-WORLD ANALOGUE: Power users who watch lectures at 2x speed
|
|
265
|
+
* tend to be domain-confident and outperform on assessments
|
|
266
|
+
* despite spending less time.
|
|
267
|
+
*
|
|
268
|
+
* ---------------------------------------------------------------
|
|
269
|
+
* 9. COURSE COMPLETION TIME-TO-CONVERT (funnel-post)
|
|
270
|
+
*
|
|
271
|
+
* PATTERN: Annual subscribers complete the course-completion funnel
|
|
272
|
+
* 1.4x faster (factor 0.71); Free users 1.4x slower (factor 1.4).
|
|
144
273
|
*
|
|
145
|
-
*
|
|
146
|
-
* - The Ideal Student: notes (H3) + study groups (H4) + no hints (H5) + paid (H7) + speed (H8)
|
|
147
|
-
* - Cramming Cascade: deadline crammers (H2) compounded with semester-end spike (H6)?
|
|
148
|
-
* - Social Safety Net: does early study group joining (H4) prevent churn for low scorers?
|
|
149
|
-
* - Hint-to-Mastery: do hint-dependent (H5) students who join groups (H4) wean off hints?
|
|
150
|
-
* - Payment + Notes: are paid subscribers (H7) more likely to take notes (H3)?
|
|
274
|
+
* HOW TO FIND IT IN MIXPANEL:
|
|
151
275
|
*
|
|
152
|
-
*
|
|
153
|
-
*
|
|
154
|
-
*
|
|
155
|
-
*
|
|
156
|
-
*
|
|
276
|
+
* Report 1: Course Completion Median Time-to-Convert by Subscription
|
|
277
|
+
* - Funnels > "course enrolled" -> "lecture completed" -> "quiz completed" -> "certificate earned"
|
|
278
|
+
* - Measure: Median time to convert
|
|
279
|
+
* - Breakdown: subscription_status
|
|
280
|
+
* - Expected: annual ~ 0.71x baseline; free ~ 1.4x baseline
|
|
157
281
|
*
|
|
158
|
-
*
|
|
159
|
-
* -
|
|
160
|
-
* -
|
|
161
|
-
*
|
|
282
|
+
* NOTE (funnel-post measurement): visible only via Mixpanel funnel
|
|
283
|
+
* median TTC. Cross-event MIN→MIN SQL queries on raw events do NOT
|
|
284
|
+
* show this — funnel-post adjusts gaps within funnel instances, not
|
|
285
|
+
* across the user's full event history.
|
|
286
|
+
*
|
|
287
|
+
* REAL-WORLD ANALOGUE: Paid commitment accelerates throughput.
|
|
162
288
|
*
|
|
163
289
|
* ═══════════════════════════════════════════════════════════════════════════════
|
|
164
290
|
* EXPECTED METRICS SUMMARY
|
|
165
291
|
* ═══════════════════════════════════════════════════════════════════════════════
|
|
166
292
|
*
|
|
167
293
|
* Hook | Metric | Baseline | Hook Effect | Ratio
|
|
168
|
-
*
|
|
169
|
-
* Student vs Instructor | Profile attributes | generic | role-specific|
|
|
170
|
-
* Deadline Cramming |
|
|
171
|
-
*
|
|
172
|
-
* Notes
|
|
173
|
-
*
|
|
174
|
-
*
|
|
175
|
-
*
|
|
176
|
-
* Hint Dependency | Easy problem rate | ~33% | ~60% | 1.8x
|
|
177
|
-
* Hint Dependency | Hard problem rate | ~33% | ~40% (no hint)| 1.2x
|
|
178
|
-
* Semester-End Spike | Assessment volume | baseline | ~2x | 2x
|
|
294
|
+
* ------------------------|-----------------------|----------|--------------|------
|
|
295
|
+
* Student vs Instructor | Profile attributes | generic | role-specific| n/a
|
|
296
|
+
* Deadline Cramming | Sun/Mon quiz score | 1x | -25 pt | -38%
|
|
297
|
+
* Notes Magic Number | sweet quiz score | 1x | 1.3x | 1.3x
|
|
298
|
+
* Notes Magic Number | over certificates/user| 1x | 0.65x | -35%
|
|
299
|
+
* Study Group Retention | D14 retention | ~ 40% | ~ 90% | 2.3x
|
|
300
|
+
* Hint Dependency | easy problem rate (hint) | 33% | ~ 60% | 1.8x
|
|
301
|
+
* Semester-End Spike | Assessment volume | 1x | ~ 2x | 2x
|
|
179
302
|
* Free vs Paid | Course completion | 15% | 33% | 2.2x
|
|
180
|
-
* Playback Speed | Quiz score (speed) | ~65
|
|
303
|
+
* Playback Speed | Quiz score (speed) | ~ 65 | ~ 73 | +8 pt
|
|
304
|
+
* Course Completion T2C | median min by tier | 1x | 0.71x/1.4x | ~ 2x range
|
|
181
305
|
*/
|
|
182
306
|
|
|
183
307
|
// Generate consistent IDs for lookup tables and event properties
|
|
@@ -190,12 +314,16 @@ const problemIds = v.range(1, 601).map(n => `problem_${v.uid(6)}`);
|
|
|
190
314
|
|
|
191
315
|
/** @type {Config} */
|
|
192
316
|
const config = {
|
|
317
|
+
version: 2,
|
|
193
318
|
token,
|
|
194
319
|
seed: SEED,
|
|
195
|
-
|
|
320
|
+
datasetStart: "2026-01-01T00:00:00Z",
|
|
321
|
+
datasetEnd: "2026-05-01T23:59:59Z",
|
|
322
|
+
// numDays: num_days,
|
|
196
323
|
avgEventsPerUserPerDay: avg_events_per_user_per_day,
|
|
197
324
|
numUsers: num_users,
|
|
198
|
-
hasAnonIds:
|
|
325
|
+
hasAnonIds: true,
|
|
326
|
+
avgDevicePerUser: 2,
|
|
199
327
|
hasSessionIds: true,
|
|
200
328
|
format: "json",
|
|
201
329
|
gzip: true,
|
|
@@ -268,6 +396,7 @@ const config = {
|
|
|
268
396
|
event: "account registered",
|
|
269
397
|
weight: 1,
|
|
270
398
|
isFirstEvent: true,
|
|
399
|
+
isAuthEvent: true,
|
|
271
400
|
properties: {
|
|
272
401
|
"account_type": ["instructor", "instructor", "instructor", "instructor", "instructor", "instructor", "student"],
|
|
273
402
|
"signup_source": ["organic", "referral", "school_partnership", "social_ad"],
|
|
@@ -302,8 +431,6 @@ const config = {
|
|
|
302
431
|
"watch_time_mins": u.weighNumRange(3, 60, 0.8, 20),
|
|
303
432
|
"playback_speed": [0.75, 1.0, 1.0, 1.0, 1.25, 1.5, 2.0],
|
|
304
433
|
"notes_taken": [false, false, true],
|
|
305
|
-
"speed_learner": [false],
|
|
306
|
-
"thorough_learner": [false],
|
|
307
434
|
}
|
|
308
435
|
},
|
|
309
436
|
{
|
|
@@ -314,7 +441,6 @@ const config = {
|
|
|
314
441
|
"quiz_id": quizIds,
|
|
315
442
|
"quiz_type": ["practice", "graded", "final_exam"],
|
|
316
443
|
"question_count": u.weighNumRange(5, 50, 0.7, 15),
|
|
317
|
-
"semester_end_rush": [false],
|
|
318
444
|
}
|
|
319
445
|
},
|
|
320
446
|
{
|
|
@@ -326,9 +452,6 @@ const config = {
|
|
|
326
452
|
"score_percent": u.weighNumRange(0, 100, 1.2, 50),
|
|
327
453
|
"time_spent_mins": u.weighNumRange(3, 120, 0.6, 25),
|
|
328
454
|
"attempts": u.weighNumRange(1, 5, 0.5, 3),
|
|
329
|
-
"diligent_student": [false],
|
|
330
|
-
"speed_learner_effect": [false],
|
|
331
|
-
"semester_end_rush": [false],
|
|
332
455
|
}
|
|
333
456
|
},
|
|
334
457
|
{
|
|
@@ -340,8 +463,6 @@ const config = {
|
|
|
340
463
|
"submission_type": ["text", "code", "file", "project"],
|
|
341
464
|
"word_count": u.weighNumRange(100, 5000, 0.6, 500),
|
|
342
465
|
"is_late": [false, false, false, false, true],
|
|
343
|
-
"is_deadline_rush": [false],
|
|
344
|
-
"semester_end_rush": [false],
|
|
345
466
|
}
|
|
346
467
|
},
|
|
347
468
|
{
|
|
@@ -362,7 +483,6 @@ const config = {
|
|
|
362
483
|
"course_id": courseIds,
|
|
363
484
|
"post_type": ["question", "answer", "comment"],
|
|
364
485
|
"word_count": u.weighNumRange(10, 500, 0.6, 80),
|
|
365
|
-
"study_group_member": [false],
|
|
366
486
|
}
|
|
367
487
|
},
|
|
368
488
|
{
|
|
@@ -372,7 +492,6 @@ const config = {
|
|
|
372
492
|
"course_id": courseIds,
|
|
373
493
|
"completion_time_days": u.weighNumRange(7, 180, 0.5, 45),
|
|
374
494
|
"final_grade": u.weighNumRange(60, 100, 1.2, 30),
|
|
375
|
-
"diligent_student": [false],
|
|
376
495
|
}
|
|
377
496
|
},
|
|
378
497
|
{
|
|
@@ -436,7 +555,6 @@ const config = {
|
|
|
436
555
|
"difficulty": ["easy", "medium", "hard"],
|
|
437
556
|
"time_to_solve_sec": u.weighNumRange(10, 3600, 0.5, 300),
|
|
438
557
|
"hint_used": [false, false, true],
|
|
439
|
-
"independent_solver": [false],
|
|
440
558
|
}
|
|
441
559
|
},
|
|
442
560
|
],
|
|
@@ -511,12 +629,7 @@ const config = {
|
|
|
511
629
|
* 8. PLAYBACK SPEED CORRELATION: Speed learners paradoxically score higher; thorough learners get extended time
|
|
512
630
|
*/
|
|
513
631
|
hook: function (record, type, meta) {
|
|
514
|
-
|
|
515
|
-
const DATASET_START = NOW.subtract(num_days, 'days');
|
|
516
|
-
|
|
517
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
518
|
-
// Hook #1: STUDENT VS INSTRUCTOR PROFILES
|
|
519
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
632
|
+
// HOOK 1: STUDENT VS INSTRUCTOR PROFILES (user) — role-based attributes.
|
|
520
633
|
if (type === "user") {
|
|
521
634
|
if (record.account_type === "instructor") {
|
|
522
635
|
record.courses_created = chance.integer({ min: 1, max: 15 });
|
|
@@ -528,216 +641,147 @@ const config = {
|
|
|
528
641
|
}
|
|
529
642
|
}
|
|
530
643
|
|
|
531
|
-
//
|
|
532
|
-
//
|
|
533
|
-
//
|
|
644
|
+
// HOOK 5 (event): HINT DEPENDENCY — hint users get 60% easy problems;
|
|
645
|
+
// non-hint users get 40% hard problems. Mutates difficulty (raw).
|
|
646
|
+
// HOOK 8 (event): PLAYBACK SPEED — speed learners (>= 2.0x) get
|
|
647
|
+
// watch_time_mins compressed 0.6x; thorough learners (<= 1.0x) get 1.4x.
|
|
534
648
|
if (type === "event") {
|
|
535
|
-
if (record.event === "
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
record.is_late = !chance.bool({ likelihood: 40 });
|
|
541
|
-
} else {
|
|
542
|
-
record.is_deadline_rush = false;
|
|
543
|
-
record.is_late = !chance.bool({ likelihood: 80 });
|
|
649
|
+
if (record.event === "practice problem solved") {
|
|
650
|
+
if (record.hint_used === true && chance.bool({ likelihood: 60 })) {
|
|
651
|
+
record.difficulty = "easy";
|
|
652
|
+
} else if (record.hint_used === false && chance.bool({ likelihood: 40 })) {
|
|
653
|
+
record.difficulty = "hard";
|
|
544
654
|
}
|
|
545
655
|
}
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
553
|
-
// Hook #5: HINT DEPENDENCY
|
|
554
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
555
|
-
if (type === "event") {
|
|
556
|
-
if (record.event === "practice problem solved") {
|
|
557
|
-
if (record.hint_used === true) {
|
|
558
|
-
// Hint users gravitate toward easy problems
|
|
559
|
-
if (chance.bool({ likelihood: 60 })) {
|
|
560
|
-
record.difficulty = "easy";
|
|
561
|
-
}
|
|
562
|
-
record.independent_solver = false;
|
|
563
|
-
} else if (record.hint_used === false) {
|
|
564
|
-
// Independent solvers tackle harder problems
|
|
565
|
-
if (chance.bool({ likelihood: 40 })) {
|
|
566
|
-
record.difficulty = "hard";
|
|
567
|
-
record.independent_solver = true;
|
|
568
|
-
} else {
|
|
569
|
-
record.independent_solver = false;
|
|
570
|
-
}
|
|
571
|
-
} else {
|
|
572
|
-
record.independent_solver = false;
|
|
656
|
+
if (record.event === "lecture completed") {
|
|
657
|
+
const speed = record.playback_speed;
|
|
658
|
+
if (speed >= 2.0 && record.watch_time_mins !== undefined) {
|
|
659
|
+
record.watch_time_mins = Math.max(3, Math.floor(record.watch_time_mins * 0.6));
|
|
660
|
+
} else if (speed !== undefined && speed <= 1.0 && record.watch_time_mins !== undefined) {
|
|
661
|
+
record.watch_time_mins = Math.min(90, Math.floor(record.watch_time_mins * 1.4));
|
|
573
662
|
}
|
|
574
663
|
}
|
|
575
664
|
}
|
|
576
665
|
|
|
577
|
-
//
|
|
578
|
-
//
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
if (
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
const spikableEvents = ["quiz started", "quiz completed", "assignment submitted"];
|
|
586
|
-
if (spikableEvents.includes(record.event)) {
|
|
587
|
-
if (dayInDataset >= 75 && dayInDataset <= 85) {
|
|
588
|
-
record.semester_end_rush = true;
|
|
589
|
-
} else {
|
|
590
|
-
record.semester_end_rush = false;
|
|
591
|
-
}
|
|
592
|
-
}
|
|
666
|
+
// HOOK 7 (funnel-pre): FREE VS PAID — free users get 0.5x conversion rate;
|
|
667
|
+
// paid subscribers get 1.5x. Applies to course-completion funnel.
|
|
668
|
+
if (type === "funnel-pre") {
|
|
669
|
+
const subStatus = meta?.profile?.subscription_status;
|
|
670
|
+
if (subStatus === "free") {
|
|
671
|
+
record.conversionRate = Math.round(record.conversionRate * 0.5);
|
|
672
|
+
} else if (subStatus === "monthly" || subStatus === "annual") {
|
|
673
|
+
record.conversionRate = Math.min(100, Math.round(record.conversionRate * 1.5));
|
|
593
674
|
}
|
|
594
675
|
}
|
|
595
676
|
|
|
596
|
-
//
|
|
597
|
-
//
|
|
598
|
-
//
|
|
599
|
-
if (type === "
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
record.thorough_learner = true;
|
|
613
|
-
// Extend watch time for thorough learners
|
|
614
|
-
if (record.watch_time_mins !== undefined) {
|
|
615
|
-
record.watch_time_mins = Math.min(90, Math.floor(record.watch_time_mins * 1.4));
|
|
677
|
+
// HOOK 9 (T2C): COURSE COMPLETION TIME-TO-CONVERT (funnel-post)
|
|
678
|
+
// Annual subscribers complete the Course Completion funnel 1.4x
|
|
679
|
+
// faster (factor 0.71); Free users 1.4x slower (factor 1.4).
|
|
680
|
+
if (type === "funnel-post") {
|
|
681
|
+
const segment = meta?.profile?.subscription_status;
|
|
682
|
+
if (Array.isArray(record) && record.length > 1) {
|
|
683
|
+
const factor = (
|
|
684
|
+
segment === "annual" ? 0.71 :
|
|
685
|
+
segment === "free" ? 1.4 :
|
|
686
|
+
1.0
|
|
687
|
+
);
|
|
688
|
+
if (factor !== 1.0) {
|
|
689
|
+
for (let i = 1; i < record.length; i++) {
|
|
690
|
+
const prev = dayjs(record[i - 1].time);
|
|
691
|
+
const newGap = Math.round(dayjs(record[i].time).diff(prev) * factor);
|
|
692
|
+
record[i].time = prev.add(newGap, "milliseconds").toISOString();
|
|
616
693
|
}
|
|
617
|
-
} else {
|
|
618
|
-
record.speed_learner = false;
|
|
619
|
-
record.thorough_learner = false;
|
|
620
694
|
}
|
|
621
695
|
}
|
|
622
696
|
}
|
|
623
697
|
|
|
624
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
625
|
-
// Hook #3: NOTES-TAKERS SUCCEED
|
|
626
|
-
// Hook #4: STUDY GROUP RETENTION
|
|
627
|
-
// Hook #7: FREE VS PAID (funnel-pre handled below)
|
|
628
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
629
698
|
if (type === "everything") {
|
|
699
|
+
const datasetStart = dayjs.unix(meta.datasetStart);
|
|
630
700
|
const userEvents = record;
|
|
631
701
|
const profile = meta.profile;
|
|
632
702
|
const firstEventTime = userEvents.length > 0 ? dayjs(userEvents[0].time) : null;
|
|
633
703
|
|
|
634
|
-
// ── Stamp superProps from profile so they stay consistent per user ──
|
|
635
704
|
if (profile) {
|
|
636
705
|
userEvents.forEach((event) => {
|
|
637
706
|
if (profile.Platform !== undefined) event.Platform = profile.Platform;
|
|
638
707
|
});
|
|
639
708
|
}
|
|
640
709
|
|
|
641
|
-
// ---------------------------------------------------------------
|
|
642
|
-
// First pass: identify user patterns
|
|
643
|
-
// ---------------------------------------------------------------
|
|
644
710
|
let notesTakenCount = 0;
|
|
645
711
|
let joinedStudyGroupEarly = false;
|
|
646
712
|
let hasLowQuizScore = false;
|
|
713
|
+
let speedLectureCount = 0;
|
|
647
714
|
|
|
648
715
|
userEvents.forEach((event) => {
|
|
649
716
|
const eventTime = dayjs(event.time);
|
|
650
717
|
const daysSinceStart = firstEventTime ? eventTime.diff(firstEventTime, 'days', true) : 0;
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
if (event.event === "
|
|
654
|
-
|
|
655
|
-
}
|
|
656
|
-
|
|
657
|
-
// Hook #4: Check if user joined a study group within the first 10 days
|
|
658
|
-
if (event.event === "study group joined" && daysSinceStart <= 10) {
|
|
659
|
-
joinedStudyGroupEarly = true;
|
|
660
|
-
}
|
|
661
|
-
|
|
662
|
-
// Hook #4: Check for any quiz_completed with score < 60
|
|
663
|
-
if (event.event === "quiz completed" && event.score_percent < 60) {
|
|
664
|
-
hasLowQuizScore = true;
|
|
665
|
-
}
|
|
718
|
+
if (event.event === "lecture completed" && event.notes_taken === true) notesTakenCount++;
|
|
719
|
+
if (event.event === "study group joined" && daysSinceStart <= 10) joinedStudyGroupEarly = true;
|
|
720
|
+
if (event.event === "quiz completed" && event.score_percent < 60) hasLowQuizScore = true;
|
|
721
|
+
if (event.event === "lecture completed" && event.playback_speed >= 2.0) speedLectureCount++;
|
|
666
722
|
});
|
|
667
723
|
|
|
668
|
-
//
|
|
669
|
-
//
|
|
670
|
-
//
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
if (event.event === "quiz completed") {
|
|
677
|
-
if (event.score_percent !== undefined) {
|
|
678
|
-
event.score_percent = Math.min(100, event.score_percent + 20);
|
|
679
|
-
}
|
|
680
|
-
event.diligent_student = true;
|
|
724
|
+
// HOOK 3 + HOOK 10: NOTES MAGIC NUMBER (in-funnel, no flags)
|
|
725
|
+
// Sweet 5-8 notes-taken lectures → +30% quiz score_percent (cap 100).
|
|
726
|
+
// Over 9+ → drop 35% of certificate-earned events (over-noted but
|
|
727
|
+
// can't synthesize; gets stuck in "study mode").
|
|
728
|
+
if (notesTakenCount >= 5 && notesTakenCount <= 8) {
|
|
729
|
+
userEvents.forEach((event) => {
|
|
730
|
+
if (event.event === "quiz completed" && event.score_percent !== undefined) {
|
|
731
|
+
event.score_percent = Math.min(100, Math.round(event.score_percent * 1.3));
|
|
681
732
|
}
|
|
682
733
|
});
|
|
683
|
-
|
|
684
|
-
// 40% chance to splice in an extra certificate_earned event
|
|
685
734
|
if (chance.bool({ likelihood: 40 })) {
|
|
686
735
|
const lastEvent = userEvents[userEvents.length - 1];
|
|
687
736
|
const certTemplate = userEvents.find(e => e.event === "certificate earned");
|
|
688
737
|
if (lastEvent && certTemplate) {
|
|
689
|
-
|
|
738
|
+
userEvents.push({
|
|
690
739
|
...certTemplate,
|
|
691
740
|
time: dayjs(lastEvent.time).add(chance.integer({ min: 1, max: 5 }), 'days').toISOString(),
|
|
692
741
|
user_id: lastEvent.user_id,
|
|
693
742
|
course_id: chance.pickone(courseIds),
|
|
694
743
|
completion_time_days: chance.integer({ min: 14, max: 90 }),
|
|
695
744
|
final_grade: chance.integer({ min: 80, max: 100 }),
|
|
696
|
-
|
|
697
|
-
};
|
|
698
|
-
userEvents.push(certEvent);
|
|
745
|
+
});
|
|
699
746
|
}
|
|
700
747
|
}
|
|
701
|
-
}
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
speedLectureCount++;
|
|
748
|
+
} else if (notesTakenCount >= 9) {
|
|
749
|
+
// Over-noters: drop 35% of certificates (stuck in study mode)
|
|
750
|
+
for (let i = userEvents.length - 1; i >= 0; i--) {
|
|
751
|
+
if (userEvents[i].event === "certificate earned" && chance.bool({ likelihood: 35 })) {
|
|
752
|
+
userEvents.splice(i, 1);
|
|
753
|
+
}
|
|
708
754
|
}
|
|
709
|
-
}
|
|
710
|
-
const isSpeedLearner = speedLectureCount >= 3;
|
|
755
|
+
}
|
|
711
756
|
|
|
712
|
-
|
|
757
|
+
// HOOK 8 (cont): Speed learners (3+ lectures at 2.0x) score +8 on quizzes.
|
|
758
|
+
if (speedLectureCount >= 3) {
|
|
713
759
|
userEvents.forEach((event) => {
|
|
714
|
-
if (event.event === "quiz completed") {
|
|
715
|
-
|
|
716
|
-
event.score_percent = Math.min(100, event.score_percent + 8);
|
|
717
|
-
event.speed_learner_effect = true;
|
|
718
|
-
}
|
|
760
|
+
if (event.event === "quiz completed" && event.score_percent !== undefined) {
|
|
761
|
+
event.score_percent = Math.min(100, event.score_percent + 8);
|
|
719
762
|
}
|
|
720
763
|
});
|
|
721
764
|
}
|
|
722
765
|
|
|
723
|
-
//
|
|
766
|
+
// HOOK 6: SEMESTER-END SPIKE — duplicate quiz/assignment events
|
|
767
|
+
// in days 75-85 window. No flag — discover via line chart.
|
|
724
768
|
const duplicates = [];
|
|
769
|
+
const spikableEvents = ["quiz started", "quiz completed", "assignment submitted"];
|
|
725
770
|
userEvents.forEach((event) => {
|
|
726
|
-
if (event.
|
|
727
|
-
const
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
771
|
+
if (spikableEvents.includes(event.event) && event.time) {
|
|
772
|
+
const dayInDataset = dayjs.utc(event.time).diff(datasetStart, 'days', true);
|
|
773
|
+
if (dayInDataset >= 75 && dayInDataset <= 85 && chance.bool({ likelihood: 80 })) {
|
|
774
|
+
const dup = JSON.parse(JSON.stringify(event));
|
|
775
|
+
dup.time = dayjs(event.time).add(chance.integer({ min: 5, max: 120 }), 'minutes').toISOString();
|
|
776
|
+
duplicates.push(dup);
|
|
777
|
+
}
|
|
731
778
|
}
|
|
732
779
|
});
|
|
733
|
-
if (duplicates.length > 0)
|
|
734
|
-
userEvents.push(...duplicates);
|
|
735
|
-
}
|
|
780
|
+
if (duplicates.length > 0) userEvents.push(...duplicates);
|
|
736
781
|
|
|
737
|
-
//
|
|
782
|
+
// HOOK 7: FREE VS PAID — free users lose 55% of certificates.
|
|
738
783
|
const subStatus = profile ? profile.subscription_status : "free";
|
|
739
784
|
if (subStatus === "free") {
|
|
740
|
-
// Free users lose 55% of their certificates (simulating lower completion)
|
|
741
785
|
for (let i = userEvents.length - 1; i >= 0; i--) {
|
|
742
786
|
if (userEvents[i].event === "certificate earned" && chance.bool({ likelihood: 55 })) {
|
|
743
787
|
userEvents.splice(i, 1);
|
|
@@ -745,62 +789,51 @@ const config = {
|
|
|
745
789
|
}
|
|
746
790
|
}
|
|
747
791
|
|
|
748
|
-
//
|
|
792
|
+
// HOOK 4: STUDY GROUP RETENTION — non-joiners with low scores lose
|
|
793
|
+
// all post-day-14 events. Joiners get extra cloned discussion events.
|
|
749
794
|
if (!joinedStudyGroupEarly && hasLowQuizScore) {
|
|
750
|
-
// Non-joiners with low scores: remove ALL events after day 14 from their first event (hard churn)
|
|
751
795
|
const churnCutoff = firstEventTime ? firstEventTime.add(14, 'days') : null;
|
|
752
796
|
for (let i = userEvents.length - 1; i >= 0; i--) {
|
|
753
|
-
|
|
754
|
-
if (churnCutoff && dayjs(evt.time).isAfter(churnCutoff)) {
|
|
797
|
+
if (churnCutoff && dayjs(userEvents[i].time).isAfter(churnCutoff)) {
|
|
755
798
|
userEvents.splice(i, 1);
|
|
756
799
|
}
|
|
757
800
|
}
|
|
758
801
|
} else if (joinedStudyGroupEarly) {
|
|
759
|
-
// Study group joiners keep all events and get bonus discussion_posted events
|
|
760
802
|
const lastEvent = userEvents[userEvents.length - 1];
|
|
761
803
|
const discussionTemplate = userEvents.find(e => e.event === "discussion posted");
|
|
762
804
|
if (lastEvent && discussionTemplate && chance.bool({ likelihood: 60 })) {
|
|
763
|
-
|
|
805
|
+
userEvents.push({
|
|
764
806
|
...discussionTemplate,
|
|
765
807
|
time: dayjs(lastEvent.time).add(chance.integer({ min: 1, max: 3 }), 'days').toISOString(),
|
|
766
808
|
user_id: lastEvent.user_id,
|
|
767
809
|
course_id: chance.pickone(courseIds),
|
|
768
810
|
post_type: chance.pickone(["question", "answer", "comment"]),
|
|
769
811
|
word_count: chance.integer({ min: 20, max: 400 }),
|
|
770
|
-
|
|
771
|
-
};
|
|
772
|
-
userEvents.push(bonusDiscussion);
|
|
812
|
+
});
|
|
773
813
|
}
|
|
774
814
|
}
|
|
775
815
|
|
|
776
|
-
//
|
|
777
|
-
//
|
|
778
|
-
//
|
|
779
|
-
|
|
816
|
+
// HOOK 2: DEADLINE CRAMMING — Sun/Mon assignment_submitted events
|
|
817
|
+
// flip is_late to true 60% of the time and quiz_completed score_percent
|
|
818
|
+
// drops 25 points. Mutates raw is_late + score_percent.
|
|
819
|
+
for (const event of userEvents) {
|
|
820
|
+
if (event.event === "assignment submitted" && event.time) {
|
|
821
|
+
const dow = new Date(event.time).getUTCDay();
|
|
822
|
+
if (dow === 0 || dow === 1) {
|
|
823
|
+
event.is_late = chance.bool({ likelihood: 60 });
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
}
|
|
780
827
|
userEvents.forEach((event) => {
|
|
781
828
|
if (event.event === "quiz completed" && event.time) {
|
|
782
|
-
const
|
|
783
|
-
if (
|
|
784
|
-
|
|
785
|
-
event.score_percent = Math.max(0, event.score_percent - 25);
|
|
786
|
-
}
|
|
829
|
+
const dow = new Date(event.time).getUTCDay();
|
|
830
|
+
if ((dow === 0 || dow === 1) && event.score_percent !== undefined) {
|
|
831
|
+
event.score_percent = Math.max(0, event.score_percent - 25);
|
|
787
832
|
}
|
|
788
833
|
}
|
|
789
834
|
});
|
|
790
835
|
}
|
|
791
836
|
|
|
792
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
793
|
-
// Hook #7: FREE VS PAID COURSES (funnel-pre)
|
|
794
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
795
|
-
if (type === "funnel-pre") {
|
|
796
|
-
// Hook #7: FREE VS PAID — prop tagging only
|
|
797
|
-
// conversionRate manipulation removed; the everything hook handles
|
|
798
|
-
// the completion gap by dropping 55% of free-user certificates
|
|
799
|
-
if (meta && meta.profile && meta.funnel) {
|
|
800
|
-
// no-op: kept for future prop-setting if needed
|
|
801
|
-
}
|
|
802
|
-
}
|
|
803
|
-
|
|
804
837
|
return record;
|
|
805
838
|
}
|
|
806
839
|
};
|