@ak--47/dungeon-master 1.2.3 → 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/CHANGELOG.md +42 -0
- package/README.md +51 -13
- package/dungeons/technical/ad-spend.js +2 -2
- package/dungeons/technical/anonymous-users.js +2 -2
- package/dungeons/technical/array-of-object-lookup.js +2 -4
- package/dungeons/technical/experiments.js +2 -2
- package/dungeons/technical/foobar.js +2 -2
- package/dungeons/technical/group-analytics.js +2 -2
- package/dungeons/technical/mirror-strategies.js +2 -2
- package/dungeons/technical/nested-objects.js +2 -2
- package/dungeons/technical/retention-cadence.js +2 -3
- package/dungeons/technical/sanity.js +2 -2
- package/dungeons/technical/scale-test.js +2 -2
- package/dungeons/technical/scd.js +2 -2
- package/dungeons/technical/simple.js +5 -6
- package/dungeons/technical/simplest-schema.json +5 -0
- package/dungeons/technical/simplest.js +2 -2
- package/dungeons/technical/text-generation.js +3 -3
- package/dungeons/vertical/ai-platform.js +858 -0
- package/dungeons/vertical/community.js +84 -40
- package/dungeons/vertical/crypto.js +830 -0
- package/dungeons/vertical/dating.js +744 -0
- package/dungeons/vertical/devtools.js +175 -69
- package/dungeons/vertical/ecommerce.js +242 -94
- package/dungeons/vertical/education.js +330 -313
- package/dungeons/vertical/fintech.js +442 -313
- package/dungeons/vertical/fitness.js +143 -61
- package/dungeons/vertical/food-delivery.js +327 -353
- package/dungeons/vertical/gaming.js +912 -382
- package/dungeons/vertical/healthcare.js +142 -63
- package/dungeons/vertical/insurance-application.js +170 -76
- package/dungeons/vertical/logistics.js +115 -20
- package/dungeons/vertical/marketplace.js +152 -58
- package/dungeons/vertical/media.js +248 -384
- package/dungeons/vertical/real-estate.js +781 -0
- package/dungeons/vertical/sass.js +255 -266
- package/dungeons/vertical/social.js +264 -206
- package/dungeons/vertical/travel.js +117 -41
- package/index.js +17 -17
- package/lib/core/config-validator.js +159 -31
- 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 +81 -47
- package/lib/templates/macro-presets.js +111 -0
- package/lib/templates/soup-presets.js +19 -36
- package/lib/utils/utils.js +71 -39
- package/package.json +8 -2
- package/scripts/smoke-test-all.mjs +162 -0
- package/scripts/verify-runner.mjs +72 -24
- package/types.d.ts +251 -51
- 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/user/.gitkeep +0 -0
- package/dungeons/vertical/community-schema.json +0 -579
- 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 -438
- 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/rpg-schema.json +0 -2491
- package/dungeons/vertical/rpg.js +0 -976
- package/dungeons/vertical/sass-schema.json +0 -3128
- package/dungeons/vertical/social-schema.json +0 -620
- package/dungeons/vertical/travel-schema.json +0 -580
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
const SEED = "harness-education";
|
|
3
3
|
const num_days = 100;
|
|
4
4
|
const num_users = 5_000;
|
|
5
|
-
const
|
|
5
|
+
const avg_events_per_user_per_day = 1.2;
|
|
6
6
|
let token = "your-mixpanel-token";
|
|
7
7
|
|
|
8
8
|
// ── env overrides ──
|
|
@@ -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,8 +316,10 @@ const problemIds = v.range(1, 601).map(n => `problem_${v.uid(6)}`);
|
|
|
192
316
|
const config = {
|
|
193
317
|
token,
|
|
194
318
|
seed: SEED,
|
|
195
|
-
|
|
196
|
-
|
|
319
|
+
datasetStart: "2026-01-01T00:00:00Z",
|
|
320
|
+
datasetEnd: "2026-04-28T23:59:59Z",
|
|
321
|
+
// numDays: num_days,
|
|
322
|
+
avgEventsPerUserPerDay: avg_events_per_user_per_day,
|
|
197
323
|
numUsers: num_users,
|
|
198
324
|
hasAnonIds: false,
|
|
199
325
|
hasSessionIds: true,
|
|
@@ -208,7 +334,6 @@ const config = {
|
|
|
208
334
|
hasCampaigns: false,
|
|
209
335
|
isAnonymous: false,
|
|
210
336
|
hasAdSpend: false,
|
|
211
|
-
percentUsersBornInDataset: 50,
|
|
212
337
|
hasAvatar: true,
|
|
213
338
|
concurrency: 1,
|
|
214
339
|
writeToDisk: false,
|
|
@@ -303,8 +428,6 @@ const config = {
|
|
|
303
428
|
"watch_time_mins": u.weighNumRange(3, 60, 0.8, 20),
|
|
304
429
|
"playback_speed": [0.75, 1.0, 1.0, 1.0, 1.25, 1.5, 2.0],
|
|
305
430
|
"notes_taken": [false, false, true],
|
|
306
|
-
"speed_learner": [false],
|
|
307
|
-
"thorough_learner": [false],
|
|
308
431
|
}
|
|
309
432
|
},
|
|
310
433
|
{
|
|
@@ -315,7 +438,6 @@ const config = {
|
|
|
315
438
|
"quiz_id": quizIds,
|
|
316
439
|
"quiz_type": ["practice", "graded", "final_exam"],
|
|
317
440
|
"question_count": u.weighNumRange(5, 50, 0.7, 15),
|
|
318
|
-
"semester_end_rush": [false],
|
|
319
441
|
}
|
|
320
442
|
},
|
|
321
443
|
{
|
|
@@ -327,9 +449,6 @@ const config = {
|
|
|
327
449
|
"score_percent": u.weighNumRange(0, 100, 1.2, 50),
|
|
328
450
|
"time_spent_mins": u.weighNumRange(3, 120, 0.6, 25),
|
|
329
451
|
"attempts": u.weighNumRange(1, 5, 0.5, 3),
|
|
330
|
-
"diligent_student": [false],
|
|
331
|
-
"speed_learner_effect": [false],
|
|
332
|
-
"semester_end_rush": [false],
|
|
333
452
|
}
|
|
334
453
|
},
|
|
335
454
|
{
|
|
@@ -341,8 +460,6 @@ const config = {
|
|
|
341
460
|
"submission_type": ["text", "code", "file", "project"],
|
|
342
461
|
"word_count": u.weighNumRange(100, 5000, 0.6, 500),
|
|
343
462
|
"is_late": [false, false, false, false, true],
|
|
344
|
-
"is_deadline_rush": [false],
|
|
345
|
-
"semester_end_rush": [false],
|
|
346
463
|
}
|
|
347
464
|
},
|
|
348
465
|
{
|
|
@@ -363,7 +480,6 @@ const config = {
|
|
|
363
480
|
"course_id": courseIds,
|
|
364
481
|
"post_type": ["question", "answer", "comment"],
|
|
365
482
|
"word_count": u.weighNumRange(10, 500, 0.6, 80),
|
|
366
|
-
"study_group_member": [false],
|
|
367
483
|
}
|
|
368
484
|
},
|
|
369
485
|
{
|
|
@@ -373,7 +489,6 @@ const config = {
|
|
|
373
489
|
"course_id": courseIds,
|
|
374
490
|
"completion_time_days": u.weighNumRange(7, 180, 0.5, 45),
|
|
375
491
|
"final_grade": u.weighNumRange(60, 100, 1.2, 30),
|
|
376
|
-
"diligent_student": [false],
|
|
377
492
|
}
|
|
378
493
|
},
|
|
379
494
|
{
|
|
@@ -437,7 +552,6 @@ const config = {
|
|
|
437
552
|
"difficulty": ["easy", "medium", "hard"],
|
|
438
553
|
"time_to_solve_sec": u.weighNumRange(10, 3600, 0.5, 300),
|
|
439
554
|
"hint_used": [false, false, true],
|
|
440
|
-
"independent_solver": [false],
|
|
441
555
|
}
|
|
442
556
|
},
|
|
443
557
|
],
|
|
@@ -512,12 +626,7 @@ const config = {
|
|
|
512
626
|
* 8. PLAYBACK SPEED CORRELATION: Speed learners paradoxically score higher; thorough learners get extended time
|
|
513
627
|
*/
|
|
514
628
|
hook: function (record, type, meta) {
|
|
515
|
-
|
|
516
|
-
const DATASET_START = NOW.subtract(num_days, 'days');
|
|
517
|
-
|
|
518
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
519
|
-
// Hook #1: STUDENT VS INSTRUCTOR PROFILES
|
|
520
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
629
|
+
// HOOK 1: STUDENT VS INSTRUCTOR PROFILES (user) — role-based attributes.
|
|
521
630
|
if (type === "user") {
|
|
522
631
|
if (record.account_type === "instructor") {
|
|
523
632
|
record.courses_created = chance.integer({ min: 1, max: 15 });
|
|
@@ -529,216 +638,135 @@ const config = {
|
|
|
529
638
|
}
|
|
530
639
|
}
|
|
531
640
|
|
|
532
|
-
//
|
|
533
|
-
//
|
|
534
|
-
//
|
|
535
|
-
|
|
536
|
-
if (record.event === "assignment submitted" && record.time) {
|
|
537
|
-
const eventDay = dayjs(record.time).day(); // 0 = Sunday, 1 = Monday
|
|
538
|
-
if (eventDay === 0 || eventDay === 1) {
|
|
539
|
-
record.is_deadline_rush = true;
|
|
540
|
-
// 60% chance of being late (only 40% on time)
|
|
541
|
-
record.is_late = !chance.bool({ likelihood: 40 });
|
|
542
|
-
} else {
|
|
543
|
-
record.is_deadline_rush = false;
|
|
544
|
-
record.is_late = !chance.bool({ likelihood: 80 });
|
|
545
|
-
}
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
// Quiz score penalty moved to everything hook (after churn removal)
|
|
549
|
-
// to avoid selection bias — the penalty was causing more Sun/Mon
|
|
550
|
-
// quiz-takers to trigger hasLowQuizScore churn, inflating their avg
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
554
|
-
// Hook #5: HINT DEPENDENCY
|
|
555
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
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.
|
|
556
645
|
if (type === "event") {
|
|
557
646
|
if (record.event === "practice problem solved") {
|
|
558
|
-
if (record.hint_used === true) {
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
}
|
|
563
|
-
record.independent_solver = false;
|
|
564
|
-
} else if (record.hint_used === false) {
|
|
565
|
-
// Independent solvers tackle harder problems
|
|
566
|
-
if (chance.bool({ likelihood: 40 })) {
|
|
567
|
-
record.difficulty = "hard";
|
|
568
|
-
record.independent_solver = true;
|
|
569
|
-
} else {
|
|
570
|
-
record.independent_solver = false;
|
|
571
|
-
}
|
|
572
|
-
} else {
|
|
573
|
-
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";
|
|
574
651
|
}
|
|
575
652
|
}
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
if (record.time) {
|
|
583
|
-
const eventTime = dayjs(record.time);
|
|
584
|
-
const dayInDataset = eventTime.diff(DATASET_START, 'days', true);
|
|
585
|
-
|
|
586
|
-
const spikableEvents = ["quiz started", "quiz completed", "assignment submitted"];
|
|
587
|
-
if (spikableEvents.includes(record.event)) {
|
|
588
|
-
if (dayInDataset >= 75 && dayInDataset <= 85) {
|
|
589
|
-
record.semester_end_rush = true;
|
|
590
|
-
} else {
|
|
591
|
-
record.semester_end_rush = false;
|
|
592
|
-
}
|
|
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));
|
|
593
659
|
}
|
|
594
660
|
}
|
|
595
661
|
}
|
|
596
662
|
|
|
597
|
-
//
|
|
598
|
-
//
|
|
599
|
-
//
|
|
600
|
-
if (type === "
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
record.thorough_learner = true;
|
|
614
|
-
// Extend watch time for thorough learners
|
|
615
|
-
if (record.watch_time_mins !== undefined) {
|
|
616
|
-
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();
|
|
617
679
|
}
|
|
618
|
-
} else {
|
|
619
|
-
record.speed_learner = false;
|
|
620
|
-
record.thorough_learner = false;
|
|
621
680
|
}
|
|
622
681
|
}
|
|
623
682
|
}
|
|
624
683
|
|
|
625
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
626
|
-
// Hook #3: NOTES-TAKERS SUCCEED
|
|
627
|
-
// Hook #4: STUDY GROUP RETENTION
|
|
628
|
-
// Hook #7: FREE VS PAID (funnel-pre handled below)
|
|
629
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
630
684
|
if (type === "everything") {
|
|
685
|
+
const datasetStart = dayjs.unix(meta.datasetStart);
|
|
631
686
|
const userEvents = record;
|
|
632
687
|
const profile = meta.profile;
|
|
633
688
|
const firstEventTime = userEvents.length > 0 ? dayjs(userEvents[0].time) : null;
|
|
634
689
|
|
|
635
|
-
// ── Stamp superProps from profile so they stay consistent per user ──
|
|
636
690
|
if (profile) {
|
|
637
691
|
userEvents.forEach((event) => {
|
|
638
692
|
if (profile.Platform !== undefined) event.Platform = profile.Platform;
|
|
639
693
|
});
|
|
640
694
|
}
|
|
641
695
|
|
|
642
|
-
// ---------------------------------------------------------------
|
|
643
|
-
// First pass: identify user patterns
|
|
644
|
-
// ---------------------------------------------------------------
|
|
645
696
|
let notesTakenCount = 0;
|
|
646
697
|
let joinedStudyGroupEarly = false;
|
|
647
698
|
let hasLowQuizScore = false;
|
|
699
|
+
let speedLectureCount = 0;
|
|
648
700
|
|
|
649
701
|
userEvents.forEach((event) => {
|
|
650
702
|
const eventTime = dayjs(event.time);
|
|
651
703
|
const daysSinceStart = firstEventTime ? eventTime.diff(firstEventTime, 'days', true) : 0;
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
if (event.event === "
|
|
655
|
-
|
|
656
|
-
}
|
|
657
|
-
|
|
658
|
-
// Hook #4: Check if user joined a study group within the first 10 days
|
|
659
|
-
if (event.event === "study group joined" && daysSinceStart <= 10) {
|
|
660
|
-
joinedStudyGroupEarly = true;
|
|
661
|
-
}
|
|
662
|
-
|
|
663
|
-
// Hook #4: Check for any quiz_completed with score < 60
|
|
664
|
-
if (event.event === "quiz completed" && event.score_percent < 60) {
|
|
665
|
-
hasLowQuizScore = true;
|
|
666
|
-
}
|
|
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++;
|
|
667
708
|
});
|
|
668
709
|
|
|
669
|
-
//
|
|
670
|
-
//
|
|
671
|
-
//
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
if (event.event === "quiz completed") {
|
|
678
|
-
if (event.score_percent !== undefined) {
|
|
679
|
-
event.score_percent = Math.min(100, event.score_percent + 20);
|
|
680
|
-
}
|
|
681
|
-
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));
|
|
682
718
|
}
|
|
683
719
|
});
|
|
684
|
-
|
|
685
|
-
// 40% chance to splice in an extra certificate_earned event
|
|
686
720
|
if (chance.bool({ likelihood: 40 })) {
|
|
687
721
|
const lastEvent = userEvents[userEvents.length - 1];
|
|
688
722
|
const certTemplate = userEvents.find(e => e.event === "certificate earned");
|
|
689
723
|
if (lastEvent && certTemplate) {
|
|
690
|
-
|
|
724
|
+
userEvents.push({
|
|
691
725
|
...certTemplate,
|
|
692
726
|
time: dayjs(lastEvent.time).add(chance.integer({ min: 1, max: 5 }), 'days').toISOString(),
|
|
693
727
|
user_id: lastEvent.user_id,
|
|
694
728
|
course_id: chance.pickone(courseIds),
|
|
695
729
|
completion_time_days: chance.integer({ min: 14, max: 90 }),
|
|
696
730
|
final_grade: chance.integer({ min: 80, max: 100 }),
|
|
697
|
-
|
|
698
|
-
};
|
|
699
|
-
userEvents.push(certEvent);
|
|
731
|
+
});
|
|
700
732
|
}
|
|
701
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
|
+
});
|
|
702
740
|
}
|
|
703
741
|
|
|
704
|
-
//
|
|
705
|
-
|
|
706
|
-
userEvents.forEach((event) => {
|
|
707
|
-
if (event.event === "lecture completed" && event.speed_learner === true) {
|
|
708
|
-
speedLectureCount++;
|
|
709
|
-
}
|
|
710
|
-
});
|
|
711
|
-
const isSpeedLearner = speedLectureCount >= 3;
|
|
712
|
-
|
|
713
|
-
if (isSpeedLearner) {
|
|
742
|
+
// HOOK 8 (cont): Speed learners (3+ lectures at 2.0x) score +8 on quizzes.
|
|
743
|
+
if (speedLectureCount >= 3) {
|
|
714
744
|
userEvents.forEach((event) => {
|
|
715
|
-
if (event.event === "quiz completed") {
|
|
716
|
-
|
|
717
|
-
event.score_percent = Math.min(100, event.score_percent + 8);
|
|
718
|
-
event.speed_learner_effect = true;
|
|
719
|
-
}
|
|
745
|
+
if (event.event === "quiz completed" && event.score_percent !== undefined) {
|
|
746
|
+
event.score_percent = Math.min(100, event.score_percent + 8);
|
|
720
747
|
}
|
|
721
748
|
});
|
|
722
749
|
}
|
|
723
750
|
|
|
724
|
-
//
|
|
751
|
+
// HOOK 6: SEMESTER-END SPIKE — duplicate quiz/assignment events
|
|
752
|
+
// in days 75-85 window. No flag — discover via line chart.
|
|
725
753
|
const duplicates = [];
|
|
754
|
+
const spikableEvents = ["quiz started", "quiz completed", "assignment submitted"];
|
|
726
755
|
userEvents.forEach((event) => {
|
|
727
|
-
if (event.
|
|
728
|
-
const
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
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
|
+
}
|
|
732
763
|
}
|
|
733
764
|
});
|
|
734
|
-
if (duplicates.length > 0)
|
|
735
|
-
userEvents.push(...duplicates);
|
|
736
|
-
}
|
|
765
|
+
if (duplicates.length > 0) userEvents.push(...duplicates);
|
|
737
766
|
|
|
738
|
-
//
|
|
767
|
+
// HOOK 7: FREE VS PAID — free users lose 55% of certificates.
|
|
739
768
|
const subStatus = profile ? profile.subscription_status : "free";
|
|
740
769
|
if (subStatus === "free") {
|
|
741
|
-
// Free users lose 55% of their certificates (simulating lower completion)
|
|
742
770
|
for (let i = userEvents.length - 1; i >= 0; i--) {
|
|
743
771
|
if (userEvents[i].event === "certificate earned" && chance.bool({ likelihood: 55 })) {
|
|
744
772
|
userEvents.splice(i, 1);
|
|
@@ -746,62 +774,51 @@ const config = {
|
|
|
746
774
|
}
|
|
747
775
|
}
|
|
748
776
|
|
|
749
|
-
//
|
|
777
|
+
// HOOK 4: STUDY GROUP RETENTION — non-joiners with low scores lose
|
|
778
|
+
// all post-day-14 events. Joiners get extra cloned discussion events.
|
|
750
779
|
if (!joinedStudyGroupEarly && hasLowQuizScore) {
|
|
751
|
-
// Non-joiners with low scores: remove ALL events after day 14 from their first event (hard churn)
|
|
752
780
|
const churnCutoff = firstEventTime ? firstEventTime.add(14, 'days') : null;
|
|
753
781
|
for (let i = userEvents.length - 1; i >= 0; i--) {
|
|
754
|
-
|
|
755
|
-
if (churnCutoff && dayjs(evt.time).isAfter(churnCutoff)) {
|
|
782
|
+
if (churnCutoff && dayjs(userEvents[i].time).isAfter(churnCutoff)) {
|
|
756
783
|
userEvents.splice(i, 1);
|
|
757
784
|
}
|
|
758
785
|
}
|
|
759
786
|
} else if (joinedStudyGroupEarly) {
|
|
760
|
-
// Study group joiners keep all events and get bonus discussion_posted events
|
|
761
787
|
const lastEvent = userEvents[userEvents.length - 1];
|
|
762
788
|
const discussionTemplate = userEvents.find(e => e.event === "discussion posted");
|
|
763
789
|
if (lastEvent && discussionTemplate && chance.bool({ likelihood: 60 })) {
|
|
764
|
-
|
|
790
|
+
userEvents.push({
|
|
765
791
|
...discussionTemplate,
|
|
766
792
|
time: dayjs(lastEvent.time).add(chance.integer({ min: 1, max: 3 }), 'days').toISOString(),
|
|
767
793
|
user_id: lastEvent.user_id,
|
|
768
794
|
course_id: chance.pickone(courseIds),
|
|
769
795
|
post_type: chance.pickone(["question", "answer", "comment"]),
|
|
770
796
|
word_count: chance.integer({ min: 20, max: 400 }),
|
|
771
|
-
|
|
772
|
-
};
|
|
773
|
-
userEvents.push(bonusDiscussion);
|
|
797
|
+
});
|
|
774
798
|
}
|
|
775
799
|
}
|
|
776
800
|
|
|
777
|
-
//
|
|
778
|
-
//
|
|
779
|
-
//
|
|
780
|
-
|
|
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
|
+
}
|
|
781
812
|
userEvents.forEach((event) => {
|
|
782
813
|
if (event.event === "quiz completed" && event.time) {
|
|
783
|
-
const
|
|
784
|
-
if (
|
|
785
|
-
|
|
786
|
-
event.score_percent = Math.max(0, event.score_percent - 25);
|
|
787
|
-
}
|
|
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);
|
|
788
817
|
}
|
|
789
818
|
}
|
|
790
819
|
});
|
|
791
820
|
}
|
|
792
821
|
|
|
793
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
794
|
-
// Hook #7: FREE VS PAID COURSES (funnel-pre)
|
|
795
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
796
|
-
if (type === "funnel-pre") {
|
|
797
|
-
// Hook #7: FREE VS PAID — prop tagging only
|
|
798
|
-
// conversionRate manipulation removed; the everything hook handles
|
|
799
|
-
// the completion gap by dropping 55% of free-user certificates
|
|
800
|
-
if (meta && meta.profile && meta.funnel) {
|
|
801
|
-
// no-op: kept for future prop-setting if needed
|
|
802
|
-
}
|
|
803
|
-
}
|
|
804
|
-
|
|
805
822
|
return record;
|
|
806
823
|
}
|
|
807
824
|
};
|