@ak--47/dungeon-master 1.3.0 → 1.3.1
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/technical/array-of-object-lookup.js +0 -2
- 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 +192 -133
- package/dungeons/vertical/community.js +64 -34
- package/dungeons/vertical/crypto.js +333 -224
- package/dungeons/vertical/dating.js +228 -282
- package/dungeons/vertical/devtools.js +164 -62
- package/dungeons/vertical/ecommerce.js +240 -91
- package/dungeons/vertical/education.js +328 -310
- package/dungeons/vertical/fintech.js +437 -311
- package/dungeons/vertical/fitness.js +141 -58
- package/dungeons/vertical/food-delivery.js +298 -318
- package/dungeons/vertical/gaming.js +378 -231
- package/dungeons/vertical/healthcare.js +140 -60
- package/dungeons/vertical/insurance-application.js +168 -73
- package/dungeons/vertical/logistics.js +95 -6
- package/dungeons/vertical/marketplace.js +137 -51
- package/dungeons/vertical/media.js +241 -386
- package/dungeons/vertical/real-estate.js +322 -315
- package/dungeons/vertical/sass.js +253 -263
- package/dungeons/vertical/social.js +262 -206
- package/dungeons/vertical/travel.js +59 -16
- package/index.js +17 -17
- package/lib/core/config-validator.js +93 -21
- package/lib/core/context.js +10 -24
- package/lib/core/storage.js +6 -1
- package/lib/generators/events.js +11 -14
- package/lib/generators/funnels.js +12 -4
- 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/orchestrators/user-loop.js +47 -47
- package/lib/utils/utils.js +71 -39
- package/package.json +1 -1
- package/scripts/smoke-test-all.mjs +162 -0
- package/scripts/verify-runner.mjs +72 -24
- package/types.d.ts +38 -15
- 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
|
@@ -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)
|
|
144
270
|
*
|
|
145
|
-
*
|
|
146
|
-
*
|
|
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)?
|
|
271
|
+
* PATTERN: Annual subscribers complete the course-completion funnel
|
|
272
|
+
* 1.4x faster (factor 0.71); Free users 1.4x slower (factor 1.4).
|
|
151
273
|
*
|
|
152
|
-
*
|
|
153
|
-
* - By education level: PhD vs self-taught hook patterns
|
|
154
|
-
* - By learning style: visual vs hands-on note-taking rates
|
|
155
|
-
* - By platform: mobile vs desktop playback speed preferences
|
|
156
|
-
* - By course category: CS vs Arts hint usage
|
|
274
|
+
* HOW TO FIND IT IN MIXPANEL:
|
|
157
275
|
*
|
|
158
|
-
*
|
|
159
|
-
*
|
|
160
|
-
*
|
|
161
|
-
*
|
|
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
|
|
281
|
+
*
|
|
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
|
|
@@ -192,7 +316,9 @@ const problemIds = v.range(1, 601).map(n => `problem_${v.uid(6)}`);
|
|
|
192
316
|
const config = {
|
|
193
317
|
token,
|
|
194
318
|
seed: SEED,
|
|
195
|
-
|
|
319
|
+
datasetStart: "2026-01-01T00:00:00Z",
|
|
320
|
+
datasetEnd: "2026-04-28T23:59:59Z",
|
|
321
|
+
// numDays: num_days,
|
|
196
322
|
avgEventsPerUserPerDay: avg_events_per_user_per_day,
|
|
197
323
|
numUsers: num_users,
|
|
198
324
|
hasAnonIds: false,
|
|
@@ -302,8 +428,6 @@ const config = {
|
|
|
302
428
|
"watch_time_mins": u.weighNumRange(3, 60, 0.8, 20),
|
|
303
429
|
"playback_speed": [0.75, 1.0, 1.0, 1.0, 1.25, 1.5, 2.0],
|
|
304
430
|
"notes_taken": [false, false, true],
|
|
305
|
-
"speed_learner": [false],
|
|
306
|
-
"thorough_learner": [false],
|
|
307
431
|
}
|
|
308
432
|
},
|
|
309
433
|
{
|
|
@@ -314,7 +438,6 @@ const config = {
|
|
|
314
438
|
"quiz_id": quizIds,
|
|
315
439
|
"quiz_type": ["practice", "graded", "final_exam"],
|
|
316
440
|
"question_count": u.weighNumRange(5, 50, 0.7, 15),
|
|
317
|
-
"semester_end_rush": [false],
|
|
318
441
|
}
|
|
319
442
|
},
|
|
320
443
|
{
|
|
@@ -326,9 +449,6 @@ const config = {
|
|
|
326
449
|
"score_percent": u.weighNumRange(0, 100, 1.2, 50),
|
|
327
450
|
"time_spent_mins": u.weighNumRange(3, 120, 0.6, 25),
|
|
328
451
|
"attempts": u.weighNumRange(1, 5, 0.5, 3),
|
|
329
|
-
"diligent_student": [false],
|
|
330
|
-
"speed_learner_effect": [false],
|
|
331
|
-
"semester_end_rush": [false],
|
|
332
452
|
}
|
|
333
453
|
},
|
|
334
454
|
{
|
|
@@ -340,8 +460,6 @@ const config = {
|
|
|
340
460
|
"submission_type": ["text", "code", "file", "project"],
|
|
341
461
|
"word_count": u.weighNumRange(100, 5000, 0.6, 500),
|
|
342
462
|
"is_late": [false, false, false, false, true],
|
|
343
|
-
"is_deadline_rush": [false],
|
|
344
|
-
"semester_end_rush": [false],
|
|
345
463
|
}
|
|
346
464
|
},
|
|
347
465
|
{
|
|
@@ -362,7 +480,6 @@ const config = {
|
|
|
362
480
|
"course_id": courseIds,
|
|
363
481
|
"post_type": ["question", "answer", "comment"],
|
|
364
482
|
"word_count": u.weighNumRange(10, 500, 0.6, 80),
|
|
365
|
-
"study_group_member": [false],
|
|
366
483
|
}
|
|
367
484
|
},
|
|
368
485
|
{
|
|
@@ -372,7 +489,6 @@ const config = {
|
|
|
372
489
|
"course_id": courseIds,
|
|
373
490
|
"completion_time_days": u.weighNumRange(7, 180, 0.5, 45),
|
|
374
491
|
"final_grade": u.weighNumRange(60, 100, 1.2, 30),
|
|
375
|
-
"diligent_student": [false],
|
|
376
492
|
}
|
|
377
493
|
},
|
|
378
494
|
{
|
|
@@ -436,7 +552,6 @@ const config = {
|
|
|
436
552
|
"difficulty": ["easy", "medium", "hard"],
|
|
437
553
|
"time_to_solve_sec": u.weighNumRange(10, 3600, 0.5, 300),
|
|
438
554
|
"hint_used": [false, false, true],
|
|
439
|
-
"independent_solver": [false],
|
|
440
555
|
}
|
|
441
556
|
},
|
|
442
557
|
],
|
|
@@ -511,12 +626,7 @@ const config = {
|
|
|
511
626
|
* 8. PLAYBACK SPEED CORRELATION: Speed learners paradoxically score higher; thorough learners get extended time
|
|
512
627
|
*/
|
|
513
628
|
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
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
629
|
+
// HOOK 1: STUDENT VS INSTRUCTOR PROFILES (user) — role-based attributes.
|
|
520
630
|
if (type === "user") {
|
|
521
631
|
if (record.account_type === "instructor") {
|
|
522
632
|
record.courses_created = chance.integer({ min: 1, max: 15 });
|
|
@@ -528,216 +638,135 @@ const config = {
|
|
|
528
638
|
}
|
|
529
639
|
}
|
|
530
640
|
|
|
531
|
-
//
|
|
532
|
-
//
|
|
533
|
-
//
|
|
534
|
-
|
|
535
|
-
if (record.event === "assignment submitted" && record.time) {
|
|
536
|
-
const eventDay = dayjs(record.time).day(); // 0 = Sunday, 1 = Monday
|
|
537
|
-
if (eventDay === 0 || eventDay === 1) {
|
|
538
|
-
record.is_deadline_rush = true;
|
|
539
|
-
// 60% chance of being late (only 40% on time)
|
|
540
|
-
record.is_late = !chance.bool({ likelihood: 40 });
|
|
541
|
-
} else {
|
|
542
|
-
record.is_deadline_rush = false;
|
|
543
|
-
record.is_late = !chance.bool({ likelihood: 80 });
|
|
544
|
-
}
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
// Quiz score penalty moved to everything hook (after churn removal)
|
|
548
|
-
// to avoid selection bias — the penalty was causing more Sun/Mon
|
|
549
|
-
// quiz-takers to trigger hasLowQuizScore churn, inflating their avg
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
553
|
-
// Hook #5: HINT DEPENDENCY
|
|
554
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
641
|
+
// HOOK 5 (event): HINT DEPENDENCY — hint users get 60% easy problems;
|
|
642
|
+
// non-hint users get 40% hard problems. Mutates difficulty (raw).
|
|
643
|
+
// HOOK 8 (event): PLAYBACK SPEED — speed learners (>= 2.0x) get
|
|
644
|
+
// watch_time_mins compressed 0.6x; thorough learners (<= 1.0x) get 1.4x.
|
|
555
645
|
if (type === "event") {
|
|
556
646
|
if (record.event === "practice problem solved") {
|
|
557
|
-
if (record.hint_used === true) {
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
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;
|
|
647
|
+
if (record.hint_used === true && chance.bool({ likelihood: 60 })) {
|
|
648
|
+
record.difficulty = "easy";
|
|
649
|
+
} else if (record.hint_used === false && chance.bool({ likelihood: 40 })) {
|
|
650
|
+
record.difficulty = "hard";
|
|
573
651
|
}
|
|
574
652
|
}
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
if (record.time) {
|
|
582
|
-
const eventTime = dayjs.utc(record.time);
|
|
583
|
-
const dayInDataset = eventTime.diff(DATASET_START, 'days', true);
|
|
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
|
-
}
|
|
653
|
+
if (record.event === "lecture completed") {
|
|
654
|
+
const speed = record.playback_speed;
|
|
655
|
+
if (speed >= 2.0 && record.watch_time_mins !== undefined) {
|
|
656
|
+
record.watch_time_mins = Math.max(3, Math.floor(record.watch_time_mins * 0.6));
|
|
657
|
+
} else if (speed !== undefined && speed <= 1.0 && record.watch_time_mins !== undefined) {
|
|
658
|
+
record.watch_time_mins = Math.min(90, Math.floor(record.watch_time_mins * 1.4));
|
|
592
659
|
}
|
|
593
660
|
}
|
|
594
661
|
}
|
|
595
662
|
|
|
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));
|
|
663
|
+
// HOOK 9 (T2C): COURSE COMPLETION TIME-TO-CONVERT (funnel-post)
|
|
664
|
+
// Annual subscribers complete the Course Completion funnel 1.4x
|
|
665
|
+
// faster (factor 0.71); Free users 1.4x slower (factor 1.4).
|
|
666
|
+
if (type === "funnel-post") {
|
|
667
|
+
const segment = meta?.profile?.subscription_status;
|
|
668
|
+
if (Array.isArray(record) && record.length > 1) {
|
|
669
|
+
const factor = (
|
|
670
|
+
segment === "annual" ? 0.71 :
|
|
671
|
+
segment === "free" ? 1.4 :
|
|
672
|
+
1.0
|
|
673
|
+
);
|
|
674
|
+
if (factor !== 1.0) {
|
|
675
|
+
for (let i = 1; i < record.length; i++) {
|
|
676
|
+
const prev = dayjs(record[i - 1].time);
|
|
677
|
+
const newGap = Math.round(dayjs(record[i].time).diff(prev) * factor);
|
|
678
|
+
record[i].time = prev.add(newGap, "milliseconds").toISOString();
|
|
616
679
|
}
|
|
617
|
-
} else {
|
|
618
|
-
record.speed_learner = false;
|
|
619
|
-
record.thorough_learner = false;
|
|
620
680
|
}
|
|
621
681
|
}
|
|
622
682
|
}
|
|
623
683
|
|
|
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
684
|
if (type === "everything") {
|
|
685
|
+
const datasetStart = dayjs.unix(meta.datasetStart);
|
|
630
686
|
const userEvents = record;
|
|
631
687
|
const profile = meta.profile;
|
|
632
688
|
const firstEventTime = userEvents.length > 0 ? dayjs(userEvents[0].time) : null;
|
|
633
689
|
|
|
634
|
-
// ── Stamp superProps from profile so they stay consistent per user ──
|
|
635
690
|
if (profile) {
|
|
636
691
|
userEvents.forEach((event) => {
|
|
637
692
|
if (profile.Platform !== undefined) event.Platform = profile.Platform;
|
|
638
693
|
});
|
|
639
694
|
}
|
|
640
695
|
|
|
641
|
-
// ---------------------------------------------------------------
|
|
642
|
-
// First pass: identify user patterns
|
|
643
|
-
// ---------------------------------------------------------------
|
|
644
696
|
let notesTakenCount = 0;
|
|
645
697
|
let joinedStudyGroupEarly = false;
|
|
646
698
|
let hasLowQuizScore = false;
|
|
699
|
+
let speedLectureCount = 0;
|
|
647
700
|
|
|
648
701
|
userEvents.forEach((event) => {
|
|
649
702
|
const eventTime = dayjs(event.time);
|
|
650
703
|
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
|
-
}
|
|
704
|
+
if (event.event === "lecture completed" && event.notes_taken === true) notesTakenCount++;
|
|
705
|
+
if (event.event === "study group joined" && daysSinceStart <= 10) joinedStudyGroupEarly = true;
|
|
706
|
+
if (event.event === "quiz completed" && event.score_percent < 60) hasLowQuizScore = true;
|
|
707
|
+
if (event.event === "lecture completed" && event.playback_speed >= 2.0) speedLectureCount++;
|
|
666
708
|
});
|
|
667
709
|
|
|
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;
|
|
710
|
+
// HOOK 3 + HOOK 10: NOTES MAGIC NUMBER (in-funnel, no flags)
|
|
711
|
+
// Sweet 5-8 notes-taken lectures → +30% quiz score_percent (cap 100).
|
|
712
|
+
// Over 9+ → drop 35% of certificate-earned events (over-noted but
|
|
713
|
+
// can't synthesize; gets stuck in "study mode").
|
|
714
|
+
if (notesTakenCount >= 5 && notesTakenCount <= 8) {
|
|
715
|
+
userEvents.forEach((event) => {
|
|
716
|
+
if (event.event === "quiz completed" && event.score_percent !== undefined) {
|
|
717
|
+
event.score_percent = Math.min(100, Math.round(event.score_percent * 1.3));
|
|
681
718
|
}
|
|
682
719
|
});
|
|
683
|
-
|
|
684
|
-
// 40% chance to splice in an extra certificate_earned event
|
|
685
720
|
if (chance.bool({ likelihood: 40 })) {
|
|
686
721
|
const lastEvent = userEvents[userEvents.length - 1];
|
|
687
722
|
const certTemplate = userEvents.find(e => e.event === "certificate earned");
|
|
688
723
|
if (lastEvent && certTemplate) {
|
|
689
|
-
|
|
724
|
+
userEvents.push({
|
|
690
725
|
...certTemplate,
|
|
691
726
|
time: dayjs(lastEvent.time).add(chance.integer({ min: 1, max: 5 }), 'days').toISOString(),
|
|
692
727
|
user_id: lastEvent.user_id,
|
|
693
728
|
course_id: chance.pickone(courseIds),
|
|
694
729
|
completion_time_days: chance.integer({ min: 14, max: 90 }),
|
|
695
730
|
final_grade: chance.integer({ min: 80, max: 100 }),
|
|
696
|
-
|
|
697
|
-
};
|
|
698
|
-
userEvents.push(certEvent);
|
|
731
|
+
});
|
|
699
732
|
}
|
|
700
733
|
}
|
|
734
|
+
} else if (notesTakenCount >= 9) {
|
|
735
|
+
userEvents.forEach(e => {
|
|
736
|
+
if (e.event === "certificate earned" && typeof e.final_grade === "number") {
|
|
737
|
+
e.final_grade = Math.max(50, Math.round(e.final_grade * 0.5));
|
|
738
|
+
}
|
|
739
|
+
});
|
|
701
740
|
}
|
|
702
741
|
|
|
703
|
-
//
|
|
704
|
-
|
|
705
|
-
userEvents.forEach((event) => {
|
|
706
|
-
if (event.event === "lecture completed" && event.speed_learner === true) {
|
|
707
|
-
speedLectureCount++;
|
|
708
|
-
}
|
|
709
|
-
});
|
|
710
|
-
const isSpeedLearner = speedLectureCount >= 3;
|
|
711
|
-
|
|
712
|
-
if (isSpeedLearner) {
|
|
742
|
+
// HOOK 8 (cont): Speed learners (3+ lectures at 2.0x) score +8 on quizzes.
|
|
743
|
+
if (speedLectureCount >= 3) {
|
|
713
744
|
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
|
-
}
|
|
745
|
+
if (event.event === "quiz completed" && event.score_percent !== undefined) {
|
|
746
|
+
event.score_percent = Math.min(100, event.score_percent + 8);
|
|
719
747
|
}
|
|
720
748
|
});
|
|
721
749
|
}
|
|
722
750
|
|
|
723
|
-
//
|
|
751
|
+
// HOOK 6: SEMESTER-END SPIKE — duplicate quiz/assignment events
|
|
752
|
+
// in days 75-85 window. No flag — discover via line chart.
|
|
724
753
|
const duplicates = [];
|
|
754
|
+
const spikableEvents = ["quiz started", "quiz completed", "assignment submitted"];
|
|
725
755
|
userEvents.forEach((event) => {
|
|
726
|
-
if (event.
|
|
727
|
-
const
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
756
|
+
if (spikableEvents.includes(event.event) && event.time) {
|
|
757
|
+
const dayInDataset = dayjs.utc(event.time).diff(datasetStart, 'days', true);
|
|
758
|
+
if (dayInDataset >= 75 && dayInDataset <= 85 && chance.bool({ likelihood: 80 })) {
|
|
759
|
+
const dup = JSON.parse(JSON.stringify(event));
|
|
760
|
+
dup.time = dayjs(event.time).add(chance.integer({ min: 5, max: 120 }), 'minutes').toISOString();
|
|
761
|
+
duplicates.push(dup);
|
|
762
|
+
}
|
|
731
763
|
}
|
|
732
764
|
});
|
|
733
|
-
if (duplicates.length > 0)
|
|
734
|
-
userEvents.push(...duplicates);
|
|
735
|
-
}
|
|
765
|
+
if (duplicates.length > 0) userEvents.push(...duplicates);
|
|
736
766
|
|
|
737
|
-
//
|
|
767
|
+
// HOOK 7: FREE VS PAID — free users lose 55% of certificates.
|
|
738
768
|
const subStatus = profile ? profile.subscription_status : "free";
|
|
739
769
|
if (subStatus === "free") {
|
|
740
|
-
// Free users lose 55% of their certificates (simulating lower completion)
|
|
741
770
|
for (let i = userEvents.length - 1; i >= 0; i--) {
|
|
742
771
|
if (userEvents[i].event === "certificate earned" && chance.bool({ likelihood: 55 })) {
|
|
743
772
|
userEvents.splice(i, 1);
|
|
@@ -745,62 +774,51 @@ const config = {
|
|
|
745
774
|
}
|
|
746
775
|
}
|
|
747
776
|
|
|
748
|
-
//
|
|
777
|
+
// HOOK 4: STUDY GROUP RETENTION — non-joiners with low scores lose
|
|
778
|
+
// all post-day-14 events. Joiners get extra cloned discussion events.
|
|
749
779
|
if (!joinedStudyGroupEarly && hasLowQuizScore) {
|
|
750
|
-
// Non-joiners with low scores: remove ALL events after day 14 from their first event (hard churn)
|
|
751
780
|
const churnCutoff = firstEventTime ? firstEventTime.add(14, 'days') : null;
|
|
752
781
|
for (let i = userEvents.length - 1; i >= 0; i--) {
|
|
753
|
-
|
|
754
|
-
if (churnCutoff && dayjs(evt.time).isAfter(churnCutoff)) {
|
|
782
|
+
if (churnCutoff && dayjs(userEvents[i].time).isAfter(churnCutoff)) {
|
|
755
783
|
userEvents.splice(i, 1);
|
|
756
784
|
}
|
|
757
785
|
}
|
|
758
786
|
} else if (joinedStudyGroupEarly) {
|
|
759
|
-
// Study group joiners keep all events and get bonus discussion_posted events
|
|
760
787
|
const lastEvent = userEvents[userEvents.length - 1];
|
|
761
788
|
const discussionTemplate = userEvents.find(e => e.event === "discussion posted");
|
|
762
789
|
if (lastEvent && discussionTemplate && chance.bool({ likelihood: 60 })) {
|
|
763
|
-
|
|
790
|
+
userEvents.push({
|
|
764
791
|
...discussionTemplate,
|
|
765
792
|
time: dayjs(lastEvent.time).add(chance.integer({ min: 1, max: 3 }), 'days').toISOString(),
|
|
766
793
|
user_id: lastEvent.user_id,
|
|
767
794
|
course_id: chance.pickone(courseIds),
|
|
768
795
|
post_type: chance.pickone(["question", "answer", "comment"]),
|
|
769
796
|
word_count: chance.integer({ min: 20, max: 400 }),
|
|
770
|
-
|
|
771
|
-
};
|
|
772
|
-
userEvents.push(bonusDiscussion);
|
|
797
|
+
});
|
|
773
798
|
}
|
|
774
799
|
}
|
|
775
800
|
|
|
776
|
-
//
|
|
777
|
-
//
|
|
778
|
-
//
|
|
779
|
-
|
|
801
|
+
// HOOK 2: DEADLINE CRAMMING — Sun/Mon assignment_submitted events
|
|
802
|
+
// flip is_late to true 60% of the time and quiz_completed score_percent
|
|
803
|
+
// drops 25 points. Mutates raw is_late + score_percent.
|
|
804
|
+
for (const event of userEvents) {
|
|
805
|
+
if (event.event === "assignment submitted" && event.time) {
|
|
806
|
+
const dow = new Date(event.time).getUTCDay();
|
|
807
|
+
if (dow === 0 || dow === 1) {
|
|
808
|
+
event.is_late = chance.bool({ likelihood: 60 });
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
}
|
|
780
812
|
userEvents.forEach((event) => {
|
|
781
813
|
if (event.event === "quiz completed" && event.time) {
|
|
782
|
-
const
|
|
783
|
-
if (
|
|
784
|
-
|
|
785
|
-
event.score_percent = Math.max(0, event.score_percent - 25);
|
|
786
|
-
}
|
|
814
|
+
const dow = new Date(event.time).getUTCDay();
|
|
815
|
+
if ((dow === 0 || dow === 1) && event.score_percent !== undefined) {
|
|
816
|
+
event.score_percent = Math.max(0, event.score_percent - 25);
|
|
787
817
|
}
|
|
788
818
|
}
|
|
789
819
|
});
|
|
790
820
|
}
|
|
791
821
|
|
|
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
822
|
return record;
|
|
805
823
|
}
|
|
806
824
|
};
|